/geoblock

geoblock

36
v1.1.4

๐Ÿ›ก๏ธ Traefik Geoblock Plugin

Build Status Go Report Card Latest GitHub release License

A Traefik plugin that allows or blocks requests based on IP geolocation using IP2Location database.

[!TIP]

Traefik Security

The basic middlewares you need to secure your Traefik ingress:

๐ŸŒ Geoblock: david-garcia-garcia/traefik-geoblock - Block or allow requests based on IP geolocation
๐Ÿ›ก๏ธ CrowdSec: maxlerebourg/crowdsec-bouncer-traefik-plugin - Real-time threat intelligence and automated blocking
๐Ÿ”’ ModSecurity CRS: david-garcia-garcia/traefik-modsecurity - Web Application Firewall with OWASP Core Rule Set
๐Ÿšฆ Ratelimit: Traefik Rate Limit - Control request rates and prevent abuse

[!WARNING]

You should not run middlewares as Yaegi plugins in production.

Traefik's default plugin system runs plugins via Yaegi (a Go interpreter) at runtime. Middlewares run on every request, so they sit on the hot path. Using an interpreter for that workload has concrete drawbacks related to memory management, CPU usage and observability (see feat: improve pprof experience by adding wrappers to interpreted functions by david-garcia-garcia ยท Pull Request #1712 ยท traefik/yaegi)

For production deployments where middlewares handle substantial traffic, use a Traefik build that compiles those middlewares into the binary instead of loading them as Yaegi plugins such as in david-garcia-garcia/traefik-with-plugins: Traefik container with preloaded plugins in it

For more details and discussion, read Traefik issue #12213 in the Traefik issue queue.

Performance & Scalability

Designed for high-performance production environments:

  • No external API calls - All geolocation lookups are performed using local IP2Location database files, ensuring zero latency from external services
  • Minimal memory footprint - No internal caching mechanisms; leverages the IP2Location library's efficient binary database format for direct lookups
  • Zero network dependencies - Once configured, operates entirely offline with no external service dependencies
  • Hot-swappable database updates - Database updates occur without middleware restart or service interruption

This architecture ensures consistent response times and eliminates external service bottlenecks, making it ideal for high-traffic environments and air-gapped deployments.

Performance & Scalability

Observability

The plugin is designed to provide detailed observability through Traefik access logs by adding headers to the request (not the response). This means:

  • Headers are visible in Traefik access logs but not sent back to clients
  • You can track geolocation and blocking decisions for all traffic
  • Useful for security analysis, debugging, and compliance reporting

Recommended Approach: Using logStatusHeader and logStatusDetailHeader with Traefik access logs is the recommended way to observe plugin behavior. This provides complete visibility into both allowed and blocked requests with detailed decision reasons. The built-in logBannedRequests feature only logs blocked requests and is considered a legacy approach.

Available Headers

Config SettingPurposeExample Values
countryHeaderCountry code of the request originUS, DE, PRIVATE
logStatusHeaderSimple pass/block statuspass, block
logStatusDetailHeaderDetailed decision with reasonpass:allowed_country, block:blocked_country

logStatusHeader Values

Simple status indicating whether the request was allowed or blocked:

  • pass - Request was allowed through
  • block - Request was blocked

logStatusDetailHeader Values

Detailed status with format {action}:{reason}:

Bypass reasons (blocking skipped, checked first):

These are evaluated before geo-blocking rules. If any match, the request passes without geo evaluation:

ValueDescriptionExample Scenario
pass:ignore_verbHTTP method is in ignoreVerbs listOPTIONS request for CORS preflight
pass:excluded_regexRequest matched excludedPathsRegex patternHealth check endpoint /health
pass:bypass_headerRequest had matching bypassHeaders header/valueInternal service with secret header

Geo-rule pass reasons (request allowed by geo-blocking rules):

These indicate why the geo-blocking logic allowed the request:

ValueDescriptionExample Scenario
pass:allow_privateIP is private/internal (RFC 1918) and allowPrivate is trueRequest from 192.168.1.100
pass:allowed_ip_blockIP matched a CIDR range in allowedIPBlocks or allowedIPBlocksDirTrusted partner IP 203.0.113.50
pass:allowed_countryCountry code is in allowedCountries listUS user when allowedCountries: ["US"]
pass:default_allowNo rules matched and defaultAllow is trueUnknown country with permissive config
pass:noneNo IP addresses found to evaluateMisconfigured ipHeaders

Block reasons (request denied):

ValueDescriptionExample Scenario
block:allow_privateIP is private/internal but allowPrivate is falseInternal IP blocked by strict config
block:blocked_ip_blockIP matched a CIDR range in blockedIPBlocks or blockedIPBlocksDirKnown bad actor IP range
block:blocked_countryCountry code is in blockedCountries listRequest from blocked region
block:default_allowNo rules matched and defaultAllow is falseUnknown country with strict config
block:errorIP lookup failed and banIfError is trueDatabase lookup failure

Traefik Access Log Configuration

accessLog:
filePath: "/var/log/traefik/access.log"
format: json
fields:
headers:
names:
X-IPCountry: keep
X-Geoblock-Status: keep
X-Geoblock-Decision: keep

This gives you full visibility into geoblocking decisions without exposing internal logic to clients.

Features

  • Block or allow requests based on country of origin (using ISO 3166-1 alpha-2 country codes)
  • Whitelist specific IP ranges (CIDR notation) - supports both inline configuration and directory-based files
  • Blacklist specific IP ranges (CIDR notation) - supports both inline configuration and directory-based files
  • Optional bypass using custom headers
  • Configurable handling of private/internal networks
  • Customizable error responses
  • Flexible logging options
  • Hot-swap database updates - automatic IP2Location database updates with zero downtime
  • Path exclusion via regex - exclude specific paths from geoblocking while maintaining GeoIP enrichment

Installation

โš ๏ธ IMPORTANT REQUIREMENTS

  • Traefik v3.5.0 or later is required (unsafe support was introduced in v3.5.0)

  • Unsafe operations must be enabled in Traefik configuration

    Why "Unsafe" Mode is Required

    Traefik may display this plugin as "unsafe", which can be misleading. This does not mean the plugin is dangerous or insecure.

    What "unsafe" actually means:

    Traefik plugins run inside Yaegi, a Go interpreter that sandboxes plugin code for security. By default, Yaegi restricts access to Go's unsafe package - a low-level Go standard library package used for memory operations and performance optimizations.

    Why this plugin needs it:

    This plugin depends on the ip2location-go library, which uses unsafe.Pointer for efficient byte-to-string conversions when reading the binary database file. This is a common Go performance optimization pattern that avoids unnecessary memory allocations during IP lookups.

    // Example from ip2location library - efficient string conversion
    return *(*string)(unsafe.Pointer(&b))

It is possible to install the plugin locally or to install it through Traefik Plugins.

Local Plugin Installation

Create or modify your Traefik static configuration

experimental:
localPlugins:
geoblock:
moduleName: github.com/david-garcia-garcia/traefik-geoblock
settins:
useunsafe: true
# REQUIRED: Enable unsafe operations for this plugin
plugins:
geoblock:
settings:
useunsafe: true

You should clone the plugin into the container, i.e

# Create the directory for the plugins
RUN set -eux; \
mkdir -p /plugins-local/src/github.com/david-garcia-garcia
RUN set -eux && git clone https://github.com/david-garcia-garcia/traefik-geoblock /plugins-local/src/github.com/david-garcia-garcia/traefik-geoblock --branch v1.0.1 --single-branch

Traefik Plugin Registry Installation

Add to your Traefik static configuration:

experimental:
plugins:
geoblock:
moduleName: github.com/david-garcia-garcia/traefik-geoblock
version: v1.0.1
# REQUIRED: Enable unsafe operations for this plugin
settings:
useunsafe: true

Network Requirements

For automatic database updates to function, ensure your firewall allows outbound HTTPS connections to:

  • download.ip2location.com
  • www.ip2location.com

Note: If automatic updates are disabled (databaseAutoUpdate: false), no external network access is required and the plugin operates entirely offline.

Testing and development

You can spin up a fully working environment with docker compose:

docker compose up --build

The codebase includes a full set of integration and unit tests:

# Run unit tests
go test
# Run integration tests
.\Test-Integration.ps

Configuration

Environment Variables

The plugin supports the following environment variable for configuration:

  • TRAEFIK_PLUGIN_GEOBLOCK_PATH: Directory path used as fallback location for database and HTML files when they are not found in the specified paths or when paths are empty/omitted.

Example usage:

# Docker Compose
environment:
- TRAEFIK_PLUGIN_GEOBLOCK_PATH=/data/geoblock
# Docker run
docker run -e TRAEFIK_PLUGIN_GEOBLOCK_PATH=/data/geoblock traefik:latest
# System environment variable
export TRAEFIK_PLUGIN_GEOBLOCK_PATH=/opt/traefik-plugins/geoblock

When this environment variable is set, the plugin will automatically look for IP2LOCATION-LITE-DB1.IPV6.BIN and geoblockban.html files in the specified directory if they are not found in their configured locations.

Example Docker Compose Setup

version: "3.7"
services:
traefik:
image: traefik:v3.5.3 # v3.5.0 or later required
command:
# REQUIRED: Enable unsafe operations for geoblock plugin
- "--experimental.plugins.geoblock.settings.useunsafe=true"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yml:/etc/traefik/traefik.yml
- ./dynamic-config.yml:/etc/traefik/dynamic-config.yml
- ./IP2LOCATION-LITE-DB1.IPV6.BIN:/plugins-storage/IP2LOCATION-LITE-DB1.IPV6.BIN
ports:
- "80:80"
- "443:443"
whoami:
image: traefik/whoami
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
- "traefik.http.routers.whoami.middlewares=geoblock@file"

Dynamic Configuration

http:
middlewares:
geoblock:
plugin:
geoblock:
#-------------------------------
# Core Settings
#-------------------------------
enabled: true # Enable/disable the plugin entirely
defaultAllow: false # Default behavior when no rules match (false = block)
#-------------------------------
# Database Configuration
#-------------------------------
databaseFilePath: "/plugins-local/src/github.com/david-garcia-garcia/traefik-geoblock/IP2LOCATION-LITE-DB1.IPV6.BIN"
# Can be:
# - Full path: /path/to/IP2LOCATION-LITE-DB1.IPV6.BIN
# - Directory: /path/to/ (will search for IP2LOCATION-LITE-DB1.IPV6.BIN recursively).
# Use /plugins-storage/sources/ if you are installing from plugin repository.
# - Empty: automatically searches using fallback locations (see below)
#
# Fallback search order when file is not found:
# 1. TRAEFIK_PLUGIN_GEOBLOCK_PATH environment variable directory
#-------------------------------
# Country-based Rules (ISO 3166-1 alpha-2 format)
#-------------------------------
allowedCountries: # Whitelist of countries to allow
- "US" # United States
- "CA" # Canada
- "GB" # United Kingdom
blockedCountries: # Blacklist of countries to block
- "RU" # Russia
- "CN" # China
#-------------------------------
# Network Rules
#-------------------------------
allowPrivate: true # Allow requests from private/internal networks (marked as "PRIVATE")
# This includes RFC 1918 private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
# and loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6)
allowedIPBlocks: # CIDR ranges to always allow (highest priority)
- "192.168.0.0/16"
- "10.0.0.0/8"
- "2001:db8::/32"
blockedIPBlocks: # CIDR ranges to always block
- "203.0.113.0/24"
# More specific ranges (longer prefix) take precedence
# Directory-based IP blocks (loaded once during plugin initialization)
# This is useful if you mount configmaps in your traefik plugin
# so that these will be shared among all Geoip middleware instances
allowedIPBlocksDir: "/data/allowed-ips/" # Directory with .txt files containing allowed CIDR blocks
blockedIPBlocksDir: "/data/blocked-ips/" # Directory with .txt files containing blocked CIDR blocks
# All .txt files in the directory are scanned recursively during plugin startup
# Each .txt file should contain one CIDR block per line (comments with # supported)
# Note: Changes to files require plugin restart to take effect
# Example file content:
# # AWS IP ranges
# 172.16.0.0/12
# 203.0.113.0/24
#-------------------------------
# IP Extraction Configuration
#-------------------------------
ipHeaders: # List of headers to check for client IP addresses (required, cannot be empty)
- "x-forwarded-for" # Default: check X-Forwarded-For header first
- "x-real-ip" # Default: check X-Real-IP header second
# Custom examples:
# - "cf-connecting-ip" # Cloudflare
# - "x-client-ip" # Custom proxy
# - "remoteAddress" # SYNTHETIC: Maps to req.RemoteAddr (direct connection IP)
#
# IMPORTANT: Header order matters! IPs are processed in the order headers are defined.
# Within each header, IPs are processed left-to-right (leftmost = original client IP).
# Duplicate IPs are automatically removed, preserving the first occurrence.
#
# SYNTHETIC HEADERS:
# - "remoteAddress": Special synthetic header that maps to req.RemoteAddr field
# This provides access to the actual network connection's remote address
# Useful when you need to check the direct connection IP alongside proxy headers
#
# Example configurations:
# ipHeaders: ["x-forwarded-for", "remoteAddress"] # Check proxy header first, then direct connection
# ipHeaders: ["remoteAddress"] # Only check direct connection IP
# ipHeaders: ["remoteAddress", "x-real-ip"] # Check direct connection first, then proxy header
ipHeaderStrategy: "CheckAll" # Strategy for processing multiple IP addresses (default: CheckAll)
# Options:
# - "CheckAll": Check all IPs found in headers (original behavior)
# - "CheckFirst": Check only the first IP address found
# - "CheckFirstNonePrivate": Check first non-private IP, fallback to first private IP if no public IPs found
ignoreVerbs: # List of HTTP verbs to ignore for blocking (still enriched with GeoIP)
- "OPTIONS" # Common for CORS preflight requests
- "HEAD" # Common for health checks
# Additional examples:
# - "TRACE" # HTTP TRACE method
# - "CONNECT" # HTTP CONNECT method
# Note: Verb matching is case-insensitive
#-------------------------------
# Path Exclusion
#-------------------------------
excludedPathsRegex: "^[^/]*/api/.*"
# Regular expression to match requests that should skip geoblocking
# Matching requests will NOT be blocked but will still receive GeoIP enrichment (countryHeader)
#
# MATCHING FORMAT: The regex matches against "{host}{path}"
# - host: The Host header value from the request
# - path: The URL path starting with / (no query string)
# - Example: request to example.com/api/users -> matches "example.com/api/users"
# - Note: Host header typically excludes port for standard ports (80/443)
# but may include port for non-standard ports (e.g., "example.com:8080")
#
# This is useful for:
# - API endpoints that have their own authentication/authorization
# - Domain-specific exclusions
# - Public endpoints that should bypass geoblocking
# Note: For health checks, using bypassHeaders is recommended as it's more secure
# (requires a secret header value rather than just matching a public URL path)
#
# Note: Go's regexp uses RE2 which guarantees linear time complexity,
# making it inherently safe from ReDoS (Regular Expression Denial of Service) attacks.
#
# Examples:
# - "^[^/]*/health$" # /health on any domain
# - "^[^/]*/(health|ready|live)$" # Health check paths on any domain
# - "^[^/]*/api/.*" # All /api/* paths on any domain
# - "^api\\.example\\.com/.*" # All paths on api.example.com
# - "^internal\\..*/(health|metrics)$" # /health or /metrics on internal.* subdomains
# - "/health$" # /health path (partial match, any domain)
#-------------------------------
# Bypass Configuration
#-------------------------------
bypassHeaders: # Headers that skip geoblocking entirely
X-Internal-Request: "true"
X-Skip-Geoblock: "1"
X-Cdn-Auth: "mysupersecretkey"
#-------------------------------
# Error Handling and ban
#-------------------------------
banIfError: true # Block requests if IP lookup fails
disallowedStatusCode: 403 # HTTP status code for blocked requests. If you are using banHtmlFilePath make sure to set this to a valid code (such as NOT 204).
banHtmlFilePath: "/plugins-local/src/github.com/david-garcia-garcia/traefik-geoblock/geoblockban.html"
# Can be:
# - Full path: /path/to/geoblockban.html
# - Directory: /path/to/ (will search for geoblockban.html recursively). Use /plugins-storage/sources/ if you are installing from plugin repository.
# - Empty: returns only status code
#
# Fallback search order when file is not found:
# 1. TRAEFIK_PLUGIN_GEOBLOCK_PATH environment variable directory
# Template variables available: {{.IP}} and {{.Country}}
#-------------------------------
# Logging Configuration
#-------------------------------
logLevel: "info" # Available: debug, info, warn, error
logFormat: "json" # Available: json, text
logPath: "/var/log/geoblock.log" # Empty for Traefik's standard output
logBannedRequests: true # Log blocked requests. They will be logged at info level.
# NOTE: logBannedRequests is not the recommended way to observe plugin behavior.
# Use logStatusHeader and logStatusDetailHeader with Traefik access logs instead,
# which provides visibility into both allowed AND blocked requests with detailed reasons.
fileLogBufferSizeBytes: 1024 # Buffer size for file logging in bytes (default: 1024)
fileLogBufferTimeoutSeconds: 2 # Buffer timeout for file logging in seconds (default: 2)
# File logging uses buffered writes for better performance. The buffer is flushed when:
# - The buffer reaches fileLogBufferSizeBytes size
# - fileLogBufferTimeoutSeconds seconds have passed since the last flush
# - The logger is closed/shutdown
#-------------------------------
# Database Auto-Update Settings
#-------------------------------
databaseAutoUpdate: true
# Enable automatic database updates with hot-swapping. Updates check every 24 hours
# and immediately on startup if the current database is older than 1 month.
# Updated databases are hot-swapped without requiring middleware restart.
# Make sure you whitelist in your FW domains ["download.ip2location.com", "www.ip2location.com"]
databaseAutoUpdateDir: "/data/ip2database"
# Directory to store updated databases. This must be a persistent volume in the traefik pod.
# The plugin uses a singleton pattern - multiple middlewares with identical configurations
# share the same database factory and hot-swap operations.
databaseAutoUpdateToken: "" # IP2Location download token (if using premium)
databaseAutoUpdateCode: "DB1" # Database product code to download (if using premium)
#-------------------------------
# Request header settings
#-------------------------------
countryHeader: "X-IPCountry"
# Optional header to add the country code to the REQUEST (available in Traefik access logs)
# This header is added to the request that gets forwarded to your backend service
# You can use this to see where all your traffic is coming from in access logs
# Example access log config: accesslog.fields.headers.names.X-IPCountry=keep
# Note: Header is initially set to "PRIVATE" and only overridden by the first real country found
# This ensures private IPs processed later cannot override legitimate country information
logStatusHeader: "X-Geoblock-Status"
# Optional header to add simple pass/block status to the REQUEST
# Values: "pass" or "block"
# Useful for quick filtering in logs without parsing the detailed reason
# Example access log config: accesslog.fields.headers.names.X-Geoblock-Status=keep
logStatusDetailHeader: "X-Geoblock-Decision"
# Optional header to add detailed decision result to the REQUEST
# Format: "pass:{reason}" or "block:{reason}"
# See the Observability section for all possible values
# Example access log config: accesslog.fields.headers.names.X-Geoblock-Decision=keep
# DEPRECATED - use logStatusHeader/logStatusDetailHeader instead
# remediationHeadersCustomName: "X-Geoblock-Action"
# This added a header to the RESPONSE which exposed internal details to clients.
# The new headers add to the REQUEST so they're visible in access logs but not sent to clients.

Processing Order

The plugin processes requests in the following order:

  1. Check if plugin is enabled
  2. Check bypass headers
  3. Check if HTTP verb is in ignoreVerbs list (skip blocking but continue enrichment)
  4. Check if request matches excludedPathsRegex (skip blocking but continue enrichment)
  5. Extract IP addresses from configured IP headers (ipHeaders) in the order they are defined
  6. Apply IP header strategy (ipHeaderStrategy) to determine which IPs to process:
    • CheckAll: Process all found IP addresses (original behavior)
    • CheckFirst: Process only the first IP address found
    • CheckFirstNonePrivate: Process first non-private IP, fallback to first private IP if no public IPs found
  7. For each selected IP:
    • Check if it's in private network range [allowPrivate]
    • Check allowed/blocked IP blocks [allowedIPBlocks + allowedIPBlocksDir, blockedIPBlocks + blockedIPBlocksDir] (most specific match wins)
    • Look up country code
    • Check allowed/blocked countries [allowedCountries, blockedCountries]
    • Apply default allow/deny if no rules match [defaultAllow]

Important Notes:

  • With CheckAll strategy: If any IP in the chain is blocked, the request is denied
  • With CheckFirst or CheckFirstNonePrivate strategies: Only the selected IP(s) are evaluated; the request is denied only if the selected IP is blocked
  • Country header behavior: Header is initially set to "PRIVATE" and only overridden by the first real country found, preventing private IPs from overriding legitimate geolocation information
  • Ignored HTTP verbs: Requests using verbs in ignoreVerbs skip all blocking logic but still receive GeoIP enrichment
  • Excluded paths: Requests matching excludedPathsRegex skip all blocking logic but still receive GeoIP enrichment

Log Format (Legacy)

Note: This section documents the built-in logBannedRequests feature which only logs blocked requests. For comprehensive observability of both allowed and blocked requests, use logStatusHeader and logStatusDetailHeader with Traefik access logs instead. See the Observability section for details.

When using JSON logging, the following fields are included in blocked request log entries (note: allowed requests are not logged):

  • time: Timestamp of the request in ISO 8601 format
  • level: Log level (debug, info, warn, error)
  • msg: Log message describing the action
  • plugin: Plugin identifier
  • ip: The IP address that triggered the action
  • ip_chain: Full chain of IP addresses from X-Forwarded-For header
  • country: Country code or "PRIVATE" for internal networks
  • host: Request host header
  • method: HTTP method used
  • phase: Processing phase where the action occurred:
    • allow_private: Private network check
    • blocked_ip_block: IP block rules check (blocked)
    • allowed_ip_block: IP block rules check (allowed)
    • blocked_country: Country rules check (blocked)
    • allowed_country: Country rules check (allowed)
    • default_allow: Default allow/deny rule
  • path: Request path

Example log entry:

{
"time": "2025-03-01T19:24:04.414051815Z",
"level": "INFO",
"msg": "blocked request",
"plugin": "geoblock@docker",
"ip": "172.18.0.1",
"ip_chain": "",
"country": "PRIVATE",
"host": "localhost:8000",
"method": "GET",
"phase": "allow_private",
"path": "/bar"
}

๐Ÿ“„ This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

๐ŸŒ This project includes IP2Location LITE data available from lite.ip2location.com.