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.mddocs/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: 2k8s/base/pdb.yaml(nouveau) :PodDisruptionBudgetavecminAvailable: 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: falsek8s/base/deployment.yaml: ajout deserviceAccountName: fastapi+automountServiceAccountToken: falseau 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