# 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é
```
