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¶
Vérifier que ufw est actif (fournit banaction = ufw dans la config) :
2. Fichier principal /etc/fail2ban/jail.local¶
À placer via :
# =============================================================================
# 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 :
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¶
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 admin/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.1 via dtrack.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 :
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]: 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.