Durcir l'instance avec Fail2ban
Évolution importante : depuis l'introduction de la crate
gitrust-ssh-guard, le jail[gitrust-ssh]ne lit plus les logsrusshviajournalctlmais consomme directement le flux JSON stable émis par ssh-guard dans/var/log/gitrust-ssh-guard.json. Cela rend le jail bien plus fiable (format garanti stable, pas de regex fragile sur les messages derussh). ActivezSSH_GUARD_LOG_TARGET=bothdans.env— voir Configurer ssh-guard.
Configuration complète pour protéger un déploiement gitrust exposant :
- sshd système sur
:2022(admin) - Nginx en reverse proxy HTTPS
:443+ redirect:80+stream :22 → :2222 - Gitrust (web
127.0.0.1:4000, SSH russh127.0.0.1:2222) - Dagger CI runner (local, pas d'exposition réseau directe)
- Dependency-Track API
:8081+ UI:8080(si exposés) - PostgreSQL
127.0.0.1:5432(défensif au cas où le bind fuit)
1. Installation
sudo apt install -y fail2ban sudo systemctl enable --now fail2ban fail2ban-client --version
Vérifier que ufw est actif (fournit banaction = ufw dans la config) :
sudo ufw status
2. Fichier principal /etc/fail2ban/jail.local
À placer via :
sudo tee /etc/fail2ban/jail.local <<'EOF' [Le contenu ci-dessous] EOF
# ============================================================================= # Défauts globaux # ============================================================================= [DEFAULT] backend = systemd bantime = 1h findtime = 10m maxretry = 5 # LAN privé et loopback jamais bannis ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 # UFW banaction = ufw banaction_allports = ufw # Notifications (optionnel — nécessite un relay SMTP local) # destemail = contact@gitrust.eu # sender = fail2ban@votre-serveur.example.com # action = %(action_mwl)s # ============================================================================= # 1) sshd admin système — port 2022 # ============================================================================= [sshd] enabled = true port = 2022 filter = sshd backend = %(sshd_backend)s logpath = %(sshd_log)s maxretry = 3 findtime = 10m bantime = 1h # ============================================================================= # 2) Nginx — 401/403 auth basic (si un jour activé sur /admin, /metrics, etc.) # ============================================================================= [nginx-http-auth] enabled = true port = http,https filter = nginx-http-auth logpath = /var/log/nginx/gitrust.error.log maxretry = 5 # ============================================================================= # 3) Nginx — scanners d'URL (wp-admin, .env, phpmyadmin, config.php, etc.) # ============================================================================= [nginx-botsearch] enabled = true port = http,https filter = nginx-botsearch logpath = /var/log/nginx/gitrust.access.log maxretry = 2 findtime = 10m bantime = 24h # ============================================================================= # 4) Nginx — bad user-agents (scanners agressifs, masscan, nikto, etc.) # ============================================================================= [nginx-badbots] enabled = true port = http,https filter = nginx-badbots logpath = /var/log/nginx/gitrust.access.log maxretry = 2 bantime = 24h # ============================================================================= # 5) Nginx — dépassement limit_req (voir section 4 — zones HTTPS) # ============================================================================= [nginx-limit-req] enabled = true port = http,https filter = nginx-limit-req logpath = /var/log/nginx/gitrust.error.log maxretry = 10 findtime = 5m bantime = 1h # ============================================================================= # 6) Gitrust — brute force login (formulaire + API JWT) # ============================================================================= [gitrust-login] enabled = true port = http,https filter = gitrust-login logpath = /var/log/nginx/gitrust.access.log maxretry = 5 findtime = 10m bantime = 1h # ============================================================================= # 7) Gitrust — abus API (tokens personnels leaked, scrapers) # ============================================================================= [gitrust-api-abuse] enabled = true port = http,https filter = gitrust-api-abuse logpath = /var/log/nginx/gitrust.access.log maxretry = 30 findtime = 1m bantime = 2h # ============================================================================= # 8) Gitrust SSH — consomme les événements JSON stables de ssh-guard # ============================================================================= # IMPORTANT : ce jail consomme le flux JSON émis par la crate gitrust-ssh-guard # (champs stables documentés dans ../reference/ssh-guard-evenements.md). # Le ban est déclenché soit sur le "signal fort" ip_banned (ssh-guard a déjà # détecté la brute-force, fail2ban relaye au firewall) — un seul ip_banned # suffit (maxretry=1) — soit sur le brut auth_failed avec le seuil habituel. # # Prérequis dans /opt/gitrust/.env : # SSH_GUARD_LOG_TARGET=both # SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json [gitrust-ssh] enabled = true port = 22,2222 filter = gitrust-ssh logpath = /var/log/gitrust-ssh-guard.json maxretry = 1 # ssh-guard a déjà corrélé : 1 ip_banned = 1 ban firewall findtime = 10m bantime = 1h # ============================================================================= # 9) Gitrust import worker — tokens invalides sur clone de dépôts externes # ============================================================================= [gitrust-import] enabled = true port = http,https filter = gitrust-import backend = systemd journalmatch = _SYSTEMD_UNIT=gitrust.service maxretry = 3 findtime = 5m bantime = 30m # ============================================================================= # 10) Dependency-Track UI — brute force login (port 8080) # ============================================================================= # Activer UNIQUEMENT si DTrack est exposé publiquement. # Si DTrack est sur réseau privé/loopback, laisser enabled=false. [dtrack-login] enabled = false port = 8080,http,https filter = dtrack-login logpath = /var/log/nginx/dtrack.access.log maxretry = 5 findtime = 10m bantime = 2h # ============================================================================= # 11) Dependency-Track API — abus clé API (port 8081) # ============================================================================= [dtrack-api] enabled = false port = 8081,http,https filter = dtrack-api logpath = /var/log/nginx/dtrack.access.log maxretry = 10 findtime = 5m bantime = 1h # ============================================================================= # 12) PostgreSQL — tentatives de connexion invalides (défensif) # ============================================================================= # PG doit binder 127.0.0.1 uniquement. Ce jail protège si la config fuit. [postgresql] enabled = true port = 5432 filter = postgresql backend = systemd journalmatch = _SYSTEMD_UNIT=postgresql.service + _SYSTEMD_UNIT=docker.service maxretry = 5 findtime = 10m bantime = 1h # ============================================================================= # 13) Récidive — ban long pour IPs bannies 3x en 24h, tous jails confondus # ============================================================================= [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d maxretry = 3
3. Filtres personnalisés
À placer dans /etc/fail2ban/filter.d/ (un fichier par jail custom).
3.1 gitrust-login.conf
sudo tee /etc/fail2ban/filter.d/gitrust-login.conf <<'EOF' [Definition] # POST sur /login ou endpoints JWT avec code 4xx (401/403/422/429) failregex = ^<HOST> .* "POST /(login|api/v1/auth/login|api/v1/auth/refresh|api/v1/auth/2fa)[^"]*" (401|403|422|429) .*$ ignoreregex = EOF
3.2 gitrust-api-abuse.conf
sudo tee /etc/fail2ban/filter.d/gitrust-api-abuse.conf <<'EOF' [Definition] # Abus API : 401/403 répétés sur /api/v1/* (hors auth déjà couvert par gitrust-login) failregex = ^<HOST> .* "(GET|POST|PUT|DELETE|PATCH) /api/v1/(?!auth/)[^"]*" (401|403) .*$ ignoreregex = EOF
3.3 gitrust-ssh.conf (consomme le JSON ssh-guard)
Le filtre matche les événements JSON stables produits par gitrust-ssh-guard. Voir Événements ssh-guard (JSON) pour le schéma complet.
Le « signal fort » ip_banned est privilégié : ssh-guard a déjà corrélé brute-force / énumération / scan de clés, et fail2ban n'a plus qu'à appliquer le ban au niveau firewall (UFW/iptables) pour les autres ports si désiré.
sudo tee /etc/fail2ban/filter.d/gitrust-ssh.conf <<'EOF' [Definition] # Filtre les événements stables émis par gitrust-ssh-guard dans # /var/log/gitrust-ssh-guard.json. Format : une ligne JSON par événement. # # Capture <HOST> depuis le champ "ip" du JSON pour les variants pertinents. # # Tester : # sudo fail2ban-regex /var/log/gitrust-ssh-guard.json \ # /etc/fail2ban/filter.d/gitrust-ssh.conf failregex = ^.*"event":"ip_banned".*"ip":"<HOST>".*$ ^.*"event":"brute_force_detected".*"ip":"<HOST>".*$ ^.*"event":"user_enumeration_detected".*"ip":"<HOST>".*$ ^.*"event":"key_scanning_detected".*"ip":"<HOST>".*$ ^.*"event":"connection_dropped".*"ip":"<HOST>".*"reason":"untrusted_proxy".*$ ^.*"event":"connection_dropped".*"ip":"<HOST>".*"reason":"proxy_header_invalid".*$ ignoreregex = # Date au format ISO 8601 UTC produit par ssh-guard datepattern = "ts":"%%Y-%%m-%%dT%%H:%%M:%%S EOF
Variante « brute uniquement » — si vous préférez que fail2ban corrèle lui-même à partir des
auth_failedsans dépendre du verdict ssh-guard, remplacez le bloc ci-dessus parfailregex = ^.*"event":"auth_failed".*"ip":"<HOST>".*$et passezmaxretry = 5dans le jail. Les deux approches sont valides ; la première est plus rapide à réagir, la seconde est plus indépendante.
3.4 gitrust-import.conf
sudo tee /etc/fail2ban/filter.d/gitrust-import.conf <<'EOF' [Definition] # Import worker : tokens OAuth/GitHub invalides sur clone de dépôts externes failregex = ^.*import.*authentication failed.*from <HOST>.*$ ^.*import.*invalid (token|credentials).*from <HOST>.*$ ^.*import.*clone failed.*401.*from <HOST>.*$ ignoreregex = EOF
3.5 dtrack-login.conf (si DTrack exposé)
sudo tee /etc/fail2ban/filter.d/dtrack-login.conf <<'EOF' [Definition] # Dependency-Track : brute force du POST /api/v1/user/login failregex = ^<HOST> .* "POST /api/v1/user/login[^"]*" (401|403) .*$ ^<HOST> .* "POST /api/v1/user/forceChangePassword[^"]*" (401|403) .*$ ignoreregex = EOF
3.6 dtrack-api.conf (si DTrack exposé)
sudo tee /etc/fail2ban/filter.d/dtrack-api.conf <<'EOF' [Definition] # Dependency-Track API : abus de clé API ou requêtes non authentifiées failregex = ^<HOST> .* "[^"]+ /api/v1/[^"]*" 401 .*$ ^<HOST> .* "[^"]+ /api/v1/[^"]*" 403 .*$ ignoreregex = EOF
3.7 postgresql.conf (créer si absent)
if [ ! -f /etc/fail2ban/filter.d/postgresql.conf ]; then sudo tee /etc/fail2ban/filter.d/postgresql.conf <<'EOF' [Definition] failregex = ^.*authentication failed for user.*host=<HOST>.*$ ^.*FATAL:.*password authentication failed.*<HOST>.*$ ^.*no pg_hba\.conf entry for host "<HOST>".*$ ignoreregex = EOF fi
3.8 nginx-limit-req.conf (créer si absent sur votre distro)
if [ ! -f /etc/fail2ban/filter.d/nginx-limit-req.conf ]; then sudo tee /etc/fail2ban/filter.d/nginx-limit-req.conf <<'EOF' [Definition] failregex = ^.*limiting requests, excess: .* by zone .*, client: <HOST>.*$ ignoreregex = EOF fi
3.9 nginx-badbots.conf — OBLIGATOIRE (pas fourni par Debian/Ubuntu)
Le paquet fail2ban Debian/Ubuntu fournit apache-badbots.conf mais PAS nginx-badbots.conf. Sans ce filter, fail2ban-client reload affiche :
Found no accessible config files for 'filter.d/nginx-badbots' under /etc/fail2ban
Le filter ci-dessous réutilise la liste de bad user-agents de apache-badbots avec un failregex adapté au format combined de nginx :
sudo tee /etc/fail2ban/filter.d/nginx-badbots.conf <<'EOF' [Definition] badbotscustom = EmailCollector|WebEMailExtrac|TrackBack/1\.02|sogou music spider badbots = Atomic_Email_Hunter/4\.0|atSpider/1\.0|autoemailspider|bwh3_user_agent|China Local Browse 2\.6|ContactBot/0\.2|ContentSmartz|DataCha0s/2\.0|DBrowse 1\.4b|DBrowse 1\.4d|Demo Bot DOT 16b|Demo Bot Z 16b|DSurf15a 01|DSurf15a 71|DSurf15a 81|DSurf15a VA|EBrowse 1\.4b|Educate Search VxB|EmailSiphon|EmailSpider|EmailWolf 1\.00|ExtractorPro|Franklin Locator 1\.8|Full Web Bot 0416B|Guestbook Auto Submitter|ISC Systems iRc Search 2\.1|LMQueueBot/0\.2|LWP\:\:Simple/5\.803|Microsoft URL Control - 6\.00\.8xxx|Missigua Locator 1\.9|Mozilla/4\.0 efp@gmx\.net|Nsauditor/1\.x|PBrowse 1\.4b|PEval 1\.4b|Poirot|psycheclone|sogou spider|sohu agent|VadixBot|WebVulnCrawl\.unknown/1\.0 libwww-perl/5\.803|Wells Search II|WEP Search 00 failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*"(?:%(badbots)s|%(badbotscustom)s)"$ ignoreregex = datepattern = ^[^\[]*\[({DATE}) {^LN-BEG} EOF
Alternative : si vous ne voulez pas maintenir la liste, simplement désactiver le jail (enabled = false dans le [nginx-badbots]) — la combinaison nginx-botsearch + nginx-limit-req + gitrust-api-abuse couvre déjà l'essentiel.
4. Rate limiting côté Nginx (requis par nginx-limit-req)
4.1 Déclarer les zones globales
sudo tee /etc/nginx/conf.d/gitrust-limit.conf <<'EOF' # Zones de rate limiting gitrust limit_req_zone $binary_remote_addr zone=gitrust_login:10m rate=1r/s; limit_req_zone $binary_remote_addr zone=gitrust_api:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=gitrust_git:10m rate=30r/s; EOF
4.2 Ajouter les location dans /etc/nginx/sites-available/gitrust
Dans le bloc server { listen 443 ssl; ... }, avant location / finale :
# Protection brute force login (1 req/s par IP, burst 5) location ~ ^/(login|api/v1/auth/) { limit_req zone=gitrust_login burst=5 nodelay; proxy_pass http://gitrust_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Protection API (10 req/s par IP, burst 20) location ^~ /api/v1/ { limit_req zone=gitrust_api burst=20 nodelay; proxy_pass http://gitrust_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Protection Git smart HTTP (30 req/s — clone/push légitime peut être verbeux) location ~ ^/[^/]+/[^/]+\.git/ { limit_req zone=gitrust_git burst=50 nodelay; proxy_pass http://gitrust_backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_request_buffering off; client_max_body_size 2G; }
4.3 Reload
sudo nginx -t && sudo systemctl reload nginx
5. Démarrage et validation
sudo systemctl restart fail2ban # Liste des jails actifs sudo fail2ban-client status # Attendu : # sshd, nginx-http-auth, nginx-botsearch, nginx-badbots, nginx-limit-req, # gitrust-login, gitrust-api-abuse, gitrust-ssh, gitrust-import, # postgresql, recidive # (+ dtrack-login, dtrack-api si activées) # Stats par jail sudo fail2ban-client status gitrust-login sudo fail2ban-client status sshd
Valider chaque filtre custom
# 1. gitrust-login contre l'access log nginx sudo fail2ban-regex /var/log/nginx/gitrust.access.log \ /etc/fail2ban/filter.d/gitrust-login.conf # 2. gitrust-api-abuse sudo fail2ban-regex /var/log/nginx/gitrust.access.log \ /etc/fail2ban/filter.d/gitrust-api-abuse.conf # 3. gitrust-ssh contre le flux JSON ssh-guard sudo fail2ban-regex /var/log/gitrust-ssh-guard.json \ /etc/fail2ban/filter.d/gitrust-ssh.conf # Si /var/log/gitrust-ssh-guard.json n'existe pas, vérifiez SSH_GUARD_LOG_TARGET # et SSH_GUARD_LOG_FILE dans /opt/gitrust/.env (voir how-to/configurer-ssh-guard.md). # 4. postgresql journalctl -u docker --no-pager -n 5000 | grep -i postgres > /tmp/pg.log sudo fail2ban-regex /tmp/pg.log \ /etc/fail2ban/filter.d/postgresql.conf
0 failregex found = regex à ajuster après observation des logs réels de chaque service.
Test de bannissement
# Depuis un host HORS LAN (sinon ignoreip s'applique) for i in $(seq 1 10); do curl -sk -o /dev/null -w "%{http_code}\n" \ -X POST https://<votre-domaine>/login \ -d 'username=admin&password=wrong' done # Vérifier le ban ssh gitrust-host 'sudo fail2ban-client status gitrust-login' # Currently banned : 1 — Banned IP list : <IP test> # Débannir manuellement ssh gitrust-host 'sudo fail2ban-client unban <IP>'
6. Monitoring
# Logs fail2ban temps réel sudo tail -f /var/log/fail2ban.log # Toutes les IPs bannies, tous jails confondus sudo fail2ban-client banned # Stats détaillées d'un jail sudo fail2ban-client status gitrust-ssh # Currently failed : 2 # Total failed : 47 # Currently banned : 1 # Total banned : 12 # Banned IP list : 203.0.113.42 sudo grep "Ban " /var/log/fail2ban.log | tail -20
7. Matrice récapitulative
| # | Jail | Port(s) | Source logs | Max retry | Ban time | Surface protégée |
|---|---|---|---|---|---|---|
| 1 | sshd | 2022 | journald sshd | 3 | 1h | SSH admin |
| 2 | nginx-http-auth | 80,443 | nginx error | 5 | 1h | Basic auth (admin futur) |
| 3 | nginx-botsearch | 80,443 | nginx access | 2 | 24h | Scanners WP/PHP |
| 4 | nginx-badbots | 80,443 | nginx access | 2 | 24h | User-agents malveillants |
| 5 | nginx-limit-req | 80,443 | nginx error | 10 | 1h | Flood global |
| 6 | gitrust-login | 80,443 | nginx access | 5 | 1h | Brute force login UI + API |
| 7 | gitrust-api-abuse | 80,443 | nginx access | 30 | 2h | Scrapers API (tokens fuités) |
| 8 | gitrust-ssh | 22,2222 | /var/log/gitrust-ssh-guard.json (JSON stable ssh-guard) | 1 | 1h | Brute force / scan clés / énumération SSH Git |
| 9 | gitrust-import | 80,443 | journald gitrust | 3 | 30m | Brute force PAT/OAuth import |
| 10 | dtrack-login | 8080 | nginx access | 5 | 2h | Brute force UI Dep-Track |
| 11 | dtrack-api | 8081 | nginx access | 10 | 1h | Abus clé API Dep-Track |
| 12 | postgresql | 5432 | journald | 5 | 1h | Défense en profondeur PG |
| 13 | recidive | all | fail2ban.log | 3 | 1w | Méta-ban 3×/24h |
Ordre de déclenchement typique : brute force → jail spécifique (ban 1h) → si récidive 3x → recidive (ban 1 semaine, tous ports).
8. Notes spécifiques à la stack gitrust
8.1 Dagger (CI)
Dagger s'exécute localement sur la machine gitrust (ou un runner distant — voir administration_manual/how-to/configurer-ci-runner-remote.md). Aucun port réseau ouvert → pas de jail dédié.
8.2 Dependency-Track
Si DTrack est déployé sur le même serveur et exposé, activer les jails 10 et 11 en mettant enabled = true et en pointant logpath vers les logs nginx du vhost DTrack.
Si DTrack est interne (VPN, réseau privé), laisser enabled = false — le firewall suffit.
Config DTrack recommandée :
- Bind sur
127.0.0.1viadtrack.url.base=http://127.0.0.1:8080
8.3 PostgreSQL
Le jail postgresql est défensif. PG doit rester bindé sur 127.0.0.1:5432 via Docker. Vérifier :
sudo ss -tlnp | grep 5432 # Attendu : 127.0.0.1:5432 — PAS 0.0.0.0:5432
8.4 Notifications
Pour recevoir un mail à chaque ban :
- Installer un relay SMTP local :
sudo apt install msmtp-mta - Configurer
/etc/msmtprcavec un compte SMTP - Décommenter dans
[DEFAULT]:destemail = contact@gitrust.eu sender = fail2ban@votre-serveur.example.com action = %(action_mwl)s
sudo systemctl restart fail2ban
9. Limites et évolutions
- IPv6 : toutes les regex utilisent
<HOST>qui matche IPv4 ET IPv6. Vérifier que UFW est configuré pour v6 également. - CDN/Cloudflare devant : si un CDN est ajouté,
$remote_addrcôté Nginx sera l'IP du CDN — il faut récupérer la vraie IP viaX-Forwarded-Foret propager au logging Nginx. Changerfailregexen conséquence. Sinon les jails banniront le CDN. - Docker/Podman : si gitrust passe en conteneur, les logs de
gitrust.servicedeviennentdocker.serviceoupodman.service→ mettre à jourjournalmatchdans le jail 9 (gitrust-import). Le jail 8 (gitrust-ssh) n'est pas affecté car il consomme le fichier JSON ssh-guard, à condition que ce fichier soit monté côté hôte. - GeoIP : pour bloquer des pays entiers en amont, ajouter
geo $blocked_countrydans Nginx (modulengx_http_geoip2_module) — complémentaire à fail2ban.
GitRust