350 lines
8.6 KiB
Markdown
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é
|
|
```
|