# 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@ # 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":""}, {"op":"replace","path":"/data/AWS_SECRET_ACCESS_KEY","value":""} ]' 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@ \ "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é ```