feat(proxy): jail fail2ban gitea + rate-limit et fermeture de /metrics #28

Closed
Damien wants to merge 1 commits from feat/proxy-jail-gitea into feat/proxy-rsyslog
Owner

⚠️ Prérequis bloquant — rappel

Ne pas merger/déployer avant que la procédure de validation XFF de #19 ait confirmé que gitea.log contient l'IP publique réelle du client, sinon le jail bannira le proxy lui-même au bout de 5 échecs et gitea.arnodo.fr deviendra intégralement inaccessible. Procédure documentée dans gitea/README.md (#19).

Dépendance

Base branchée sur feat/proxy-rsyslog (#20, non mergée) puisque 50-gitea.conf routage par tag repose sur le ruleset remoteLogs posé par le récepteur générique de #20. Ce diff ne montre donc que le delta propre à #21.

Contenu

  1. jail.local[DEFAULT] ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10, posé avant le jail lui-même : garantit structurellement qu'une XFF mal configurée ne peut pas bannir le tailnet, plutôt que de compter sur un rattrapage après coup.
  2. Jail gitea-auth (filter.d/gitea.conf + jail.d/gitea.conf) — Gitea renvoie un 200 sur un login raté (page simplement ré-affichée avec l'erreur), donc traefik-auth (qui matche sur DownstreamStatus) est structurellement aveugle à ce brute force. Ce jail lit le log applicatif forwardé via 50-gitea.conf (dessus du récepteur générique de #20).
  3. conf.d/gitea.yml réécrit avec 3 routers : gitea-metrics-deny (priorité 200, ipAllowList limité à 127.0.0.1/32), gitea-auth (priorité 100, rate-limit 6/1m burst 12 sur /user/login, /user/sign_up, /user/forgot_password), gitea (priorité 1, catch-all). Backend basculé sur https://gitea.taila5ad8.ts.net (443, sans port) — conséquence du tailscale serve --https=443 de #19, incompatible avec l'ancien backend :3000 de l'instance community-scripts actuelle.

Vérification

Tout testé avec les vrais binaires (fail2ban, rsyslog, Traefik v3) dans des conteneurs jetables, pas de simulation :

  • Filtre gitea.conf : fail2ban-regex sur un gitea.log synthétique → les 3 lignes d'échec matchent, la ligne GET /user/login ... 200 OK normale ne matche pas.
  • Jail réelle : fail2ban-client démarré avec jail.local + filter.d/gitea.conf + jail.d/gitea.confgitea-auth opérationnel, ignoreip confirmé (127.0.0.0/8, 100.64.0.0/10, ::1).
  • Ban effectif : 5 échecs depuis 203.0.113.99 → banni. 5 échecs depuis 100.64.1.50 (plage tailnet) → jamais banni, ignoreip fait son travail.
  • Routage rsyslog : message tagué gitea envoyé en TCP → atterrit dans /var/log/gitea/gitea.log (jail) et dans /var/log/remote/<hostname>.log (catch-all générique de #20), sans conflit.
  • Traefik v3 réel : les 3 routers, les 2 middlewares (auth-ratelimit, deny-public) et les priorités déclarées se chargent sans erreur (seules erreurs : "nonexistent certificate resolver", attendu — letsencrypt n'est pas configuré dans le harnais de test minimal, il l'est dans le traefik.yml réel du script).
  • shellcheck proxy/install.sh : aucune nouvelle alerte (seul le SC2006 préexistant, sur du code non touché par cette PR).

Idempotence

Tous les fichiers sont écrits inconditionnellement à chaque run (même pattern que les blocs fail2ban/rsyslog existants) — un rejeu réapplique simplement la version courante, sans duplication.

Note de séquencement

Rappelée dans le script : le basculement du backend Traefik rend cette config incompatible avec l'instance Gitea actuellement en production (community-scripts, :3000). À appliquer seulement au moment de la bascule effective vers la nouvelle instance (#19), pas avant.

Closes #21

## ⚠️ Prérequis bloquant — rappel **Ne pas merger/déployer avant que la procédure de validation XFF de #19 ait confirmé que `gitea.log` contient l'IP publique réelle du client**, sinon le jail bannira le proxy lui-même au bout de 5 échecs et `gitea.arnodo.fr` deviendra intégralement inaccessible. Procédure documentée dans `gitea/README.md` (#19). ## Dépendance Base branchée sur `feat/proxy-rsyslog` (#20, non mergée) puisque `50-gitea.conf` routage par tag repose sur le ruleset `remoteLogs` posé par le récepteur générique de #20. Ce diff ne montre donc que le delta propre à #21. ## Contenu 1. **`jail.local`** — `[DEFAULT] ignoreip = 127.0.0.1/8 ::1 100.64.0.0/10`, posé avant le jail lui-même : garantit structurellement qu'une XFF mal configurée ne peut pas bannir le tailnet, plutôt que de compter sur un rattrapage après coup. 2. **Jail `gitea-auth`** (`filter.d/gitea.conf` + `jail.d/gitea.conf`) — Gitea renvoie un **200** sur un login raté (page simplement ré-affichée avec l'erreur), donc `traefik-auth` (qui matche sur `DownstreamStatus`) est structurellement aveugle à ce brute force. Ce jail lit le log applicatif forwardé via `50-gitea.conf` (dessus du récepteur générique de #20). 3. **`conf.d/gitea.yml`** réécrit avec 3 routers : `gitea-metrics-deny` (priorité 200, `ipAllowList` limité à `127.0.0.1/32`), `gitea-auth` (priorité 100, rate-limit 6/1m burst 12 sur `/user/login`, `/user/sign_up`, `/user/forgot_password`), `gitea` (priorité 1, catch-all). Backend basculé sur `https://gitea.taila5ad8.ts.net` (443, sans port) — conséquence du `tailscale serve --https=443` de #19, **incompatible avec l'ancien backend `:3000`** de l'instance community-scripts actuelle. ## Vérification Tout testé avec les vrais binaires (fail2ban, rsyslog, Traefik v3) dans des conteneurs jetables, pas de simulation : - **Filtre `gitea.conf`** : `fail2ban-regex` sur un `gitea.log` synthétique → les 3 lignes d'échec matchent, la ligne `GET /user/login ... 200 OK` normale ne matche pas. - **Jail réelle** : `fail2ban-client` démarré avec `jail.local` + `filter.d/gitea.conf` + `jail.d/gitea.conf` → `gitea-auth` opérationnel, `ignoreip` confirmé (`127.0.0.0/8`, `100.64.0.0/10`, `::1`). - **Ban effectif** : 5 échecs depuis `203.0.113.99` → banni. 5 échecs depuis `100.64.1.50` (plage tailnet) → jamais banni, `ignoreip` fait son travail. - **Routage rsyslog** : message tagué `gitea` envoyé en TCP → atterrit dans `/var/log/gitea/gitea.log` (jail) et dans `/var/log/remote/<hostname>.log` (catch-all générique de #20), sans conflit. - **Traefik v3 réel** : les 3 routers, les 2 middlewares (`auth-ratelimit`, `deny-public`) et les priorités déclarées se chargent sans erreur (seules erreurs : "nonexistent certificate resolver", attendu — `letsencrypt` n'est pas configuré dans le harnais de test minimal, il l'est dans le `traefik.yml` réel du script). - `shellcheck proxy/install.sh` : aucune nouvelle alerte (seul le SC2006 préexistant, sur du code non touché par cette PR). ## Idempotence Tous les fichiers sont écrits inconditionnellement à chaque run (même pattern que les blocs fail2ban/rsyslog existants) — un rejeu réapplique simplement la version courante, sans duplication. ## Note de séquencement Rappelée dans le script : le basculement du backend Traefik rend cette config incompatible avec l'instance Gitea actuellement en production (community-scripts, `:3000`). À appliquer seulement au moment de la bascule effective vers la nouvelle instance (#19), pas avant. Closes #21
Damien added 1 commit 2026-08-01 06:46:05 +00:00
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
Damien closed this pull request 2026-08-01 17:44:27 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Damien/infra-scripts#28