feat: Fail2ban, auto configure reverse proxies
This commit is contained in:
@@ -20,9 +20,203 @@ cloudflare_api_token: "{{ vault_cloudflare_api_token }}"
|
|||||||
# Pocket ID configuration
|
# Pocket ID configuration
|
||||||
pocketid_encryption_key: "{{ vault_pocketid_encryption_key }}"
|
pocketid_encryption_key: "{{ vault_pocketid_encryption_key }}"
|
||||||
|
|
||||||
sonarr_host: 10.0.2.25
|
auto_configure_traefik:
|
||||||
sonarr_port: 8989
|
# 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_host: 10.0.4.10
|
||||||
pocketid_port: 8001
|
pocketid_port: 8001
|
||||||
|
|
||||||
|
|||||||
@@ -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/traefik.yml:/traefik.yml:ro
|
||||||
- ./data/dynamic:/etc/traefik/dynamic:ro
|
- ./data/dynamic:/etc/traefik/dynamic:ro
|
||||||
- ./data/acme.json:/acme.json
|
- ./data/acme.json:/acme.json
|
||||||
|
- /var/log/traefik:/var/log/traefik
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ http:
|
|||||||
permanent: true
|
permanent: true
|
||||||
|
|
||||||
routers:
|
routers:
|
||||||
|
# Static services - HTTPS
|
||||||
traefik-secure:
|
traefik-secure:
|
||||||
rule: "Host(`traefik.{{ domain }}`)"
|
rule: "Host(`traefik.{{ domain }}`)"
|
||||||
entryPoints:
|
entryPoints:
|
||||||
@@ -30,16 +31,6 @@ http:
|
|||||||
tls:
|
tls:
|
||||||
certResolver: cloudflare
|
certResolver: cloudflare
|
||||||
|
|
||||||
sonarr:
|
|
||||||
rule: "Host(`sonarr.{{ domain }}`)"
|
|
||||||
entryPoints:
|
|
||||||
- https
|
|
||||||
middlewares:
|
|
||||||
- pocketid-auth
|
|
||||||
service: sonarr
|
|
||||||
tls:
|
|
||||||
certResolver: cloudflare
|
|
||||||
|
|
||||||
pocketid:
|
pocketid:
|
||||||
rule: "Host(`auth.{{ domain }}`)"
|
rule: "Host(`auth.{{ domain }}`)"
|
||||||
entryPoints:
|
entryPoints:
|
||||||
@@ -56,21 +47,116 @@ http:
|
|||||||
tls:
|
tls:
|
||||||
certResolver: cloudflare
|
certResolver: cloudflare
|
||||||
|
|
||||||
services:
|
# Static services - HTTP to HTTPS redirect
|
||||||
sonarr:
|
traefik-redirect:
|
||||||
loadBalancer:
|
rule: "Host(`traefik.{{ domain }}`)"
|
||||||
passHostHeader: true
|
entryPoints:
|
||||||
servers:
|
- http
|
||||||
- url: "http://{{ sonarr_host }}:{{ sonarr_port }}"
|
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:
|
||||||
pocketid:
|
pocketid:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
servers:
|
servers:
|
||||||
- url: "http://{{ pocketid_host }}:{{ pocketid_port }}"
|
- url: "http://{{ pocketid_host }}:{{ pocketid_port }}"
|
||||||
|
|
||||||
tinyauth:
|
tinyauth:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
servers:
|
servers:
|
||||||
- url: "http://{{ tinyauth_host }}:{{ tinyauth_port }}"
|
- 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
|
debug: true
|
||||||
insecure: true
|
insecure: true
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
accessLog:
|
||||||
|
filePath: /var/log/traefik/access.log
|
||||||
|
format: json
|
||||||
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
http:
|
http:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
|
|||||||
Reference in New Issue
Block a user