Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
181 lines
6.0 KiB
Markdown
181 lines
6.0 KiB
Markdown
# 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)**
|