Durcir l'instance avec Fail2ban

Évolution importante : depuis l'introduction de la crate gitrust-ssh-guard, le jail [gitrust-ssh] ne lit plus les logs russh via journalctl mais 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 de russh). Activez SSH_GUARD_LOG_TARGET=both dans .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 russh 127.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_failed sans dépendre du verdict ssh-guard, remplacez le bloc ci-dessus par failregex = ^.*"event":"auth_failed".*"ip":"<HOST>".*$ et passez maxretry = 5 dans 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 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 :

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 :

  1. Installer un relay SMTP local : sudo apt install msmtp-mta
  2. Configurer /etc/msmtprc avec un compte SMTP
  3. Décommenter dans [DEFAULT] :
    destemail = contact@gitrust.eu
    sender    = fail2ban@votre-serveur.example.com
    action    = %(action_mwl)s
    
  4. 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_addr côté Nginx sera l'IP du CDN — il faut récupérer la vraie IP via X-Forwarded-For et propager au logging Nginx. Changer failregex en conséquence. Sinon les jails banniront le CDN.
  • Docker/Podman : si gitrust passe en conteneur, les logs de gitrust.service deviennent docker.service ou podman.service → mettre à jour journalmatch dans 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_country dans Nginx (module ngx_http_geoip2_module) — complémentaire à fail2ban.