Issues with Geoblocking and Crowdsec

1. The problem I’m having:

I am trying to get the Maxmind Geoblocking to work but I am having trouble finding the correct Caddyfile Syntax.
According to logs the IPs are getting recognized as not allowed, but blocking is not working.

2. Error messages and/or full log output:

{"level":"debug","ts":1750965926.6331162,"logger":"http.matchers.maxmind_geolocation","msg":"Detected MaxMind data","ip":"188.214.XXX","country":"MA","subdivisions":"","metro_code":0,">
{"level":"debug","ts":1750965926.6332605,"logger":"http.matchers.maxmind_geolocation","msg":"Country not allowed","country":"MA"}

3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

a. System environment:

Caddy on Docker on Ubuntu built with xcaddy:
–with github.com/hslatman/caddy-crowdsec-bouncer/http
–with github.com/hslatman/caddy-crowdsec-bouncer/appsec
–with GitHub - porech/caddy-maxmind-geolocation: Caddy v2 module to filter requests based on source IP geolocation

b. Command:

docker compose

c. Service/unit/compose file:

d. My complete Caddy config:

{
        order crowdsec after error # forces the CrowdSec directive to be executed after geoblock
## CROWDSEC ##        
        crowdsec {
                api_url http://crowdsec:8080
                api_key 999999999
                appsec_url http://crowdsec:7422         
        }
}

## GLOBAL SNIPPETS ##

(auth) {
        reverse_proxy /outpost.goauthentik.io/* authentik_server:9000 {
                header_up Host {http.reverse_proxy.upstream.host}
        }
        forward_auth authentik_server:9000 {
                uri /outpost.goauthentik.io/auth/caddy
                copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
                trusted_proxies private_ranges
        }
}

(geoblock) {
        @geofilter {
                not maxmind_geolocation {
                        db_path "/usr/local/etc/geoip/GeoLite2-Country.mmdb"
                        allow_countries DE
                }
        }
        error @geofilter 403
}

## SITE CONFIG  ##

test.test.de {
        import security-header
        import geoblock
        route {
                crowdsec
                #appsec
                reverse_proxy authentik_server:9443 {
                        transport http {
                                tls_insecure_skip_verify
                                }
                }
        }
}

5. Links to relevant resources:

You can check the directive order in the Caddy documentation here:

Since route has a higher priority than error, it will always take precedence.

You might want to try something like this:

(geoblock) {
        @geofilter {
                not maxmind_geolocation {
                        db_path "/usr/local/etc/geoip/GeoLite2-Country.mmdb"
                        allow_countries DE
                }
        }
        handle @geofilter {
                error 403
        }
}

Thanks!
Now blocking works. New problem is now, that my real IP is not getting recognized but the internal IP:

>
{"level":"debug","ts":1751104956.4746811,"logger":"http.matchers.maxmind_geolocation","msg":"Detected MaxMind data","ip":"172.18.0.1","country":"","subdivisions":"","metro_code":0,"asn":0}
{"level":"debug","ts":1751104956.4747846,"logger":"http.matchers.maxmind_geolocation","msg":"Country not allowed","country":""}

Any idea how to fix this?

You haven’t shared how you’re running Caddy in Docker, so I’m going to assume you’re using bridge mode. Try switching it to the host network.

That said, without seeing your docker-compose file or the exact Docker command you’re using, I’m really just guessing here.

You are completly right. Yes it is running in docker. Heres my compose:


services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile
    image: caddy-crowdsec-geoip
    container_name: caddy
    restart: unless-stopped
    networks:
      - proxy
    ports:
      - 80:80
      - 443:443
    environment:
      - CROWDSEC_API_KEY=XXXXXXXXXX
    volumes:
      - ./data:/data
      - ./config:/config
      - /home/srv:/srv
      - /home//geoip/:/usr/local/etc/geoip
      - ./conf:/etc/caddy
      - ./log:/var/log

networks:
  proxy:
    external: true

I think switchting to host network mode would not be a good solution as I am loosing the docker name resolutions (which I use in the config for crowdsec etc).

The proxy network has every other service which I want to expose to caddy.

What I dont understand: As per my initial post the IPs got recognized correctly. The only thing I changed was adding the correct error handle and adding X-Client-IP.
This is the current Caddyfile:

(geoblock) {
        @geofilter {
                not maxmind_geolocation {
                        db_path "/usr/local/etc/geoip/GeoLite2-Country.mmdb"
                        allow_countries DE UNK
                }
        }
        header X-Client-IP "{remote_host}"
        handle @geofilter {
                error 403
        }
        
}

test.test.de {
        import security-header
        import geoblock
        route {
                crowdsec
                reverse_proxy authentik_server:9443 {
                        transport http {
                                tls_insecure_skip_verify
                                }
                }
        }
}

I don’t think that’s what caused the change in the behaviour.

Caddy in a bridge mode won’t see the real client’s IP, unless there’s another proxy sitting in front of it passing the information to Caddy via X-Forwarded-For.

True. Caddy is in front - no other proxy involved.
Anyone has an idea why we see this change in behaviour? If caddy does not see the real client IP, why where those listed in the logs?

I am currently thinking of switchting the Geoblocking to my firewall if this tends to be so complicated within Caddy.

You need to disable the userland proxy in Docker so the original remote IP address is retained. Note that there’s been reports of issues when disabling it, but your experience might differ.

Thanks for the hint. I now switched to using GeoIP-Shell on the firewall as this is very uncomplicated to setup.

Thanks for your support everyone!