# 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 ```bash scp -i ~/.ssh/xpeditis_prod infra/prod/scripts/00-bootstrap-common.sh \ deploy@:/tmp/ ssh -i ~/.ssh/xpeditis_prod deploy@ \ 'sudo bash /tmp/00-bootstrap-common.sh app' ``` ### db-01 `APP_PRIVATE_IP` détermine qui a le droit d'atteindre PostgreSQL et Redis. ```bash scp -i ~/.ssh/xpeditis_prod infra/prod/scripts/00-bootstrap-common.sh \ deploy@:/tmp/ ssh -i ~/.ssh/xpeditis_prod deploy@ \ '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 : ```bash ssh deploy@ ' 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 : ```bash ssh deploy@ 'sudo passwd -S root' # doit afficher "L" (locked) ``` ### 4.2 Alertes de connexion SSH Pour être prévenu de toute connexion réussie : ```bash ssh deploy@ '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@ ' sudo mkdir -p /etc/xpeditis echo "" | 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 ```bash ssh deploy@ '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@ '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](./04-noeud-donnees.md)**