feat: Fail2ban, auto configure reverse proxies
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Restart fail2ban
|
||||
systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
enabled: yes
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user