Search code examples
dockernginxtraefiknginx-ingressfail2ban

How to implement Fail2Ban inside a Docker container running Nginx


I am trying to implement fail2ban inside my docker environment that uses my Nginx logs. <-- I may in the future just target the entire traefik logs.

version: '3'
services:
  fail2ban:
    image: 'crazymax/fail2ban:latest'
    restart: 'always'
    network_mode: 'host'
    cap_add:
      - 'NET_ADMIN'
      - 'NET_RAW'
    volumes:
      - 'nginx-log:/var/log:ro'
      - 'fail2ban-data:/data'
    env_file:
      - './fail2ban.env'
  laravel-mysql:
    [SNIP]
  laravel-php:
    [SNIP]
  laravel-nginx:
    image: 'nginx:alpine'
    restart: 'always'
    depends_on:
      - 'laravel-php'
    expose:
      - '80'
    volumes:
      - 'laravel-src:/var/www/html'
      - './nginx.conf:/etc/nginx/conf.d/default.conf'
      - 'nginx-log:/var/log/nginx'
    networks:
      - 'traefik'
      - 'laravel'
    labels:
      - 'traefik.enable=true'
      - 'traefik.docker.network=traefik'
      - 'traefik.http.routers.nginx.entrypoints=http'
      - 'traefik.http.routers.nginx.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`)'
      - 'traefik.http.routers.nginx.middlewares=redirect@file'
      - 'traefik.http.routers.nginx-https.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`)'
      - 'traefik.http.routers.nginx-https.tls=true'
      - 'traefik.http.routers.nginx-https.tls.certresolver=${DNS_PROVIDER}'
      - 'traefik.http.routers.nginx-https.tls.domains[0].main=${DOMAIN}'
      - 'traefik.http.routers.nginx-https.tls.domains[1].main=www.${DOMAIN}'
      - 'traefik.http.routers.nginx.service=nginx'
      - 'traefik.http.services.nginx.loadbalancer.server.port=80'
      - 'traefik.http.services.nginx.loadBalancer.passHostHeader=true'
      - 'traefik.http.middlewares.https_redirect.redirectscheme.scheme=https'
      - 'traefik.http.middlewares.https-redirect.redirectscheme.scheme=https'
      - 'traefik.http.middlewares.https-redirect.headers.customrequestheaders.X-Forwarded-Proto=https'
      - 'traefik.http.routers.nginx.middlewares=https-redirect'
      - 'traefik.http.middlewares.https_redirect.redirectscheme.permanent=true'
      - 'traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)'
      - 'traefik.http.routers.http_catchall.entrypoints=http'
      - 'traefik.http.routers.http_catchall.middlewares=https_redirect'
networks:
  laravel:
    driver: 'bridge'
  traefik:
    name: '${TRAEFIK_NETWORK}'
    external: 'true'
volumes:
  laravel-database:
    driver: 'local'
  laravel-src:
    driver: 'local'
  nginx-log:
    driver: 'local'
  fail2ban-data:
    driver: 'local'

Running docker logs laravel_fail2ban_1 --tail 100 after docker-compose up -d shows me:

Setting timezone to Europe/London...
Setting SSMTP configuration...
WARNING: SSMTP_HOST must be defined if you want fail2ban to send emails
Initializing files and folders...
Setting Fail2ban configuration...
Checking for custom actions in /data/action.d...
Checking for custom filters in /data/filter.d...
2021-08-01 11:40:13,199 fail2ban.configreader   [1]: INFO    Loading configs for fail2ban under /etc/fail2ban
2021-08-01 11:40:13,202 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/fail2ban.conf']
2021-08-01 11:40:13,203 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/fail2ban.conf']
2021-08-01 11:40:13,204 fail2ban                [1]: INFO    Using socket file /var/run/fail2ban/fail2ban.sock
2021-08-01 11:40:13,204 fail2ban                [1]: INFO    Using pid file /var/run/fail2ban/fail2ban.pid, [INFO] logging to STDOUT
2021-08-01 11:40:13,218 fail2ban.configreader   [1]: INFO    Loading configs for jail under /etc/fail2ban
2021-08-01 11:40:13,219 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/jail.conf']
2021-08-01 11:40:13,255 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/paths-debian.conf']
2021-08-01 11:40:13,257 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/paths-common.conf']
2021-08-01 11:40:13,260 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/paths-overrides.local']
2021-08-01 11:40:13,263 fail2ban.configparserinc[1]: INFO      Loading files: ['/etc/fail2ban/paths-common.conf', '/etc/fail2ban/paths-debian.conf', '/etc/fail2ban/jail.conf']
2021-08-01 11:40:13,369 fail2ban.server         [1]: INFO    --------------------------------------------------
2021-08-01 11:40:13,372 fail2ban.server         [1]: INFO    Starting Fail2ban v0.11.2
2021-08-01 11:40:13,373 fail2ban.observer       [1]: INFO    Observer start...
2021-08-01 11:40:13,382 fail2ban.database       [1]: INFO    Connected to fail2ban persistent database '/data/db/fail2ban.sqlite3'
2021-08-01 11:40:13,385 fail2ban.database       [1]: WARNING New database created. Version '4'
Server ready

If I now attempt to stress my application, no logs are populated in fail2ban but if I --follow my nginx container logs, I see the requests firing away.

If I docker exec -it -u root laravel_fail2ban_1 /bin/bash -c 'ls -la /var/log' I can see my logs in the correct location:

total 8
drwxr-xr-x    2 root     root          4096 Aug  1 11:35 .
drwxr-xr-x    1 root     root          4096 Dec 16  2020 ..
lrwxrwxrwx    1 root     root            11 Jul  6 20:40 access.log -> /dev/stdout
lrwxrwxrwx    1 root     root            11 Jul  6 20:40 error.log -> /dev/stderr

I see the issue may be when I try cat /var/log/access.log. It is symlinked to the /dev/stdout which means the terminal tries attaching to it. I cannot unlink it whilst running:

docker exec -it -u root laravel_fail2ban_1 /bin/bash -c 'unlink /var/log/access.log'
unlink: can't remove file '/var/log/access.log': Read-only file system

Any help appreciated to get this working. I need to keep the symlink on the volume so I can use docker logs on my nginx container.


Solution

  • If /var/log/access.log is a symlink to stdout, it's not going to be available in the other container: /dev/stdout points to the stdout of the current process, so when fail2ban attempts to read from it, it gets its own stdout, rather than the stdout of the nginx process.

    If you want fail2ban to be able to read the logs from nginx, you will need to write them to an actual file. If you also want them showing up on the container stdout, you can run something like a tail -f in the background of the nginx container.