Configurer ssh-guard (durcissement SSH)¶
À qui s'adresse cette page¶
Administrateurs qui déploient ou ajustent la couche ssh-guard de gitrust selon leur topologie réseau (Internet direct, derrière nginx stream, derrière HAProxy, ou réseau privé).
Avant de commencer : si vous voulez d'abord comprendre ce que fait ssh-guard et ses compromis, lisez ssh-guard : détection d'attaques SSH. La référence des variables est dans Variables d'environnement — SSH_GUARD_*.
1. Choisir le profil de déploiement¶
Le profil pré-configure les défauts cohérents avec votre topologie. Une variable unique pilote tout le reste : SSH_GUARD_PROFILE.
| Profil | Topologie | Quand l'utiliser |
|---|---|---|
direct |
gitrust écoute en :22 ou :2222 exposé Internet, sans proxy devant |
VPS minimal, démo publique |
nginx |
nginx stream sur :22 avec proxy_protocol on; → gitrust sur 127.0.0.1:2222 |
Production avec reverse-proxy nginx (cas le plus courant) |
haproxy |
HAProxy en frontal SSH avec send-proxy ou send-proxy-v2 |
Topologies multi-services |
private |
Instance interne (VPN, réseau d'entreprise), pas d'attaque externe attendue | Intranet, lab |
custom |
Aucun preset, vous fixez chaque variable individuellement | Cas exotiques |
Le défaut au démarrage si SSH_GUARD_PROFILE n'est pas défini est custom, qui équivaut au profil direct côté détecteurs mais avec PROXY protocol désactivé.
2. Recette : profil direct (Internet direct)¶
Cas typique : VPS, pas de reverse-proxy SSH devant gitrust. Le port :2222 est ouvert sur Internet et peer_addr() est l'IP réelle du client.
2.1 .env minimal¶
Copiez template/env/ssh-guard-direct.env dans votre /opt/gitrust/.env ou ajoutez :
SSH_GUARD_ENABLED=true
SSH_GUARD_DRY_RUN=false
SSH_GUARD_PROFILE=direct
# Stockage hybride (RAM chaude + write-through PostgreSQL)
SSH_GUARD_STORE_BACKEND=hybrid
# Logs JSON dans journald + fichier dédié pour fail2ban
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Les seuils par défaut s'appliquent : brute-force 5/5min, énumération 10/5min, scan de clés 10/5min, flood 10/s burst 20, ban 1 h.
2.2 Redémarrage et vérification¶
sudo systemctl restart gitrust
# Le module annonce sa configuration au démarrage
sudo journalctl -u gitrust --since "1 min ago" | grep "SSH guard runtime assembly"
# Attendu : profile=direct enabled=true dry_run=false store=Hybrid
# Logrotate sur le fichier dédié
sudo tee /etc/logrotate.d/gitrust-ssh-guard <<'EOF'
/var/log/gitrust-ssh-guard.json {
daily
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
}
EOF
3. Recette : profil nginx (derrière nginx stream)¶
Cas typique : nginx termine TLS sur :443, fait du SSH stream sur :22 → 127.0.0.1:2222 avec PROXY protocol v2.
3.1 nginx (rappel — voir aussi Durcir avec Fail2ban)¶
# /etc/nginx/nginx.conf (bloc stream)
stream {
upstream gitrust_ssh {
server 127.0.0.1:2222;
}
server {
listen 22;
proxy_pass gitrust_ssh;
proxy_protocol on; # IMPORTANT — sinon ssh-guard rejettera
proxy_timeout 30m; # git push de gros pack peut être long
}
}
3.2 .env côté gitrust¶
Copiez template/env/ssh-guard-nginx.env ou :
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=nginx
# Le profil nginx pose déjà :
# SSH_GUARD_PROXY_PROTOCOL=v2
# SSH_GUARD_PROXY_PROTOCOL_STRICT=true
# SSH_GUARD_TRUSTED_PROXIES=127.0.0.1/32,::1/128
# (override individuel possible)
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Côté binding gitrust :
3.3 Vérification que la vraie IP est bien extraite¶
Faites un git ls-remote ssh://git@VOTRE_FQDN/owner/repo.git depuis une IP externe connue, puis :
sudo journalctl -u gitrust --since "1 min ago" | grep '"event":"connection_accepted"' | tail -1
# Attendu : "ip":"<VOTRE IP EXTERNE>" et NON "ip":"127.0.0.1"
Si vous voyez 127.0.0.1, c'est que le PROXY protocol n'est pas correctement transmis : vérifiez proxy_protocol on; dans nginx.
4. Recette : profil haproxy¶
Le profil pose SSH_GUARD_PROXY_PROTOCOL=any (auto-détection v1 ou v2) avec strict=true, mais n'a pas de trusted_proxies par défaut (HAProxy est presque toujours sur une IP distincte). À fournir explicitement :
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=haproxy
SSH_GUARD_TRUSTED_PROXIES=10.0.0.5/32 # IP de l'haproxy
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=both
SSH_GUARD_LOG_FILE=/var/log/gitrust-ssh-guard.json
Si SSH_GUARD_TRUSTED_PROXIES est vide, gitrust refuse de démarrer avec l'erreur :
C'est volontaire : sans cette protection, n'importe qui pourrait forger un en-tête PROXY pour usurper une IP cliente.
5. Recette : profil private (réseau interne)¶
Détecteurs désactivés (seuils u32::MAX), événements toujours émis pour audit, store en mémoire (rapide, perdu au restart).
SSH_GUARD_ENABLED=true
SSH_GUARD_PROFILE=private
# Tous les détecteurs sont désactivés par le preset.
# Les événements connection_accepted / auth_failed / auth_succeeded
# restent émis pour audit.
SSH_GUARD_LOG_FORMAT=json
SSH_GUARD_LOG_TARGET=stderr # journald uniquement, pas de fichier
6. Mode dry-run : observer avant de bloquer¶
Idéal pour valider un nouveau seuil ou laisser fail2ban faire le ban réel :
Effets :
- Les détecteurs continuent à corréler.
- Un événement
ip_bannedest émis pour chaque déclenchement (signal pour fail2ban / Loki). - Aucun ban n'est persisté côté ssh-guard. Le
BanManagerreste no-op.
C'est le mode recommandé pendant les premiers jours après un changement de seuil agressif. Surveillez :
sudo journalctl -u gitrust --since "24 hours ago" --no-pager \
| grep '"event":"ip_banned"' \
| jq -r '.ip + " " + .reason' \
| sort | uniq -c | sort -rn
7. Ajuster les seuils¶
7.1 Variables disponibles¶
# Brute-force : nombre d'échecs auth depuis une IP dans la fenêtre
SSH_GUARD_BRUTE_FORCE_THRESHOLD=5 # défaut 5
SSH_GUARD_BRUTE_FORCE_WINDOW_SECS=300 # défaut 300 (5 min)
# Énumération : nombre d'usernames distincts essayés depuis une IP
SSH_GUARD_USER_ENUM_THRESHOLD=10
SSH_GUARD_USER_ENUM_WINDOW_SECS=300
# Scan de clés : nombre de fingerprints distincts essayés depuis une IP
SSH_GUARD_KEY_SCAN_THRESHOLD=10
SSH_GUARD_KEY_SCAN_WINDOW_SECS=300
# Flood TCP : cap dur de connexions par seconde par IP
SSH_GUARD_CONN_FLOOD_PER_SEC=10
SSH_GUARD_CONN_FLOOD_BURST=20
# Durée d'un ban auto. 0 = permanent.
SSH_GUARD_AUTO_BAN_DURATION_SECS=3600 # défaut 3600 (1 h)
7.2 Profils suggérés¶
| Posture | Brute-force | Conn flood | TTL ban |
|---|---|---|---|
| Stricte (instance publique très exposée) | 3 / 5 min | 5/s burst 10 | 6 h |
| Standard (défaut) | 5 / 5 min | 10/s burst 20 | 1 h |
| Tolérante (équipe interne, partenaires CI) | 10 / 5 min | 20/s burst 50 | 30 min |
Désactiver complètement un détecteur : mettre son seuil à 4294967295 (u32::MAX). Le préfixe private le fait déjà pour les quatre.
8. Allowlist d'un partenaire CI¶
Une IP allowlistée bypasse les détecteurs et le flood-limit, mais reste auditée. Les ACL se gèrent via la table ssh_guard_acl (admin UI ou SQL direct).
-- Allow le runner CI de l'équipe build (CIDR /32)
INSERT INTO ssh_guard_acl (id, kind, ip_cidr, reason, created_by, created_at, updated_at)
VALUES (
gen_random_uuid(),
'allow',
'198.51.100.42/32',
'Runner CI équipe build (ticket OPS-1234)',
'<UUID admin>',
NOW(), NOW()
);
Vérifier : depuis cette IP, déclencher 50 tentatives ratées en 1 minute, puis vérifier qu'aucun ip_banned n'a été émis :
sudo journalctl -u gitrust --since "5 min ago" --no-pager \
| grep '"ip":"198.51.100.42"' \
| jq -r '.event' | sort | uniq -c
# Attendu : auth_failed: 50, brute_force_detected: 0, ip_banned: 0
9. Bannir manuellement un CIDR abusif¶
Pour un AS qui scanne en masse, posez un ban permanent :
-- Denylist permanente d'un CIDR
INSERT INTO ssh_guard_bans (id, ip_cidr, reason, banned_at, expires_at, auto_banned)
VALUES (
gen_random_uuid(),
'198.51.100.0/24',
'admin_deny_list',
NOW(),
NULL, -- NULL = permanent
false
);
expires_at = NULL rend le ban permanent. Pour un TTL fini : NOW() + INTERVAL '7 days'.
10. Troubleshooting¶
10.1 Tous les ip valent 127.0.0.1¶
Vous êtes en profil direct mais nginx fait du stream devant gitrust. Passez en nginx et activez proxy_protocol on; dans nginx.
10.2 « SSH_GUARD_TRUSTED_PROXIES required »¶
Vous avez activé le profil haproxy (ou SSH_GUARD_PROXY_PROTOCOL=v1|v2|any) sans déclarer de proxies de confiance. Ajoutez SSH_GUARD_TRUSTED_PROXIES=<CIDR> et redémarrez.
10.3 Faux positifs sur un développeur avec 8 clés SSH¶
L'agent SSH du dev présente toutes ses clés successivement. Si la 9e est la bonne, le détecteur de scan de clés pourrait s'activer (seuil 10 par défaut). Solutions, par ordre de préférence :
- Côté dev : forcer une clé spécifique avec
IdentitiesOnly yesdans~/.ssh/config. - Côté admin : monter le seuil à 15 (
SSH_GUARD_KEY_SCAN_THRESHOLD=15).
10.4 Lever un ban auto sans attendre l'expiration¶
UPDATE ssh_guard_bans
SET unbanned_at = NOW(), unbanned_by = '<UUID admin>'
WHERE id = '<UUID du ban>';
ssh-guard émet un ip_unbanned au prochain cycle d'effective_status.
10.5 Désactiver temporairement ssh-guard¶
ssh-guard devient un pass-through complet : aucune détection, aucun ban, le listener délègue directement à russh. À éviter sauf urgence.
11. Pour aller plus loin¶
- Variables d'environnement — SSH_GUARD_*
- Événements ssh-guard (JSON)
- ssh-guard : détection d'attaques SSH
- Durcir avec Fail2ban — chaîner ssh-guard et fail2ban
- Dépanner SSH — diagnostic général SSH
- Régler le rate limiting — distinction rate-limit HTTP vs TCP