feat: Fail2ban, auto configure reverse proxies

This commit is contained in:
Lino Silva
2026-04-01 22:45:10 +01:00
parent f17526afc3
commit 3f28ed0c14
11 changed files with 451 additions and 19 deletions
+196 -2
View File
@@ -20,9 +20,203 @@ cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
# Pocket ID configuration
pocketid_encryption_key: "{{ vault_pocketid_encryption_key }}"
sonarr_host: 10.0.2.25
sonarr_port: 8989
auto_configure_traefik:
# arr
sonarr:
subdomain: "sonarr"
host: "10.0.2.25"
port: 8989
auth_required: true
radarr:
subdomain: "radarr"
host: "10.0.2.25"
port: 7878
auth_required: true
lidarr:
subdomain: "lidarr"
host: "10.0.2.25"
port: 8686
auth_required: true
transmission:
subdomain: "transmission"
host: "10.0.2.25"
port: 9091
auth_required: true
tdarr:
subdomain: "tdarr"
host: "10.0.2.25"
port: 8265
auth_required: true
bazarr:
subdomain: "bazarr"
host: "10.0.2.25"
port: 6767
auth_required: true
seerr:
subdomain: "overseerr"
host: "10.0.2.25"
port: 5055
auth_required: false
prowlarr:
subdomain: "prowlarr"
host: "10.0.2.25"
port: 9696
auth_required: true
unpackerr:
subdomain: "unpackerr"
host: "10.0.2.25"
port: 5656
auth_required: true
homeassistant:
subdomain: "homeassistant"
host: "10.0.2.100"
port: 8123
auth_required: false
# media
plex:
subdomain: "plex"
host: "10.0.2.10"
port: 32400
auth_required: false
tracearr:
subdomain: "tracearr"
host: "10.0.2.21"
port: 3000
auth_required: true
tautulli:
subdomain: "tautulli"
host: "10.0.2.21"
port: 8181
auth_required: true
vaultwarden:
subdomain: "vaultwarden"
host: "10.0.2.27"
port: 8004
auth_required: false
changedetection:
subdomain: "changedetection"
host: "10.0.2.24"
port: 5000
auth_required: true
nextcloud:
subdomain: "cloud"
host: "10.0.2.30"
port: 8001
auth_required: false
convertx:
subdomain: "convertx"
host: "10.0.2.43"
port: 3000
auth_required: true
dawarich:
subdomain: "places"
host: "10.0.2.48"
port: 3000
auth_required: false
frigate:
subdomain: "frigate"
host: "10.0.2.14"
port: 5000
auth_required: true
droposs:
subdomain: "games"
host: "10.0.2.46"
port: 3000
auth_required: false
# geoguessr:
# subdomain: "geoguessr"
# host: "10.0.2.39"
# port: 8080
# auth_required: true
gitea:
subdomain: "gitea"
host: "10.0.2.28"
port: 3000
auth_required: true
immich:
subdomain: "immich"
host: "10.0.2.18"
port: 2283
auth_required: false
mastodon:
subdomain: "social"
host: "10.0.2.20"
port: 80
auth_required: false
matrix:
subdomain: "chat"
host: "10.0.2.20"
port: 8008
auth_required: false
mealie:
subdomain: "recipes"
host: "10.0.2.26"
port: 9000
auth_required: false
truenas:
subdomain: "nas"
host: "10.0.2.200"
port: 80
auth_required: true
paperless:
subdomain: "paperless"
host: "10.0.2.29"
port: 8003
auth_required: true
pbs:
subdomain: "pbs"
host: "10.0.2.104"
port: 8007
auth_required: true
# pinchflat:
# subdomain: "youtube"
# host: "10.0.2.23"
# port: 8081
# auth_required: true
proxmox:
subdomain: "proxmox"
host: "10.0.2.2"
port: 8006
auth_required: true
resume:
subdomain: "resume"
host: "10.0.2.53"
port: 3000
auth_required: true
auth_bypass_paths:
- /lino
- /assets
- /api
speedtest-tracker:
subdomain: "fast"
host: "10.0.2.254"
port: 8765
auth_required: true
stocks:
subdomain: "stocks"
host: "10.0.2.40"
port: 3333
auth_required: false
super-productivity:
subdomain: "tasks"
host: "10.0.2.45"
port: 80
auth_required: true
uptime-kuma:
subdomain: "uptime"
host: "10.0.2.203"
port: 3001
auth_required: true
wealthfolio:
subdomain: "wealth"
host: "10.0.2.40"
port: 8088
auth_required: true
# Auth services configuration
pocketid_host: 10.0.4.10
pocketid_port: 8001
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart fail2ban
systemd:
name: fail2ban
state: restarted
enabled: yes
+51
View File
@@ -0,0 +1,51 @@
---
- name: Install fail2ban
apt:
name: fail2ban
state: present
update_cache: yes
- name: Ensure fail2ban filter directory exists
file:
path: /etc/fail2ban/filter.d
state: directory
mode: '0755'
- name: Ensure fail2ban jail directory exists
file:
path: /etc/fail2ban/jail.d
state: directory
mode: '0755'
- name: Ensure traefik log directory exists
file:
path: /var/log/traefik
state: directory
mode: '0755'
owner: root
group: root
- name: Deploy Traefik fail2ban filters
template:
src: "{{ item }}"
dest: "/etc/fail2ban/filter.d/{{ item | basename | regex_replace('\\.j2$', '') }}"
mode: '0644'
loop:
- traefik-auth.conf.j2
- traefik-404.conf.j2
- traefik-ratelimit.conf.j2
- traefik-badreq.conf.j2
notify: Restart fail2ban
- name: Deploy fail2ban jail configuration
template:
src: jail.local.j2
dest: /etc/fail2ban/jail.d/traefik.local
mode: '0644'
notify: Restart fail2ban
- name: Ensure fail2ban is enabled and started
systemd:
name: fail2ban
state: started
enabled: yes
@@ -0,0 +1,51 @@
# Fail2ban jails for Traefik
# Each jail monitors different attack patterns
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
# Authentication failures - strict rules
[traefik-auth]
enabled = true
port = http,https
filter = traefik-auth
logpath = /var/log/traefik/access.log
maxretry = 3
findtime = 300
bantime = 7200
action = iptables-allports[name=traefik-auth]
# 404 scanning/probing - moderate rules
[traefik-404]
enabled = true
port = http,https
filter = traefik-404
logpath = /var/log/traefik/access.log
maxretry = 20
findtime = 300
bantime = 3600
action = iptables-allports[name=traefik-404]
# Rate limiting violations - strict rules
[traefik-ratelimit]
enabled = true
port = http,https
filter = traefik-ratelimit
logpath = /var/log/traefik/access.log
maxretry = 5
findtime = 60
bantime = 1800
action = iptables-allports[name=traefik-ratelimit]
# Bad requests - lenient rules
[traefik-badreq]
enabled = true
port = http,https
filter = traefik-badreq
logpath = /var/log/traefik/access.log
maxretry = 10
findtime = 300
bantime = 1800
action = iptables-allports[name=traefik-badreq]
@@ -0,0 +1,9 @@
# Fail2ban filter for Traefik 404 scanning/probing
# Blocks IPs that generate excessive 404 errors (scanning for vulnerabilities)
[Definition]
failregex = ^.*"ClientAddr":"<HOST>:\d+".*"RequestMethod":"(GET|POST|PUT|DELETE|PATCH)".*"DownstreamStatus":404.*$
ignoreregex =
# Example log line (JSON):
# {"ClientAddr":"192.168.1.100:54321","DownstreamStatus":404,"RequestMethod":"GET",...}
@@ -0,0 +1,9 @@
# Fail2ban filter for Traefik authentication failures
# Blocks IPs that repeatedly fail authentication (401 Unauthorized)
[Definition]
failregex = ^.*"ClientAddr":"<HOST>:\d+".*"RequestMethod":"(GET|POST|PUT|DELETE|PATCH)".*"DownstreamStatus":401.*$
ignoreregex =
# Example log line (JSON):
# {"ClientAddr":"192.168.1.100:54321","DownstreamStatus":401,"RequestMethod":"GET",...}
@@ -0,0 +1,9 @@
# Fail2ban filter for Traefik bad requests
# Blocks IPs that generate excessive 4xx errors (bad requests, forbidden, etc.)
[Definition]
failregex = ^.*"ClientAddr":"<HOST>:\d+".*"RequestMethod":"(GET|POST|PUT|DELETE|PATCH)".*"DownstreamStatus":4\d{2}.*$
ignoreregex = ^.*"DownstreamStatus":(401|404|429).*$
# Catches all 4xx errors except 401, 404, 429 (handled by specific filters)
# Example: 400 Bad Request, 403 Forbidden, etc.
@@ -0,0 +1,9 @@
# Fail2ban filter for Traefik rate limiting
# Blocks IPs that trigger rate limit responses (429 Too Many Requests)
[Definition]
failregex = ^.*"ClientAddr":"<HOST>:\d+".*"RequestMethod":"(GET|POST|PUT|DELETE|PATCH)".*"DownstreamStatus":429.*$
ignoreregex =
# Example log line (JSON):
# {"ClientAddr":"192.168.1.100:54321","DownstreamStatus":429,"RequestMethod":"POST",...}
@@ -19,6 +19,7 @@ services:
- ./data/traefik.yml:/traefik.yml:ro
- ./data/dynamic:/etc/traefik/dynamic:ro
- ./data/acme.json:/acme.json
- /var/log/traefik:/var/log/traefik
labels:
- "traefik.enable=true"
@@ -20,6 +20,7 @@ http:
permanent: true
routers:
# Static services - HTTPS
traefik-secure:
rule: "Host(`traefik.{{ domain }}`)"
entryPoints:
@@ -30,16 +31,6 @@ http:
tls:
certResolver: cloudflare
sonarr:
rule: "Host(`sonarr.{{ domain }}`)"
entryPoints:
- https
middlewares:
- pocketid-auth
service: sonarr
tls:
certResolver: cloudflare
pocketid:
rule: "Host(`auth.{{ domain }}`)"
entryPoints:
@@ -55,22 +46,117 @@ http:
service: tinyauth
tls:
certResolver: cloudflare
# Static services - HTTP to HTTPS redirect
traefik-redirect:
rule: "Host(`traefik.{{ domain }}`)"
entryPoints:
- http
middlewares:
- traefik-https-redirect
service: api@internal
pocketid-redirect:
rule: "Host(`auth.{{ domain }}`)"
entryPoints:
- http
middlewares:
- traefik-https-redirect
service: pocketid
tinyauth-redirect:
rule: "Host(`auth-proxy.{{ domain }}`)"
entryPoints:
- http
middlewares:
- traefik-https-redirect
service: tinyauth
# Auto-configured services - HTTPS
{% for service_name, config in auto_configure_traefik.items() %}
{% if config.auth_bypass_paths is defined %}
# {{ service_name }} - bypass paths (no auth)
{% for path in config.auth_bypass_paths %}
{{ service_name }}-bypass-{{ loop.index }}:
rule: "Host(`{{ config.subdomain }}.{{ domain }}`) && PathPrefix(`{{ path }}`)"
entryPoints:
- https
priority: 100
service: {{ service_name }}
tls:
certResolver: cloudflare
{% endfor %}
# {{ service_name }} - default path (with auth if required)
{{ service_name }}:
rule: "Host(`{{ config.subdomain }}.{{ domain }}`)"
entryPoints:
- https
priority: 1
{% if config.auth_required | default(true) %}
middlewares:
- pocketid-auth
{% endif %}
service: {{ service_name }}
tls:
certResolver: cloudflare
{% else %}
{{ service_name }}:
rule: "Host(`{{ config.subdomain }}.{{ domain }}`)"
entryPoints:
- https
{% if config.auth_required | default(true) %}
middlewares:
- pocketid-auth
{% endif %}
service: {{ service_name }}
tls:
certResolver: cloudflare
{% endif %}
{% endfor %}
# Auto-configured services - HTTP to HTTPS redirect
{% for service_name, config in auto_configure_traefik.items() %}
{% if config.auth_bypass_paths is defined %}
# {{ service_name }} - bypass paths redirects
{% for path in config.auth_bypass_paths %}
{{ service_name }}-bypass-{{ loop.index }}-redirect:
rule: "Host(`{{ config.subdomain }}.{{ domain }}`) && PathPrefix(`{{ path }}`)"
entryPoints:
- http
priority: 100
middlewares:
- traefik-https-redirect
service: {{ service_name }}
{% endfor %}
{% endif %}
# {{ service_name }} - default redirect
{{ service_name }}-redirect:
rule: "Host(`{{ config.subdomain }}.{{ domain }}`)"
entryPoints:
- http
middlewares:
- traefik-https-redirect
service: {{ service_name }}
{% endfor %}
services:
sonarr:
loadBalancer:
passHostHeader: true
servers:
- url: "http://{{ sonarr_host }}:{{ sonarr_port }}"
pocketid:
loadBalancer:
passHostHeader: true
servers:
- url: "http://{{ pocketid_host }}:{{ pocketid_port }}"
- url: "http://{{ pocketid_host }}:{{ pocketid_port }}"
tinyauth:
loadBalancer:
passHostHeader: true
servers:
- url: "http://{{ tinyauth_host }}:{{ tinyauth_port }}"
# Auto-configured services
{% for service_name, config in auto_configure_traefik.items() %}
{{ service_name }}:
loadBalancer:
passHostHeader: true
servers:
- url: "http://{{ config.host }}:{{ config.port }}"
{% endfor %}
@@ -3,6 +3,13 @@ api:
debug: true
insecure: true
log:
level: INFO
accessLog:
filePath: /var/log/traefik/access.log
format: json
entryPoints:
http:
address: ":80"