Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
6.0 KiB
03 — Durcissement des serveurs
Durée : environ 1 h pour les deux serveurs.
cloud-init a posé le minimum vital (compte deploy, SSH par clé, UFW fermé).
Ce script va nettement plus loin et rend le résultat vérifiable.
1. Ce que fait 00-bootstrap-common.sh
| Domaine | Mesure | Pourquoi |
|---|---|---|
| Mises à jour | unattended-upgrades, redémarrage automatique à 04h30 si le noyau l'exige |
Une faille non corrigée pendant six mois est la première cause de compromission d'un serveur laissé seul. |
| SSH | Clé uniquement, root interdit, 3 tentatives, algorithmes modernes, AllowUsers deploy |
Supprime l'attaque par mot de passe et réduit la surface cryptographique. |
| fail2ban | Bannissement 24 h après 3 échecs, réseau privé exclu | Ralentit le balayage automatisé permanent d'Internet. |
| sysctl | Anti-spoofing, pas de redirections ICMP, kptr_restrict, ptrace_scope |
Rend l'escalade locale nettement plus difficile après une compromission applicative. |
| journald | Plafonné à 2 Go, 30 jours | Des journaux qui remplissent le disque provoquent une panne totale. |
| auditd | Trace passwd, shadow, sudoers, sshd_config, commandes root |
Sans journal d'audit, une intrusion est indémontrable. |
| chrony | Fuseau Europe/Paris, NTP | Une horloge décalée invalide les JWT, casse TLS et rend les audit_logs inexploitables. |
| UFW | Refus par défaut, ouvertures selon le rôle | Le firewall Hetzner ne filtre que les interfaces publiques : le trafic du réseau privé n'est filtré que par UFW. |
2. Exécution
app-01
scp -i ~/.ssh/xpeditis_prod infra/prod/scripts/00-bootstrap-common.sh \
deploy@<app_public_ipv4>:/tmp/
ssh -i ~/.ssh/xpeditis_prod deploy@<app_public_ipv4> \
'sudo bash /tmp/00-bootstrap-common.sh app'
db-01
APP_PRIVATE_IP détermine qui a le droit d'atteindre PostgreSQL et Redis.
scp -i ~/.ssh/xpeditis_prod infra/prod/scripts/00-bootstrap-common.sh \
deploy@<db_public_ipv4>:/tmp/
ssh -i ~/.ssh/xpeditis_prod deploy@<db_public_ipv4> \
'sudo APP_PRIVATE_IP=10.10.1.10 bash /tmp/00-bootstrap-common.sh data'
Gardez la session SSH ouverte pendant que le script tourne. Il redémarre
sshd: si la configuration était invalide,sshd -téchouerait avant le redémarrage, mais mieux vaut pouvoir corriger sans passer par la console de secours Hetzner. Ouvrez un second terminal et vérifiez que vous arrivez encore à vous connecter avant de fermer le premier.
3. Vérifications
Sur chaque serveur :
ssh deploy@<ip> '
echo "--- SSH ---"
sudo sshd -T | grep -E "^(permitrootlogin|passwordauthentication|maxauthtries|allowusers)"
echo "--- Services ---"
systemctl is-active fail2ban unattended-upgrades auditd chrony
echo "--- Pare-feu ---"
sudo ufw status verbose
echo "--- Horloge ---"
timedatectl | grep -E "Time zone|synchronized"
'
Attendu :
permitrootlogin no
passwordauthentication no
maxauthtries 3
allowusers deploy
active × 4
Status: active (avec les règles correspondant au rôle)
System clock synchronized: yes
Règles UFW attendues
app-01
22/tcp ALLOW Anywhere # SSH (filtré en amont par Hetzner)
80/tcp ALLOW Anywhere # HTTP (filtré en amont par Cloudflare)
443/tcp ALLOW Anywhere # HTTPS
6443/tcp ALLOW Anywhere # API k3s (filtré en amont)
db-01
22/tcp ALLOW Anywhere
5432/tcp ALLOW 10.10.1.10 # PostgreSQL ← app-01 uniquement
6379/tcp ALLOW 10.10.1.10 # Redis ← app-01 uniquement
Si 5432 ALLOW Anywhere apparaît sur db-01, arrêtez et corrigez : la base
serait joignable depuis n'importe quelle machine du réseau privé.
4. Durcissement complémentaire (recommandé)
4.1 Protéger la console Hetzner
La console web Hetzner donne un accès clavier au serveur, sans passer par SSH. Elle contourne donc tout ce qui précède.
- Activez la 2FA sur le compte Hetzner (si ce n'est pas déjà fait).
- Ne définissez jamais de mot de passe root sur les serveurs : sans mot de passe, la console ne permet pas de se connecter.
Vérifiez qu'aucun mot de passe n'est défini :
ssh deploy@<ip> 'sudo passwd -S root' # doit afficher "L" (locked)
4.2 Alertes de connexion SSH
Pour être prévenu de toute connexion réussie :
ssh deploy@<ip> 'sudo tee /etc/profile.d/99-ssh-alert.sh >/dev/null' <<'EOF'
#!/bin/sh
# Notifie Discord a chaque ouverture de session SSH interactive.
[ -n "$SSH_CONNECTION" ] || return 0
[ -f /etc/xpeditis/discord-webhook ] || return 0
WEBHOOK=$(cat /etc/xpeditis/discord-webhook)
curl -sf -m 5 -H 'Content-Type: application/json' \
-d "{\"content\":\"SSH sur \`$(hostname)\` : ${USER} depuis ${SSH_CONNECTION%% *}\"}" \
"$WEBHOOK" >/dev/null 2>&1 &
EOF
ssh deploy@<ip> '
sudo mkdir -p /etc/xpeditis
echo "<webhook_discord_alertes>" | sudo tee /etc/xpeditis/discord-webhook >/dev/null
sudo chmod 600 /etc/xpeditis/discord-webhook
'
Une notification pour chacune de vos propres connexions est le prix à payer pour repérer immédiatement celle qui n'est pas la vôtre.
4.3 Bannissement permanent des récidivistes
ssh deploy@<ip> 'sudo tee /etc/fail2ban/jail.d/recidive.local >/dev/null' <<'EOF'
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = iptables-allports
bantime = 1w
findtime = 1d
maxretry = 3
EOF
ssh deploy@<ip> 'sudo systemctl restart fail2ban'
5. Contrôle
[ ] Script exécuté sur app-01 (rôle app) et db-01 (rôle data)
[ ] SSH : root interdit, mot de passe interdit, AllowUsers deploy
[ ] fail2ban, unattended-upgrades, auditd, chrony actifs sur les deux
[ ] UFW db-01 : 5432 et 6379 restreints à 10.10.1.10
[ ] Horloge synchronisée sur les deux
[ ] Compte root verrouillé (passwd -S root → L)
[ ] Une seconde session SSH a été testée avant de fermer la première
[ ] Alertes SSH Discord en place (optionnel mais recommandé)
→ Suite : 04 — Nœud de données