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

181 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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@<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.
```bash
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 :
```bash
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 :
```bash
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 :
```bash
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
```bash
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](./04-noeud-donnees.md)**