xpeditis2.0/docs/mise-en-prod/03-durcissement-serveurs.md
David b22f4e0b74 docs: procedure de mise en production pas a pas
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:50 +02:00

6.0 KiB
Raw Permalink Blame History

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