Skip to content

ADR 008 — Security Hardening Sprint 3 (2026-05-20)

Contexte

Audit de sĂ©curitĂ© complet rĂ©alisĂ© le 2026-05-20 sur le projet en prĂ©paration des entretiens techniques. Trois failles critiques ou hautes identifiĂ©es et corrigĂ©es dans la mĂȘme session via GitFlow professionnel (issue GitLab → feature branch → MR → pipeline green → squash merge).


DĂ©cision 1 — Suppression du mot de passe DB en clair dans les docs (C4)

Issue GitLab : #41 priority::critical

ProblĂšme

TF_VAR_db_password avec sa valeur réelle apparaissait en clair dans deux fichiers de documentation trackés dans un repo public :

  • docs/infra-eks-summary.md
  • docs/adr/007-incidents-lessons-learned.md

Fix

Remplacement par <REDACTED - stored in GitLab CI as Masked variable> dans les deux fichiers.

Remédiation complÚte : - Nouveau password généré - Variable GitLab CI mise à jour (Masked + Protected) - terraform/ephemeral/terraform.tfvars mis à jour localement (gitignored) - Rotation RDS différée au prochain aws-start.sh (infra DOWN)

Leçon

Ne jamais Ă©crire de valeurs rĂ©elles dans la documentation, mĂȘme Ă  titre d'exemple. Utiliser systĂ©matiquement des placeholders ou des rĂ©fĂ©rences vers le secret manager.


DĂ©cision 2 — Correction CORS misconfiguration (C1)

Issue GitLab : #42 priority::high

ProblĂšme

app/main.py configurait CORSMiddleware avec allow_origins=["*"] ET allow_credentials=True. Cette combinaison est interdite par la spec CORS (RFC 6454) et rejetée silencieusement par tous les navigateurs modernes. L'API aurait été inaccessible depuis tout frontend.

Fix

Ajout de ALLOWED_ORIGINS: str = "http://localhost:3000" dans app/config.py via le pattern pydantic_settings existant. app/main.py utilise désormais :

origins = [o.strip() for o in settings.ALLOWED_ORIGINS.split(",")]

Configuration par environnement

Environnement Valeur ALLOWED_ORIGINS
Dev local http://localhost:3000 (défaut)
CI tests http://localhost:3000 (défaut, non-browser)
EKS prod (Sprint 5) https://app.devopsyouss.com (variable GitLab CI)

Leçon

allow_credentials=True exige des origins explicites. Le wildcard * est réservé aux APIs publiques sans authentification.


DĂ©cision 3 — Hardening securityContext container K8s (C3)

Issue GitLab : #43 priority::high

ProblĂšme

k8s/base/deployment.yaml avait un securityContext partiel : runAsNonRoot, runAsUser et fsGroup étaient configurés au niveau pod, mais les hardening container-level étaient absents. Un attaquant exploitant une faille dans l'app pouvait encore écrire des fichiers arbitraires, appeler des syscalls dangereux ou tenter une élévation de privilÚges.

Fix

Ajout sur l'initContainer (migrations) et le container fastapi :

securityContext:
  allowPrivilegeEscalation: false   # bloque toute élévation de privilÚges
  readOnlyRootFilesystem: true      # filesystem en lecture seule
  capabilities:
    drop: [ALL]                     # supprime toutes les Linux capabilities
  seccompProfile:
    type: RuntimeDefault            # filtre les syscalls autorisés

Un volume emptyDir est monté sur /tmp pour les deux containers afin de supporter readOnlyRootFilesystem: true sans bloquer les éventuelles écritures temporaires Python.

Validation

Fix validé localement sur kind (Kubernetes IN Docker) avec image python:3.11-slim. Le securityContext a été accepté par Kubernetes sans erreur. Validation runtime complÚte au prochain aws-start.sh (liveness/readiness probes).

Leçon

Le securityContext pod-level (runAsNonRoot) n'est pas suffisant. Le hardening container-level (allowPrivilegeEscalation, capabilities, seccompProfile) est nécessaire pour respecter le principe de moindre privilÚge au niveau process.


DĂ©cision 4 — Haute disponibilitĂ© (P1)

Issue GitLab : #44 priority::high type::infra

ProblĂšme

replicas: 1 dans le deployment = Single Point Of Failure. Aucun PodDisruptionBudget dĂ©fini = lors d'opĂ©rations de maintenance Kubernetes (node drain, rolling update, eviction), le pod unique peut ĂȘtre dĂ©truit sans garantie de remplacement immĂ©diat, provoquant une indisponibilitĂ© de l'API.

Fix

  • k8s/base/deployment.yaml : replicas: 1 → replicas: 2
  • k8s/base/pdb.yaml (nouveau) : PodDisruptionBudget avec minAvailable: 1
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: fastapi
  namespace: fastapi
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: fastapi

Effet combiné

Scénario Avant AprÚs
Node drain API DOWN 1 pod survit, API UP
Rolling update API DOWN briÚvement maxSurge respecté, API UP
Pod crash DOWN jusqu'au reschedule 1 pod restant gĂšre les requĂȘtes

Leçon

replicas >= 2 + PodDisruptionBudget est le minimum pour toute application en production. Le PDB est obligatoire pour respecter minAvailable pendant les voluntary disruptions Kubernetes.


DĂ©cision 5 — ServiceAccount dĂ©diĂ© et least privilege (P3)

Issue GitLab : #45 priority::high type::security

ProblĂšme

Le deployment utilisait le ServiceAccount default du namespace, avec montage automatique du token (/var/run/secrets/kubernetes.io/serviceaccount/token). Tout process dans le pod (y compris un attaquant suite à un RCE) pouvait appeler l'API Kubernetes avec cette identité partagée.

Fix

  • k8s/base/serviceaccount.yaml (nouveau) : SA dĂ©diĂ© fastapi, automountServiceAccountToken: false
  • k8s/base/deployment.yaml : ajout de serviceAccountName: fastapi + automountServiceAccountToken: false au PodSpec (dĂ©fense en profondeur)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fastapi
  namespace: fastapi
automountServiceAccountToken: false

Pourquoi désactiver le token

FastAPI n'a aucun besoin de parler à l'API Kubernetes. Elle communique uniquement avec PostgreSQL via le réseau. Désactiver l'automount supprime un vecteur d'attaque sans aucun impact fonctionnel.

Préparation pour ESO + IRSA (futur)

Le SA dédié sera la cible naturelle de l'annotation IRSA quand External Secrets Operator sera installé (gap C5) :

metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/fastapi-eso

À ce moment-lĂ , le token devra ĂȘtre rĂ©activĂ© pour le SA dĂ©diĂ© Ă  ESO uniquement, pas pour FastAPI.

Leçon

Chaque pod doit utiliser un ServiceAccount dédié avec automountServiceAccountToken: false par défaut. Le token n'est activé que pour les pods qui doivent réellement parler à l'API Kubernetes ou à AWS via IRSA.


Résumé des décisions

Décision Issue Priorité Fichiers modifiés
Suppression password en clair #41 critical docs/infra-eks-summary.md, docs/adr/007-incidents-lessons-learned.md
Fix CORS misconfiguration #42 high app/main.py, app/config.py
Hardening securityContext #43 high k8s/base/deployment.yaml
Haute disponibilité (replicas + PDB) #44 high k8s/base/deployment.yaml, k8s/base/pdb.yaml (nouveau)
ServiceAccount dédié + least privilege #45 high k8s/base/serviceaccount.yaml (nouveau), k8s/base/deployment.yaml

Date : 2026-05-20 Sprint : 3