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.
Designed for high-performance production environments:
This architecture ensures consistent response times and eliminates external service bottlenecks, making it ideal for high-traffic environments and air-gapped deployments.
The plugin is designed to provide detailed observability through Traefik access logs by adding headers to the request (not the response). This means:
Recommended Approach: Using
logStatusHeaderandlogStatusDetailHeaderwith 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-inlogBannedRequestsfeature only logs blocked requests and is considered a legacy approach.
| Config Setting | Purpose | Example Values |
|---|---|---|
countryHeader | Country code of the request origin | US, DE, PRIVATE |
logStatusHeader | Simple pass/block status | pass, block |
logStatusDetailHeader | Detailed decision with reason | pass:allowed_country, block:blocked_country |
Simple status indicating whether the request was allowed or blocked:
pass - Request was allowed throughblock - Request was blockedDetailed 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:
| Value | Description | Example Scenario |
|---|---|---|
pass:ignore_verb | HTTP method is in ignoreVerbs list | OPTIONS request for CORS preflight |
pass:excluded_regex | Request matched excludedPathsRegex pattern | Health check endpoint /health |
pass:bypass_header | Request had matching bypassHeaders header/value | Internal service with secret header |
Geo-rule pass reasons (request allowed by geo-blocking rules):
These indicate why the geo-blocking logic allowed the request:
| Value | Description | Example Scenario |
|---|---|---|
pass:allow_private | IP is private/internal (RFC 1918) and allowPrivate is true | Request from 192.168.1.100 |
pass:allowed_ip_block | IP matched a CIDR range in allowedIPBlocks or allowedIPBlocksDir | Trusted partner IP 203.0.113.50 |
pass:allowed_country | Country code is in allowedCountries list | US user when allowedCountries: ["US"] |
pass:default_allow | No rules matched and defaultAllow is true | Unknown country with permissive config |
pass:none | No IP addresses found to evaluate | Misconfigured ipHeaders |
Block reasons (request denied):
| Value | Description | Example Scenario |
|---|---|---|
block:allow_private | IP is private/internal but allowPrivate is false | Internal IP blocked by strict config |
block:blocked_ip_block | IP matched a CIDR range in blockedIPBlocks or blockedIPBlocksDir | Known bad actor IP range |
block:blocked_country | Country code is in blockedCountries list | Request from blocked region |
block:default_allow | No rules matched and defaultAllow is false | Unknown country with strict config |
block:error | IP lookup failed and banIfError is true | Database lookup failure |
accessLog:filePath: "/var/log/traefik/access.log"format: jsonfields:headers:names:X-IPCountry: keepX-Geoblock-Status: keepX-Geoblock-Decision: keep
This gives you full visibility into geoblocking decisions without exposing internal logic to clients.
โ ๏ธ 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
unsafepackage - 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.Pointerfor 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 conversionreturn *(*string)(unsafe.Pointer(&b))
It is possible to install the plugin locally or to install it through Traefik Plugins.
Create or modify your Traefik static configuration
experimental:localPlugins:geoblock:moduleName: github.com/david-garcia-garcia/traefik-geoblocksettins:useunsafe: true# REQUIRED: Enable unsafe operations for this pluginplugins:geoblock:settings:useunsafe: true
You should clone the plugin into the container, i.e
# Create the directory for the pluginsRUN set -eux; \mkdir -p /plugins-local/src/github.com/david-garcia-garciaRUN 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
Add to your Traefik static configuration:
experimental:plugins:geoblock:moduleName: github.com/david-garcia-garcia/traefik-geoblockversion: v1.0.1# REQUIRED: Enable unsafe operations for this pluginsettings:useunsafe: true
For automatic database updates to function, ensure your firewall allows outbound HTTPS connections to:
download.ip2location.comwww.ip2location.comNote: If automatic updates are disabled (
databaseAutoUpdate: false), no external network access is required and the plugin operates entirely offline.
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 testsgo test# Run integration tests.\Test-Integration.ps
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 Composeenvironment:- TRAEFIK_PLUGIN_GEOBLOCK_PATH=/data/geoblock# Docker rundocker run -e TRAEFIK_PLUGIN_GEOBLOCK_PATH=/data/geoblock traefik:latest# System environment variableexport 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.
version: "3.7"services:traefik:image: traefik:v3.5.3 # v3.5.0 or later requiredcommand:# 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.BINports:- "80:80"- "443:443"whoami:image: traefik/whoamilabels:- "traefik.enable=true"- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"- "traefik.http.routers.whoami.middlewares=geoblock@file"
http:middlewares:geoblock:plugin:geoblock:#-------------------------------# Core Settings#-------------------------------enabled: true # Enable/disable the plugin entirelydefaultAllow: 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 KingdomblockedCountries: # 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 instancesallowedIPBlocksDir: "/data/allowed-ips/" # Directory with .txt files containing allowed CIDR blocksblockedIPBlocksDir: "/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 headeripHeaderStrategy: "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 foundignoreVerbs: # 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 entirelyX-Internal-Request: "true"X-Skip-Geoblock: "1"X-Cdn-Auth: "mysupersecretkey"#-------------------------------# Error Handling and ban#-------------------------------banIfError: true # Block requests if IP lookup failsdisallowedStatusCode: 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, errorlogFormat: "json" # Available: json, textlogPath: "/var/log/geoblock.log" # Empty for Traefik's standard outputlogBannedRequests: 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 informationlogStatusHeader: "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=keeplogStatusDetailHeader: "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.
The plugin processes requests in the following order:
Important Notes:
CheckAll strategy: If any IP in the chain is blocked, the request is deniedCheckFirst or CheckFirstNonePrivate strategies: Only the selected IP(s) are evaluated; the request is denied only if the selected IP is blockedignoreVerbs skip all blocking logic but still receive GeoIP enrichmentexcludedPathsRegex skip all blocking logic but still receive GeoIP enrichmentNote: This section documents the built-in
logBannedRequestsfeature which only logs blocked requests. For comprehensive observability of both allowed and blocked requests, uselogStatusHeaderandlogStatusDetailHeaderwith 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 formatlevel: Log level (debug, info, warn, error)msg: Log message describing the actionplugin: Plugin identifierip: The IP address that triggered the actionip_chain: Full chain of IP addresses from X-Forwarded-For headercountry: Country code or "PRIVATE" for internal networkshost: Request host headermethod: HTTP method usedphase: Processing phase where the action occurred:
allow_private: Private network checkblocked_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 rulepath: Request pathExample 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.