feat(proxy): add gitea-auth fail2ban jail, rate-limit, close /metrics
Gitea returns HTTP 200 on a failed web login (the page is simply re-rendered with an error), so the existing traefik-auth jail, which matches on DownstreamStatus, is structurally blind to brute force on the login form. Adds a dedicated gitea-auth jail reading the forwarded application log (routed by the new 50-gitea.conf on top of #20's generic receiver), plus a Traefik-side rate limit on /user/login, /user/sign_up and /user/forgot_password as a second layer. [DEFAULT] ignoreip in jail.local guards every jail (not just gitea-auth) so a misconfigured X-Forwarded-For chain can't ban the tailnet itself — this must be in place before the jail, since the whole point is to make that failure mode structurally impossible rather than caught after the fact. conf.d/gitea.yml gains a highest-priority router that denies public access to /metrics (ipAllowList limited to loopback) while leaving it reachable on the tailnet, where Gitea's own tailscale serve answers directly. The backend also moves to https://gitea.taila5ad8.ts.net (443, no port) — the result of #19's tailscale serve --https=443 — which is NOT compatible with the previous :3000 backend of the old community-scripts deployment; only apply once the data migration has actually happened. Verified with real fail2ban/rsyslog/Traefik v3 in disposable containers: the gitea filter matches real failure log lines and misses the normal login-page line; 5 failed logins from an external IP bans it while 5 from the Tailscale CGNAT range never do (ignoreip in effect); a forwarded "gitea"-tagged message lands in /var/log/gitea/gitea.log; Traefik loads all three routers, both middlewares, and the stated priorities without error. Closes #21
This commit is contained in:
+103
-1
@@ -157,6 +157,14 @@ main() {
|
||||
sudo chown "$USER":"$USER" /var/log/traefik
|
||||
sudo touch /var/log/traefik/access.log
|
||||
|
||||
# DEFAULT ignoreip guards every jail below, including gitea-auth: a
|
||||
# misconfigured X-Forwarded-For chain must never be able to ban the
|
||||
# tailnet itself. 100.64.0.0/10 is Tailscale's CGNAT range.
|
||||
sudo tee /etc/fail2ban/jail.local > /dev/null << 'EOF'
|
||||
[DEFAULT]
|
||||
ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10
|
||||
EOF
|
||||
|
||||
sudo tee /etc/fail2ban/filter.d/traefik.conf > /dev/null << 'EOF'
|
||||
[Definition]
|
||||
# Match JSON log lines where ClientHost is the offending IP and DownstreamStatus
|
||||
@@ -177,6 +185,31 @@ maxretry = 10
|
||||
findtime = 5m
|
||||
bantime = 1h
|
||||
action = iptables-multiport[name=traefik, port="80,443", protocol=tcp]
|
||||
EOF
|
||||
|
||||
log_info "Configuring Fail2ban for Gitea..."
|
||||
# Gitea returns HTTP 200 on a failed web login (the page is simply
|
||||
# re-rendered with an error) — the traefik-auth jail above, which
|
||||
# matches on DownstreamStatus, is structurally blind to brute force on
|
||||
# the login form. This jail reads the application log instead
|
||||
# (forwarded via rsyslog below), which does log failed attempts.
|
||||
sudo mkdir -p /var/log/gitea
|
||||
|
||||
sudo tee /etc/fail2ban/filter.d/gitea.conf > /dev/null << 'EOF'
|
||||
[Definition]
|
||||
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
|
||||
ignoreregex =
|
||||
EOF
|
||||
|
||||
sudo tee /etc/fail2ban/jail.d/gitea.conf > /dev/null << 'EOF'
|
||||
[gitea-auth]
|
||||
enabled = true
|
||||
filter = gitea
|
||||
logpath = /var/log/gitea/gitea.log
|
||||
maxretry = 5
|
||||
findtime = 10m
|
||||
bantime = 1h
|
||||
action = iptables-multiport[name=gitea, port="80,443", protocol=tcp]
|
||||
EOF
|
||||
|
||||
sudo systemctl restart fail2ban
|
||||
@@ -211,6 +244,30 @@ EOF
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Configuring rsyslog routing for Gitea..."
|
||||
# Routes the "gitea" tag (set by gitea/install.sh's imfile forwarder)
|
||||
# into its own file, on top of the generic 10-remote-receiver.conf
|
||||
# catch-all — the gitea-auth fail2ban jail above reads this file.
|
||||
sudo tee /etc/rsyslog.d/50-gitea.conf > /dev/null << 'EOF'
|
||||
$RuleSet remoteLogs
|
||||
if $programname == 'gitea' then {
|
||||
action(type="omfile" file="/var/log/gitea/gitea.log")
|
||||
stop
|
||||
}
|
||||
$RuleSet RSYSLOG_DefaultRuleset
|
||||
EOF
|
||||
|
||||
sudo tee /etc/logrotate.d/gitea > /dev/null << 'EOF'
|
||||
/var/log/gitea/gitea.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
|
||||
sudo systemctl restart rsyslog
|
||||
@@ -313,22 +370,67 @@ accessLog:
|
||||
EOF
|
||||
|
||||
# --- conf.d/gitea.yml (dynamic config) ---
|
||||
# NOTE: backend is https://gitea.taila5ad8.ts.net (443, no port) — the
|
||||
# result of gitea/install.sh (#19) moving to `tailscale serve
|
||||
# --https=443`. This is NOT compatible with the previous :3000 backend
|
||||
# of the old community-scripts deployment; only apply this once the
|
||||
# data migration to the new instance has actually happened.
|
||||
cat > "$TRAEFIK_DIR/conf.d/gitea.yml" << 'EOF'
|
||||
http:
|
||||
routers:
|
||||
# /metrics carries the Prometheus bearer token; never expose it publicly.
|
||||
# Highest priority so it wins over the catch-all "gitea" router below.
|
||||
gitea-metrics-deny:
|
||||
rule: "Host(`gitea.arnodo.fr`) && PathPrefix(`/metrics`)"
|
||||
priority: 200
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: gitea
|
||||
middlewares:
|
||||
- deny-public
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
# Gitea returns HTTP 200 on a failed web login, so the traefik-auth
|
||||
# jail (which matches on DownstreamStatus) can't see login brute
|
||||
# force. Rate-limit the login/signup/forgot-password surface directly
|
||||
# as a second layer on top of the gitea-auth fail2ban jail.
|
||||
gitea-auth:
|
||||
rule: "Host(`gitea.arnodo.fr`) && (Path(`/user/login`) || Path(`/user/sign_up`) || Path(`/user/forgot_password`))"
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: gitea
|
||||
middlewares:
|
||||
- auth-ratelimit
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
gitea:
|
||||
rule: "Host(`gitea.arnodo.fr`)"
|
||||
priority: 1
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: gitea
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
middlewares:
|
||||
auth-ratelimit:
|
||||
rateLimit:
|
||||
average: 6
|
||||
period: 1m
|
||||
burst: 12
|
||||
deny-public:
|
||||
ipAllowList:
|
||||
sourceRange:
|
||||
- "127.0.0.1/32"
|
||||
|
||||
services:
|
||||
gitea:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://gitea.taila5ad8.ts.net:3000"
|
||||
- url: "https://gitea.taila5ad8.ts.net"
|
||||
EOF
|
||||
|
||||
log_info "Starting Traefik stack..."
|
||||
|
||||
Reference in New Issue
Block a user