xpeditis2.0/docs/deployment/hetzner/14-security-hardening.md
2026-03-26 18:08:28 +01:00

350 lines
8.6 KiB
Markdown

# 14 — Sécurité et hardening
---
## Couches de sécurité
```
Internet
▼ Couche 1 : Cloudflare (WAF, DDoS, Bot protection)
▼ Couche 2 : Hetzner Firewall (ports, IP whitelist)
▼ Couche 3 : k3s Network Policies (isolation namespace)
▼ Couche 4 : NestJS Guards (JWT, Rate Limiting, Roles)
▼ Couche 5 : PostgreSQL (SSL, auth md5)
```
---
## Hardening des nœuds Hetzner
Ces commandes sont exécutées automatiquement via `post_create_commands` dans `cluster.yaml`, mais voici les détails :
```bash
# Se connecter sur chaque nœud
ssh -i ~/.ssh/xpeditis_hetzner root@<NODE_IP>
# 1. Mettre à jour le système
apt-get update && apt-get upgrade -y
# 2. Désactiver le login root par mot de passe (SSH key uniquement)
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl restart sshd
# 3. Configurer fail2ban
apt-get install -y fail2ban
cat > /etc/fail2ban/jail.d/sshd.conf << 'EOF'
[sshd]
enabled = true
maxretry = 3
bantime = 3600
findtime = 600
EOF
systemctl enable fail2ban && systemctl restart fail2ban
# 4. Configurer le firewall UFW (en plus du firewall Hetzner)
apt-get install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow from 10.0.0.0/16 # Réseau privé Hetzner
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP (LB)
ufw allow 443/tcp # HTTPS (LB)
ufw allow 6443/tcp # K8s API
ufw --force enable
# 5. Kernel hardening
cat >> /etc/sysctl.d/99-security.conf << 'EOF'
# Désactiver les paquets IP forwardés depuis des sources inconnues
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignorer les ICMP broadcasts
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Désactiver l'acceptation des redirections ICMP
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
# SYN flood protection
net.ipv4.tcp_syncookies = 1
EOF
sysctl -p /etc/sysctl.d/99-security.conf
```
---
## Network Policies Kubernetes
Les NetworkPolicies limitent les communications entre pods :
```bash
cat > /tmp/network-policies.yaml << 'EOF'
# Politique par défaut : bloquer tout trafic entrant dans le namespace
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: xpeditis-prod
spec:
podSelector: {}
policyTypes:
- Ingress
# Autoriser le trafic depuis Traefik vers le backend
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-traefik-to-backend
namespace: xpeditis-prod
spec:
podSelector:
matchLabels:
app: xpeditis-backend
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- port: 4000
# Autoriser le trafic depuis Traefik vers le frontend
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-traefik-to-frontend
namespace: xpeditis-prod
spec:
podSelector:
matchLabels:
app: xpeditis-frontend
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- port: 3000
# Autoriser le trafic du backend vers Redis (self-hosted)
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-redis
namespace: xpeditis-prod
spec:
podSelector:
matchLabels:
app: redis
ingress:
- from:
- podSelector:
matchLabels:
app: xpeditis-backend
ports:
- port: 6379
# Autoriser Prometheus à scraper les métriques du backend
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: xpeditis-prod
spec:
podSelector:
matchLabels:
app: xpeditis-backend
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 4000
EOF
kubectl apply -f /tmp/network-policies.yaml
```
---
## Rotation des secrets
### Script de rotation du JWT secret
```bash
#!/bin/bash
# scripts/rotate-jwt-secret.sh
# ⚠️ Cette opération déconnecte TOUS les utilisateurs connectés
set -e
echo "⚠️ Rotation du JWT Secret — tous les utilisateurs seront déconnectés"
read -p "Confirmer ? (yes/no): " CONFIRM
[ "$CONFIRM" != "yes" ] && exit 1
# Générer un nouveau secret
NEW_SECRET=$(openssl rand -base64 48)
# Mettre à jour le Secret Kubernetes
kubectl patch secret backend-secrets -n xpeditis-prod \
--type='json' \
-p="[{\"op\":\"replace\",\"path\":\"/data/JWT_SECRET\",\"value\":\"$(echo -n $NEW_SECRET | base64)\"}]"
# Redémarrer les pods pour prendre en compte le nouveau secret
kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod
# Attendre
kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=120s
echo "✅ JWT Secret roté. Tous les utilisateurs devront se reconnecter."
```
### Rotation des credentials Hetzner Object Storage
```bash
# 1. Dans la console Hetzner → Object Storage → Access Keys → Generate new key
# 2. Mettre à jour le Secret Kubernetes avec les nouvelles valeurs
# 3. Redémarrer les pods
kubectl patch secret backend-secrets -n xpeditis-prod \
--type='json' \
-p='[
{"op":"replace","path":"/data/AWS_ACCESS_KEY_ID","value":"<NEW_KEY_BASE64>"},
{"op":"replace","path":"/data/AWS_SECRET_ACCESS_KEY","value":"<NEW_SECRET_BASE64>"}
]'
kubectl rollout restart deployment/xpeditis-backend -n xpeditis-prod
# 4. Supprimer l'ancienne clé dans la console Hetzner
```
---
## Sécurisation des accès Kubernetes
### RBAC — Utilisateur de déploiement limité
```bash
cat > /tmp/rbac-deploy.yaml << 'EOF'
# Utilisateur de déploiement CI/CD (accès limité au namespace xpeditis-prod)
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deploy
namespace: xpeditis-prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer
namespace: xpeditis-prod
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deploy-binding
namespace: xpeditis-prod
subjects:
- kind: ServiceAccount
name: ci-deploy
namespace: xpeditis-prod
roleRef:
kind: Role
name: deployer
apiGroup: rbac.authorization.k8s.io
EOF
kubectl apply -f /tmp/rbac-deploy.yaml
# Générer un kubeconfig limité pour le CI (alternative au kubeconfig admin)
SECRET_NAME=$(kubectl get serviceaccount ci-deploy -n xpeditis-prod \
-o jsonpath='{.secrets[0].name}')
TOKEN=$(kubectl get secret $SECRET_NAME -n xpeditis-prod \
-o jsonpath='{.data.token}' | base64 -d)
# Utiliser ce token dans GitHub Secrets pour le CI (plus sécurisé que le kubeconfig admin)
echo "Token CI : $TOKEN"
```
---
## Audit des accès
```bash
# Vérifier les dernières connexions SSH sur les nœuds
for NODE in $(hcloud server list -o columns=name --no-header); do
IP=$(hcloud server ip $NODE)
echo "=== $NODE ($IP) ==="
ssh -i ~/.ssh/xpeditis_hetzner root@$IP "last -20 | head -10"
done
# Vérifier les événements Kubernetes suspects
kubectl get events -A --field-selector type=Warning | grep -v "Normal"
# Vérifier les tentatives d'accès bloquées par fail2ban
ssh -i ~/.ssh/xpeditis_hetzner root@<NODE_IP> \
"fail2ban-client status sshd"
```
---
## Checklist de sécurité
```
Infrastructure
□ Token API Hetzner limité au projet (read+write minimum nécessaire)
□ Firewall Hetzner : SSH uniquement depuis votre IP
□ fail2ban actif sur tous les nœuds
□ Mises à jour OS automatiques (unattended-upgrades)
Kubernetes
□ NetworkPolicies appliquées
□ Secrets dans Kubernetes (pas dans les ConfigMaps)
□ k8s/01-secrets.yaml dans .gitignore
□ RBAC CI/CD avec ServiceAccount limité
□ Pod Security Standards activés
Application
□ JWT_SECRET 48+ caractères aléatoires
□ NEXTAUTH_SECRET différent du JWT_SECRET
□ Stripe en mode live (pas test) en production
□ Sentry configuré pour les erreurs
□ SMTP_FROM vérifié (SPF/DKIM dans Brevo/SendGrid)
TLS/DNS
□ Cloudflare SSL mode "Full (strict)"
□ HSTS activé (stsPreload: true dans Traefik)
□ Certificats Let's Encrypt valides (READY=True)
□ HTTP → HTTPS redirect actif
Backups
□ Backup PostgreSQL quotidien testé
□ Secrets Kubernetes sauvegardés chiffrés
□ Test de restauration effectué
```