Traefik IP Geolocation Plugin for IPGeolocation.io

Overview

Geo-blocking, VPN and proxy detection, and IP intelligence headers for Traefik, powered by IPGeolocation.io MMDB databases. This Traefik middleware plugin reads the databases from local disk and answers every lookup inside the Traefik process, with no API calls and no per-request cost.

Use it to block traffic by country, stop Tor exit nodes, VPNs, proxies and known attackers at the edge, route visitors by region, and add country, city, ASN and threat data to requests and access logs.

# Block Tor and known attackers, refuse high risk addresses, tell the backend where visitors are
http:
  middlewares:
    geo:
      plugin:
        ipgeolocation:
          databases:
            - /etc/traefik/ipgeo/db-ip-location.mmdb
            - /etc/traefik/ipgeo/db-ip-security.mmdb
          headerPreset: standard
          blockTor: true
          blockKnownAttacker: true
          blockThreatScoreAbove: 80

Jump to: Installation | Quick start | Databases | Configuration | Fields | Client IP | Troubleshooting | FAQ


Why use this plugin

Most Traefik geolocation setups call a remote API on every request or put a GeoIP library inside each application. This plugin loads IPGeolocation.io databases into Traefik once and answers each lookup locally in about ten microseconds ( benchmarks).

  • No API calls and no per-request billing. Every lookup is a read from a local file.
  • Decisions at the edge. Block, redirect or route in Traefik before the request reaches your backend.
  • One middleware, many databases. Location, Security, Company, ASN, Abuse Contact, Hosting and Residential Proxy all load through one list, and the plugin layers them.
  • Works with every database tier. Security v1, v3 and v4 all resolve without configuration changes.
  • Zero dependencies. The MMDB reader is written using only the Go standard library, so it runs inside Traefik's plugin interpreter.
  • Safe by default. Forwarded headers are ignored unless you trust them, private addresses skip the lookup, failures fail open, and client supplied X-IPGeo-* headers are stripped.

How it works

You list one or more .mmdb files under databases. The plugin opens each file at startup and, for every request:

  1. Resolves the client IP address (see Client IP selection).
  2. Looks that address up in each database, in the order you declared them.
  3. For each field, takes the value from the first database that has it. Declaration order is priority order.
  4. Sets the headers you asked for, evaluates the rules you enabled, then forwards or blocks.

Because the first match wins, databases layer: load a Location and a Security database together, and country_code comes from the first while is_vpn comes from the second.


Requirements

  • Traefik v3.x (tested against v3.3). Traefik v2.x is not supported.
  • One or more IPGeolocation.io .mmdb databases, or an API key for API mode.
  • Read access to the database files from inside the Traefik process or container.

No Go toolchain is needed. Traefik downloads and interprets the plugin.


Installation


1. Declare the plugin in the static configuration

# traefik.yml
experimental:
  plugins:
    ipgeolocation:
      moduleName: github.com/IPGeolocation/traefik-plugin-ipgeolocation
      version: v1.0.0

The command line equivalents are --experimental.plugins.ipgeolocation.modulename=... and --experimental.plugins.ipgeolocation.version=v1.0.0 . The Traefik plugin documentation covers how plugins are loaded at startup.


2. Mount the databases

The plugin reads files from the Traefik container's filesystem, so mount them with a volume such as ./ipgeo:/etc/traefik/ipgeo:ro . Database paths in your configuration are then resolved inside the container, not on the host. If Traefik logs cannot open ...: no such file or directory at startup, check the mount and the path first. Troubleshooting covers the other common startup problems.


3. Attach the middleware to a router

Define a middleware in your dynamic configuration and reference it from a router, as in the Quick start below. Middleware basics are in the Traefik middleware overview.


Quick start


1. Add geolocation headers for your backend

http:
  routers:
    my-app:
      rule: Host(`example.com`)
      service: my-app
      middlewares: [geo-enrich]

  middlewares:
    geo-enrich:
      plugin:
        ipgeolocation:
          databases:
            - /etc/traefik/ipgeo/db-ip-location.mmdb
            - /etc/traefik/ipgeo/db-ip-asn.mmdb
          headerPreset: standard

Your backend receives X-IPGeo-Country-Code: US , X-IPGeo-City-Name: Philadelphia , X-IPGeo-ASN: AS1257 and the rest of the standard preset. The field reference lists every name you can ask for.


2. Block countries in Traefik

Middleware definitions below go under http.middlewares , as in the example above.

us-and-canada-only:
  plugin:
    ipgeolocation:
      databases: [/etc/traefik/ipgeo/db-ip-location.mmdb]
      allowedCountries: [US, CA]
      blockStatusCode: 451
      blockMessage: This service is not available in your region.

3. Stop VPNs, proxies and attackers in front of a login page

login-shield:
  plugin:
    ipgeolocation:
      databases: [/etc/traefik/ipgeo/db-ip-security.mmdb]
      blockTor: true
      blockKnownAttacker: true
      blockResidentialProxy: true
      blockThreatScoreAbove: 80

Put your monitoring and office ranges in allowedIPs. They bypass every rule, so a policy change can never lock you out of your own service.


4. Docker labels

labels:
  - "traefik.http.middlewares.geo.plugin.ipgeolocation.databases[0]=/etc/traefik/ipgeo/db-ip-location.mmdb"
  - "traefik.http.middlewares.geo.plugin.ipgeolocation.blockedCountries[0]=KP"
  - "traefik.http.routers.my-app.middlewares=geo"

On Kubernetes, the same keys go under spec.plugin.ipgeolocation in a Traefik Middleware resource.


Getting the databases

Download the .mmdb files from your IPGeolocation.io account and point databases at them. Each database has its own static download link that never changes, so the same link serves the first download and every update.

DatabaseFileWhat it adds
IP Geolocation db-ip-location.mmdb country, state, district, city, postal code, coordinates, time zone, currency
IP Security db-ip-security.mmdb threat score, Tor, VPN, proxy, relay, bot, spam, attacker and cloud flags
IP Company db-ip-company.mmdb company or ISP name, domain and type
IP to ASN db-ip-asn.mmdb AS number, organization, type, RIR, peers and routes in the Deep tier
IP Abuse Contact db-ip-abuse.mmdb abuse email, phone, address, route, country
Residential Proxy db-residential-proxy.mmdb residential proxy provider and last seen date
IP Hosting db-ip-hosting.mmdb hosting provider name

Load only what you need. Fields from a database you did not load stay empty, so referencing them is safe. Bundles that ship two databases in one archive work the same way: list both files. Tiers are on the pricing page, the field schemas show what each database contains.


Configuration reference

Every option is a camelCase key under plugin.ipgeolocation . The tables give the short version; OPTIONS.md explains what each value does in detail.


1. Data source

OptionDefaultDescription
mode mmdb mmdb reads local files, api calls the REST API
databases List of .mmdb paths. Declaration order is priority order
loadInMemory TickRead each file into RAM. Set false for multi gigabyte files, see Performance
refreshInterval 0 Reload a database when its file changes, for example 1h . See Keeping databases up to date

2. Enrichment

OptionDefaultDescription
headerPreset minimal none , minimal (country, city, ASN), standard (15 headers), full
headers Map of header name to field name, applied over the preset. An empty value removes a preset header
language en Localized names: en , de , ru , ko , pt , ja , fa , fr , zh , es , cs , it
listSeparator , Joins list fields such as VPN provider names
booleanFormat true_false one_zero emits 1 and 0 , like the Nginx module

Header names follow X-IPGeo-<Field-Name> , so country_code becomes X-IPGeo-Country-Code . Headers the plugin manages are deleted from the incoming request before enrichment, so your backend can trust them.


3. Access control

OptionDefaultDescription
allowedCountries / blockedCountries ISO 3166-1 alpha-2 codes. Set one list, not both
allowedContinents / blockedContinents AF , AN , AS , EU , NA , OC , SA
allowedASNs / blockedASNs AS1257 or 1257
allowedIPs CIDRs that skip every check. Evaluated first
blockedIPs CIDRs refused before any lookup
blockTor , blockVPN , blockProxy , blockRelay CrossAnonymizer flags from the Security database
blockResidentialProxy , blockAnonymous CrossResidential proxy and combined anonymity flags
blockKnownAttacker , blockSpam CrossThreat flags
blockBot CrossKnown bots. Crawlers marked as known good bots are spared, see Troubleshooting
blockKnownGoodBots CrossMake blockBot apply to search engines and monitors too
blockCloudProvider CrossCloud and hosting infrastructure. Also catches corporate VPN exits and CI runners
blockCorporateGateway CrossCorporate egress gateways (Security v4)
blockThreatScoreAbove -1 Block when the score is above this value, for example 80 . 0 counts as off, use 1 to block everything above zero
allowPrivate TickSkip private and loopback addresses instead of treating them as unknown
allowUnknown TickAllow addresses no database covers
dryRun CrossEvaluate and log without blocking

4. Block response and failure handling

OptionDefaultDescription
blockStatusCode 403 451 is conventional for legal restrictions
blockMessage Access denied. Plain text body
blockRedirectURL Send a 302 to this URL instead of a status. Do not point it at a route behind the same middleware
failOpen TickForward the request when a lookup fails. false blocks instead

5. Client IP, cache, API and logging

OptionDefaultDescription
trustForwardedHeader CrossRead the client address from a forwarded header, see Client IP selection
forwardedHeaderName X-Forwarded-For For example CF-Connecting-IP behind Cloudflare
forwardedDepth 0 Take the Nth address counting from the right. 0 means leftmost
trustedProxies Your proxy CIDRs. The first address from the right that is not yours is used
cacheSize 10000 Cached addresses. 0 disables the cache
cacheTTL 1h Lifetime of a cached lookup. Use 24h in API mode
apiKey , apiEndpoint , apiInclude , apiFields , apiTimeout API mode settings
logLevel info error , warn , info or debug

Field reference

Use any of these names in the headers map. They match the Nginx module's $ip_* variables without the prefix.

GroupFields
Location country_code , country_code3 , country_code_ioc , country_name , country_name_official , country_capital , continent_code , continent_name , state_code , state_name , district_name , city_name , zip_code , latitude , longitude , geoname_id , time_zone , accuracy_radius , confidence , dma_code , connection_type , is_eu
Country metadata currency_code , currency_name , currency_symbol , calling_code , languages , tld
Company company_name , company_domain , company_type , isp_name , organization_name
ASN asn , asn_number , asn_name , asn_organization , asn_country , asn_domain , asn_type , asn_rir , asn_date_allocated , asn_allocation_status , asn_routes , asn_peers , asn_upstreams , asn_downstreams
Security threat_score , is_tor , is_proxy , is_vpn , is_relay , is_residential_proxy , is_anonymous , is_known_attacker , is_bot , is_spam , is_cloud_provider , cloud_provider , proxy_type , proxy_provider , vpn_provider , relay_provider , proxy_confidence , vpn_confidence , proxy_last_seen , vpn_last_seen , is_known_good_bot , bot_type , bot_operator , bot_confidence , bot_last_seen , is_corporate_gateway , corporate_gateway_provider , corporate_gateway_type
Residential proxy and hosting residential_proxy_provider , residential_proxy_last_seen , hosting_provider
Abuse contact abuse_name , abuse_email , abuse_phone , abuse_address , abuse_country_code , abuse_kind , abuse_route
Special ip , the client address the plugin used

Booleans render as true or false (or 1 and 0 with booleanFormat: one_zero ), lists are joined with listSeparator , AS numbers are normalized to AS1257 , and a field with no data is omitted rather than sent empty.

The Residential Proxy and Hosting databases have no boolean column, since a record existing is the signal, so is_residential_proxy and is_cloud_provider become true when those databases contain the address. A flag from a database declared earlier still wins.


Real world examples


1. Route visitors to a regional backend

Map the fields you need to your own header names, then read them in the application:

headers:
  X-Geo-Country: country_code
  X-Geo-Currency: currency_code
  X-Geo-Language: languages

2. Different rules for different paths

Attach a lighter middleware to the whole site and a stricter one to sensitive routes:

http:
  routers:
    checkout:
      rule: Host(`example.com`) && PathPrefix(`/checkout`)
      middlewares: [geo-enrich, no-anonymizers]
      service: app

  middlewares:
    no-anonymizers:
      plugin:
        ipgeolocation:
          databases: [/etc/traefik/ipgeo/db-ip-security.mmdb]
          headerPreset: none
          blockVPN: true
          blockProxy: true
          blockTor: true
          blockResidentialProxy: true

3. Separate humans from infrastructure

headers:
  X-Is-Cloud: is_cloud_provider
  X-ASN-Type: asn_type
  X-Bot-Operator: bot_operator

Rate limit or serve a lighter page when X-Is-Cloud is true , and log X-Bot-Operator to see which crawlers visit, without blocking anyone.


4. Enrich access logs

Traefik logs any request header you name under accessLog.fields.headers.names , so X-IPGeo-Country-Code , X-IPGeo-ASN and X-IPGeo-Threat-Score flow straight into your log pipeline with no application changes.


Client IP selection

The plugin geolocates one address per request. By default that is the address of the TCP connection, which is correct when Traefik faces the internet directly. Behind a load balancer or CDN, every visitor would look like the balancer, so you need the forwarded header:

# Behind exactly one proxy you control
trustForwardedHeader: true
forwardedDepth: 1

# Behind a variable number of your own proxies
trustForwardedHeader: true
trustedProxies: [10.0.0.0/8, 172.16.0.0/12]

# Behind Cloudflare
trustForwardedHeader: true
forwardedHeaderName: CF-Connecting-IP

When testing locally, a plugin that loads cleanly but enriches nothing is almost always this. Set logLevel: debug and look for 127.0.0.1 is private or loopback, skipping the lookup .

Private, loopback, link local and carrier grade NAT ranges are absent from every public database. With allowPrivate: true they skip the lookup and pass through untouched.


Keeping databases up to date

IPGeolocation.io publishes fresh releases daily. The plugin never downloads anything itself. It watches the files you gave it, and a scheduled job replaces them:

refreshInterval: 1h

When a file's size or modification time changes, the plugin opens the new file, swaps it in atomically, clears the lookup cache, and closes the old one after a short grace period. If the new file will not open, it keeps serving the old one and logs a warning. No restart, no failed requests.

The download side is yours to run, from cron, a systemd timer, a Kubernetes CronJob or your existing pipeline. Each database has its own static link with its own apiKey parameter, and each archive holds the .mmdb , a README.md and a checksum.txt . A good refresh job verifies the checksum, confirms the file really is an MMDB before installing it, and handles bundles holding two databases.


Performance and memory

Measured with go test -bench on a 2.8 GHz Xeon, one request end to end through the middleware:

CasePer requestAllocations
Repeat visitor, served from cache3.8 µs21
Cache miss, headerPreset: minimal9.4 µs98
Cache miss, headerPreset: standard 18.5 µs248
Cache miss, headerPreset: full 44.6 µs409

Cost scales with the number of fields you resolve, not with database size. The plugin resolves only the fields your headers and rules reference, and picks a lookup strategy at startup to match: with eight fields or fewer it seeks to each field inside the record and skips the rest, and with more it decodes the record once.

Memory depends on loadInMemory. With true the whole file is resident and a lookup takes 9.9 µs. With false only the search tree stays in memory and each record is read from disk in one windowed read, at 11.1 µs. During a refresh with true , both copies are briefly resident, so size the container for twice the largest database.

Leave the cache on. Real traffic repeats addresses constantly, and a cached request costs less than half of an uncached one.


API mode

When you cannot ship database files, the plugin can call the IPGeolocation.io REST API instead:

http:
  middlewares:
    geo:
      plugin:
        ipgeolocation:
          mode: api
          apiKey: YOUR_API_KEY
          apiInclude: security
          cacheTTL: 24h
          headerPreset: standard
          failOpen: true

Every cache miss is an outbound HTTPS request that costs a credit and adds a round trip, so keep cacheTTL high and failOpen on, and prefer MMDB mode for production traffic. Field names and rules are identical in both modes, so switching later is a configuration change.


Troubleshooting

Traefik will not start and logs a plugin error. Check that moduleName is exactly github.com/IPGeolocation/traefik-plugin-ipgeolocation and that the version tag exists.

A field is always empty. Confirm you loaded a database that carries it. Security fields need the Security database, ASN fields need the ASN database. The startup log prints one loaded line per database with its type and build date. To inspect a file directly, use mmdbio: mmdbio read --db db-ip-security.mmdb --ip 2.56.188.34 .

Everything is blocked, or nothing is. Turn on dryRun and read the reasons in the log. Remember that allowPrivate skips internal traffic and allowUnknown decides what happens to addresses no database covers.

All visitors look like one address. Traefik is behind a proxy. See Client IP selection.

blockBot lets Googlebot through. By design. Crawlers are flagged as bots and as known good bots in Security v4, and blocking them removes you from search results. Set blockKnownGoodBots: true if you really want them blocked.


Frequently asked questions

Not in MMDB mode. It reads local .mmdb files and makes no outbound requests, so there are no per-request costs. API mode exists for setups that cannot ship files.
Most call a remote service on every request, or wrap libmaxminddb , which cannot run inside Traefik's plugin interpreter. This plugin reads MMDB files natively, is built for IPGeolocation.io schemas, layers multiple databases, exposes security and ASN data as well as location, and blocks on any of it without extra tooling.
Yes. ./scripts/try-it.sh downloads a Traefik binary into a scratch directory, builds sample databases, runs the plugin inside it and checks enrichment, geo-blocking and VPN filtering. TESTING.md also covers the Docker Compose route.
It depends on what you are blocking or sending to your backend. Country and city headers or geo-blocking need the IP Geolocation database. VPN, proxy, Tor, bot and threat score rules need IP Security. ASN filtering needs IP to ASN, and ISP or company names need IP Company. You can load several together and the plugin layers them, so start with one and add more later without changing your rules. The full list is in Getting the databases, and the same files also work with the Nginx module if you run both.
Yes, but two settings are needed. Set trustForwardedHeader: true on the middleware with either forwardedDepth or trustedProxies , and list your proxy under the Traefik entrypoint's forwardedHeaders.trustedIPs . Without the second, Traefik overwrites the header before any middleware runs and every visitor looks like your load balancer. Behind Cloudflare, set forwardedHeaderName: CF-Connecting-IP . See Client IP selection.
Around 3.8 µs for a repeat visitor served from cache, and 9.4 µs to 44.6 µs for a cache miss depending on how many fields you resolve. Cost scales with the number of headers and rules you use, not with the size of the database, so a minimal preset stays fast even with multi gigabyte files. The numbers and the benchmark setup are in Performance and memory.
Set refreshInterval: 1h and have a scheduled job replace the files. The plugin notices the change, swaps the database in atomically, clears its cache and keeps serving the old copy if the new file will not open. Your job must write the download under a temporary name in the same directory and then rename it into place, otherwise a half written file can be picked up. See Keeping databases up to date.
Yes. IPGeolocation.io databases cover both address families, and the plugin looks up whichever address the visitor connected with. Every rule and header works the same for IPv6, including the forwarded header strategies, which accept bracketed addresses such as [2a04:4540::1]:443 .

Development

make test       # unit tests, including MMDB fixtures built byte by byte
make yaegi      # load the plugin through Yaegi, the interpreter Traefik uses
make try        # end to end inside a real Traefik binary
make check      # all of the above

Traefik does not compile plugins. It interprets them with Yaegi, which supports a subset of Go, so code that passes go test can still fail to load. Run make yaegi before every release. The reader is also checked against the official MMDB Go reader on real databases.

Two Yaegi rules for contributors, documented where they apply in the code: never assign a concrete type to an interface variable in a multi value assignment from a call, and never convert a value to an interface inside a loop body (return it from a function instead, see newMMDBRecord in provider.go ). Neither fails under go test . Third party dependencies are not allowed.


Related tools and links

IPGeolocation.io tools

Account, data and support

Traefik and format references


License

MIT. See LICENSE.

Built for IPGeolocation.io databases. Questions about the data, tiers or bundles are answered on the database documentation and pricing pages.

Subscribe to Our Newsletter

Get the latest in geolocation tech, straight to your inbox.