Skip to content

Sprint 3 — Kubernetes, EKS, Pipeline avancĂ©e

19 incidents ! Envoy Gateway, RBAC, metrics-server, Ansible bootstrap, GitLab CI avancé.

Pour la version complÚte avec tous les détails : voir le fichier 007-incidents-lessons-learned.md dans le repo.


INC-013 Ă  INC-024 — RĂ©sumĂ©s rapides

INC-013 : ELB manquant aprĂšs Envoy Gateway

Fix : Vérifier que la Gateway API est correctement déployée (kubectl get gateway)

INC-014 : RBAC manquant pour bootstrap Job

Fix : Créer ServiceAccount + ClusterRoleBinding pour les actions bootstrap (Alembic, etc)

INC-015 : Metrics-server non installé (HPA échoue)

Fix : helm install metrics-server metrics-server/metrics-server -n kube-system

INC-016 : Alembic in-container vs init container

Fix : Utiliser init container pour migrations avant le pod FastAPI

INC-017 : Envoy Gateway upstream mutable

Fix : Vérifier la configuration HTTPRoute (host, port, service)

INC-018 : ConfigMap montée en volume vide

Fix : Vérifier que le ConfigMap existe et mountPath est correct

INC-019 : ServiceAccount token absent

Fix : Créer ServiceAccount, vérifier /var/run/secrets/kubernetes.io/serviceaccount/

INC-020 : Image ECR pull rate limit

Fix : Utiliser imagePullSecrets avec credentials AWS (ECR auth token)

INC-021 : Probes (liveness/readiness) timeout

Fix : Augmenter initialDelaySeconds et timeoutSeconds pour FastAPI slow startup

INC-022 : HPA metrics CPU insuffisantes

Fix : Vérifier requests/limits et que metrics-server remonte les données

INC-023 : Terraform ELB sec groups pas générés

Fix : Vérifier que kubernetes.io/service tags sont présents

INC-024 : GitLab CI job sans entrypoint sur images spéciales

Fix : Ajouter entrypoint: [""] pour alpine, aws-cli, terraform, kaniko, trivy


INC-025 à INC-030 — Variables GitLab CI & Pipeline

INC-025 : Variables projet écrasent variables YAML

Fix : Utiliser before_script: - export VAR=... pour forcer les values

INC-026 : terraform.tfvars gitignored → TF_VAR_ en CI

Fix : Utiliser TF_VAR_db_password en variables projet (Terraform lit auto TF_VAR_*)

INC-027 : pip introuvable dans alpine/ansible

Fix : apk add --no-cache py3-pip avant pip3 install

INC-028 : Envoy Gateway Helm repo HTTP 404 (migration OCI)

Fix : Utiliser chart_ref: oci://docker.io/envoyproxy/gateway-helm (OCI, pas HTTP repo)

INC-029 : eks:DescribeCluster manquant pour user CI

Fix : Ajouter IAM permission eks:DescribeCluster ou utiliser $AWS_INFRA_ACCESS_KEY_ID

INC-030 : needs DAG vs stages pour destroy infra

Fix : Utiliser stages: (ordre strict) au lieu de needs: pour les flows destructifs


📋 Incidents Sprint 3 par catĂ©gorie

Catégorie Incidents Fix time
Kubernetes/EKS INC-013, 015, 016, 017, 018, 019, 020, 021, 022, 023 30min-2h
GitLab CI avancé INC-024, 025, 026, 029, 030 5-15min
Tools/Ansible INC-027, 028 10min
Total Sprint 3 19 incidents —

EKS Access Entries + Deploy End-to-End (2026-05-19)

INC-031 — AmazonEKSClusterPolicy attachĂ©e Ă  un IAM user

SymptÎme : youss_admin ne peut pas kubectl malgré la policy attachée Cause : AmazonEKSClusterPolicy est pour le control plane EKS, pas pour un user Fix : Policy custom eks:DescribeCluster + eks:ListClusters uniquement

INC-032 — eks:DescribeCluster manquant pour gitlab-ci

SymptĂŽme : AccessDeniedException eks:DescribeCluster dans le deploy job Cause : policy ecr_push couvre ECR uniquement, pas EKS Fix : Ajouter statement EKSConnect Ă  la policy ecr_push

INC-033 — kubectl cluster-info forbidden

SymptĂŽme : cannot list services in namespace kube-system Cause : gitlab-ci a edit scope fastapi uniquement Fix : Remplacer par kubectl auth can-i create deployments -n fastapi

INC-034 — kubectl create namespace forbidden + doublon bootstrap

SymptÎme : namespaces is forbidden pour gitlab-ci Cause : Création namespace = opération cluster-level + déjà créé par bootstrap Fix : Supprimer du deploy job, déléguer au bootstrap Ansible

INC-035 — Init:InvalidImageName — ${ECR_IMAGE} non substituĂ©

SymptĂŽme : Failed to apply default image tag "${ECR_IMAGE}" Cause : kubectl apply sans envsubst → placeholder littĂ©ral dans K8s Fix : yum install gettext + envsubst < deployment.yaml | kubectl apply -f -

INC-036 — DB_PASSWORD manquant dans fastapi-secrets

SymptÎme : Pydantic ValidationError: DB_PASSWORD Field required Cause : Secret créé avec SECRET_KEY uniquement, DB_PASSWORD oublié Fix : Ajouter --from-literal=DB_PASSWORD="$TF_VAR_db_password"

INC-037 — terraform apply ancien pipeline → No changes

SymptÎme : Fix mergé mais policy non mise à jour dans AWS Cause : Apply depuis un ancien pipeline (avant le merge), code pas à jour Fix : git pull origin develop avant tout terraform apply manuel

INC-038 — State lock avec mauvais credentials

SymptĂŽme : Error acquiring the state lock avec youss_admin Cause : youss_admin n'a pas accĂšs S3/DynamoDB → AccessDenied masquĂ© en lock error Fix : Terraform = iamadmin uniquement / youss_admin = kubectl uniquement

INC-039 — Hardening Sprint 3 jamais appliquĂ© sur EKS (deploy fichier par fichier)

SymptĂŽme : PSA, ServiceAccount, PDB et NetworkPolicy absents du cluster malgrĂ© des manifests durcis et validĂ©s en local Cause : Le deploy appliquait 4 manifests sur 10 (configmap/service/hpa/deployment via envsubst), le reste n'atteignait jamais EKS Fix : kustomize edit set image fastapi=$ECR_IMAGE + kubectl apply -k k8s/base/ (les 10 ressources d'un coup) Leçon : DĂ©ployer le mĂȘme artefact que celui qu'on lint (kustomize build = source de vĂ©ritĂ©), tester end-to-end sur EKS et pas seulement en local

INC-040 — terraform apply rejetĂ© : accent dans une description de SG

SymptÎme : egress.0.description doesn't comply with restrictions (regex ASCII AWS) sur module.rds.aws_security_group.rds Cause : Le "é" de "Réponses" dans la description egress du SG RDS ; AWS valide les descriptions de rÚgles SG contre une regex strictement ASCII Fix : Descriptions egress (RDS + EKS) réécrites en anglais ASCII Leçon : Tout en anglais ASCII dans la conf ; ni tfsec ni terraform validate/plan n'attrapent ça (contrainte imposée par l'API AWS à l'apply)


Security Hardening Audit (2026-05-20)

Audit de sĂ©curitĂ© complet rĂ©alisĂ© en prĂ©paration des entretiens techniques. Trois failles corrigĂ©es via GitFlow professionnel (issue → branch → MR → pipeline green → squash merge).

SEC-001 — Mot de passe DB en clair dans les docs publiques (issue #41)

SĂ©vĂ©ritĂ© : Critical Fichiers : docs/infra-eks-summary.md, docs/adr/007-incidents-lessons-learned.md SymptĂŽme : TF_VAR_db_password avec sa valeur rĂ©elle visible dans le repo public Fix : Remplacement par <REDACTED> + rotation password GitLab CI + tfvars local Leçon : Ne jamais Ă©crire de valeurs rĂ©elles dans la documentation, mĂȘme Ă  titre d'exemple


SEC-002 — CORS misconfiguration (issue #42)

SĂ©vĂ©ritĂ© : High Fichiers : app/main.py, app/config.py SymptĂŽme : allow_origins=["*"] + allow_credentials=True → combinaison interdite par la spec CORS, tous les navigateurs rejettent silencieusement Fix : Variable ALLOWED_ORIGINS via pydantic_settings (dĂ©faut http://localhost:3000), configurable par environnement Leçon : allow_credentials=True exige des origins explicites, jamais de wildcard


SEC-003 — securityContext container incomplet (issue #43)

SĂ©vĂ©ritĂ© : High Fichiers : k8s/base/deployment.yaml SymptĂŽme : Hardening container-level absent (allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities, seccompProfile) Fix : Ajout des 4 paramĂštres sur initContainer + container fastapi + emptyDir /tmp Validation : TestĂ© localement sur kind — securityContext acceptĂ© par Kubernetes sans erreur Leçon : Le securityContext pod-level (runAsNonRoot) ne suffit pas, le hardening container-level est obligatoire


SEC-004 — Haute disponibilitĂ© absente (issue #44)

SĂ©vĂ©ritĂ© : High Fichiers : k8s/base/deployment.yaml, k8s/base/pdb.yaml (nouveau) SymptĂŽme : replicas: 1 = SPOF, aucun PodDisruptionBudget = pod dĂ©truit sans garantie lors d'un node drain Fix : replicas 1 → 2, ajout d'un PDB avec minAvailable: 1 Leçon : replicas >= 2 + PDB est le minimum pour toute application en production


SEC-005 — ServiceAccount default avec token montĂ© (issue #45)

SĂ©vĂ©ritĂ© : High Fichiers : k8s/base/serviceaccount.yaml (nouveau), k8s/base/deployment.yaml SymptĂŽme : Pod utilisait le SA default avec token Kubernetes montĂ© automatiquement — vecteur d'attaque si RCE Fix : SA dĂ©diĂ© fastapi avec automountServiceAccountToken: false, dĂ©fense en profondeur appliquĂ©e aussi au PodSpec Leçon : Un ServiceAccount dĂ©diĂ© par application, token dĂ©sactivĂ© sauf besoin explicite (API K8s ou IRSA AWS)


Bootstrap EKS — Marathon authentification (2026-05-23)

Une journée entiÚre de debug sur le job bootstrap (Ansible kubernetes.core). Cause finale : une régression dans une lib Python upstream. Quatre incidents empilés ont masqué la vraie cause et allongé le diagnostic.

INC-041 — kubectl i/o timeout : jobs sur runner GitLab partagĂ©

SymptĂŽme : kubectl get nodes → dial tcp ...:443: i/o timeout sur infra-start et bootstrap Cause : .gitlab-ci-infra.yml sans default: tags, les jobs tournaient sur un runner GitLab partagĂ© (GCP, IP 35.x) au lieu du runner self-hosted. L'IP du runner partagĂ© n'Ă©tait pas dans cluster_public_access_cidrs (82.66.53.81/32) → EKS drop les paquets silencieusement → timeout Fix : Ajout de default: tags: [ubuntu] pour forcer tous les jobs sur le runner self-hosted Leçon : Toujours pinner le runner via tags. Un i/o timeout sur l'API EKS = problĂšme rĂ©seau/CIDR, pas auth (qui donnerait 401/403)

INC-042 — Variable Protected non injectĂ©e sur feature branch

SymptĂŽme : TF_VAR_cluster_public_access_cidrs absente en CI sur les branches non protĂ©gĂ©es, terraform utilisait le default placeholder 203.0.113.0/24 Cause : La variable est "Protected" dans GitLab → injectĂ©e uniquement sur branches/tags protĂ©gĂ©s. Tester un fix sur une feature branch fausse le rĂ©sultat Fix : Pour les valeurs non secrĂštes (une IP), dĂ©cocher "Protected". Garder Protected pour les vrais secrets (clĂ©s AWS) Leçon : Distinguer secret (Protected) de non-secret. Une variable Protected casse la capacitĂ© Ă  valider en MR avant merge

INC-043 — Ansible : until condition crash + connexion SSH sur localhost

SymptÎme : (1) object of type 'dict' has no attribute 'resources' ; (2) ssh: connect to host localhost port 22: Connection refused Cause : (1) until: nodes.resources | length > 0 plante quand k8s_info échoue (pas de clé resources) ; (2) -i localhost, fait traiter localhost comme hÎte distant SSH Fix : (1) until: (nodes.resources | default([])) | length > 0 ; (2) connection: local dans le play Leçon : Toujours default() une variable registered dans une condition until ; connection: local pour les plays kubernetes.core qui tournent en local

INC-044 — Bootstrap 401 : rĂ©gression de la lib Python kubernetes 36.0.0

SymptĂŽme : kubernetes.core → 401 Unauthorized sur toutes les tasks, alors que kubectl get nodes passe dans le before_script avec les mĂȘmes credentials Cause : RĂ©gression dans la lib Python kubernetes 36.0.0 qui casse l'auth EKS. Le MÊME token est acceptĂ© par kubectl et curl brut (HTTP 200) mais rejetĂ© par le client v36 (401), sur Alpine ET Ubuntu. pip install kubernetes (non pinnĂ©) tirait la 36 alors qu'une version antĂ©rieure marchait 4 jours avant Diagnostic : Reproduit en local hors pipeline. kubernetes==31.0.0 + config.load_kube_config() → node listĂ© ; v36 → 401. Élimination mĂ©thodique : token (curl 200), identitĂ© (auth whoami OK), OS (Ă©chec Ubuntu aussi), version (31 OK / 36 KO) Fix : Pin pip3 install kubernetes==31.0.0 dans bootstrap + teardown Leçon : Pinner les versions des libs critiques en CI. pip install <lib> sans pin = une release upstream peut casser l'infra du jour au lendemain, sans aucun changement de ton cĂŽtĂ©

Méta-leçons du marathon

  • Tester Ă  chaque itĂ©ration. Batcher plusieurs changements infra non testĂ©s (la veille) a empilĂ© 4 causes distinctes, rendant le diagnostic trĂšs long et coĂ»teux (cluster qui tourne = argent)
  • Descendre au niveau le plus bas pour isoler. curl brut > client Python > Ansible : tester chaque couche a permis d'isoler la rĂ©gression de lib
  • Reproduire en local dĂšs que possible. Sortir de la boucle "merge → pipeline → attendre → cluster coĂ»te" en reproduisant le bug en local = itĂ©rations en secondes
  • Lire le bon signal. i/o timeout = rĂ©seau/CIDR ; 401 = authentification ; 403 = autorisation. Le code d'erreur oriente le diagnostic

Deploy E2E + ownership du namespace (2026-05-24)

AprĂšs le fix #52 (deploy via kubectl apply -k), premiĂšre validation end-to-end du deploy applicatif avec l'infra up. Le deploy applique bien le hardening workload, mais un test nĂ©gatif mĂ©thodique rĂ©vĂšle que deux contrĂŽles de sĂ©curitĂ© n'avaient jamais Ă©tĂ© enforced sur EKS : PSA (INC-045) puis les NetworkPolicy (INC-046), mĂȘme cause racine cĂŽtĂ© CNI.

INC-045 — PSA jamais enforced sur EKS : namespace ownĂ© par le bootstrap, pas par le deploy

SymptĂŽme : kubectl apply -k k8s/base/ → Error from server (Forbidden) sur le patch du Namespace fastapi (labels PSA). Le reste du hardening (SA, NetworkPolicy, PDB, deployment) s'applique quand mĂȘme Cause : Le Namespace est cluster-scoped. Le user CI deploy est en least-privilege (edit dans le ns fastapi uniquement), il ne peut pas modifier l'objet Namespace. Or namespace.yaml Ă©tait dans k8s/base, donc le deploy tentait de poser les labels PSA → Forbidden. Le bootstrap Ansible crĂ©ait le ns SANS labels → PSA enforce: restricted (cru actif depuis #48) n'a jamais atteint EKS Fix : Ownership du namespace dĂ©placĂ© vers le bootstrap (cluster-admin) : ansible/bootstrap.yml crĂ©e le ns avec les labels PSA restricted. namespace.yaml retirĂ© de k8s/base (le deploy ne gĂšre plus le ns). local-kind garde son ns baseline en resource propre Preuve : AprĂšs kubectl label ns fastapi ...=restricted, rollout 1/1 sous enforce restricted (workload C3 compliant admis) + pod nginx non conforme rejetĂ© Ă  l'admission (4 violations restricted). Le contrĂŽle bloque rĂ©ellement un dĂ©ploiement non durci Leçon : Une ressource cluster-scoped (Namespace + labels PSA) doit ĂȘtre ownĂ©e par le bootstrap cluster-admin, pas par le deploy least-privilege. Un hardening "validĂ© en kind" peut ne jamais atteindre la prod si le path d'application diffĂšre. Toujours vĂ©rifier l'Ă©tat rĂ©el (kubectl get ns --show-labels), pas l'intention dans les manifests

INC-046 — NetworkPolicy appliquĂ©es mais non enforced sur EKS (VPC CNI self-managed)

SymptĂŽme : Dans la foulĂ©e du fix PSA, test nĂ©gatif sur les NetworkPolicy : depuis le pod fastapi, une sortie sur le port 80 (hors allowlist egress) connecte alors que le couple default-deny-all + allow-fastapi devrait la bloquer. Les 3 policies sont pourtant bien prĂ©sentes (kubectl get netpol -n fastapi) Cause : Une NetworkPolicy n'est enforced que par un CNI qui l'implĂ©mente. Le cluster tourne avec le VPC CNI en self-managed (dĂ©faut EKS) : aucun addon EKS managĂ© dĂ©clarĂ©, confirmĂ© deux fois — aucun aws_eks_addon dans le Terraform ET aws eks list-addons → []. Il n'existe donc nulle part oĂč poser enableNetworkPolicy, et le VPC CNI par dĂ©faut ne l'active pas. MĂȘme classe de problĂšme que PSA (INC-045) Statut : Mitigation suivie dans #55 — dĂ©clarer vpc-cni comme addon EKS managĂ© (aws_eks_addon, version ≄ 1.14) avec configuration_values = { enableNetworkPolicy = "true" }, puis re-valider par test nĂ©gatif. Non encore livrĂ© Ă  la date de cet incident Leçon : « ConfigurĂ© n'est pas enforced » (2e occurrence aprĂšs PSA). Un manifest NetworkPolicy valide, appliquĂ© et lintĂ© ne protĂšge rien si le CNI ne l'enforce pas. La seule preuve fiable est le test nĂ©gatif sur le cluster rĂ©el (tenter un flux qui doit ĂȘtre bloquĂ©), jamais l'intention lue dans les manifests. Sur EKS : NetworkPolicy ⇒ VPC CNI en addon managĂ© + enableNetworkPolicy=true


ESO + IRSA — RBAC dĂ©ploiement (2026-05-29)

Suite directe d'INC-045/046 sur la mĂȘme classe de problĂšme : on pose des ressources sans Ă©tendre les permissions du compte qui doit les appliquer. Cette fois ce sont les CRDs ESO qui ne sont pas couvertes par le ClusterRole built-in edit.

INC-047 — Premier essai : ClusterRole d'agrĂ©gation sur les CRDs ESO (deploy app 403) (2026-05-29)

SymptĂŽme : Run du deploy app post-merge MR-D (#33), kubectl apply -k k8s/base/ tombe en Forbidden sur les nouveaux manifests ESO : externalsecrets.external-secrets.io "fastapi-secrets" is forbidden: User "arn:aws:iam::199167114788:user/ci/fastapi-eks-gitlab-ci" cannot get resource "externalsecrets" in API group "external-secrets.io". Idem secretstores. Le 403 a lieu sur le get server-side du 3-way merge, AVANT l'apply Cause (diagnostic initial) : Le user CI est mappĂ© via EKS Access Entry Ă  AmazonEKSEditPolicy scopĂ©e au ns fastapi, correspondant au ClusterRole built-in edit. Or edit est agrĂ©gĂ© et ne couvre pas les CRDs de tiers par dĂ©faut. HypothĂšse initiale : Ă©tendre edit via le label aggregate-to-edit: "true" suffit. Fix tentĂ© : ClusterRole aggregate-eso-to-edit dans ansible/bootstrap.yml. Structurellement correct : kubectl get clusterrole edit -o yaml | grep external-secrets → 3 entrĂ©es dans edit.rules. Mais kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> → no. Re-run pipeline → MÊME 403. Cause rĂ©elle : AmazonEKSEditPolicy est un snapshot statique AWS-managed, pas un binding vers le ClusterRole K8s edit agrĂ©gĂ©. L'agrĂ©gation K8s ne se propage pas aux users mappĂ©s via EKS Access Entry + AWS Access Policy. Voir INC-048 pour le vrai fix. Leçon : Le can-i --as=<ARN> Ă©tait fiable, mal interprĂ©tĂ© comme faux nĂ©gatif. Un snapshot AWS ≠ un ClusterRole K8s vivant. ConservĂ© en doc : matĂ©riau d'entretien (diagnostic en couches, humilitĂ© technique).

INC-048 — AmazonEKSEditPolicy snapshot statique : seul un binding RBAC explicite fonctionne (2026-05-30)

SymptĂŽme : MĂȘmes 403 qu'INC-047 malgrĂ© la ClusterRole aggregate-eso-to-edit en place. kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> → no (fiable). Cause : AmazonEKSEditPolicy est une policy AWS-managed, snapshot statique des permissions view+edit. Elle ne suit pas l'agrĂ©gation RBAC Kubernetes. Étendre le ClusterRole edit via aggregate-to-edit n'a aucun effet sur un user mappĂ© via EKS Access Entry + AWS Access Policy. Fix : Role fastapi-deploy-eso (ns fastapi, rules get,list,watch,create,update,patch,delete sur externalsecrets/secretstores) + RoleBinding bindant arn:aws:iam::{{ aws_account }}:user/ci/{{ project_name }}-gitlab-ci. PosĂ©s par le bootstrap (cluster-admin). ClusterRole aggregate-eso-to-edit retirĂ©e. Preuve : kubectl auth can-i create externalsecrets -n fastapi --as=<ARN> → yes. Pipeline app DEPLOY=true vert. SecretStore Valid, ExternalSecret SecretSynced, pods fastapi 1/1 Ready. curl https://api.devopsyouss.com/healthz/ready → 200. Leçon : Pour les users mappĂ©s via EKS Access Entry + AWS Access Policy, seul un binding RBAC K8s explicite (Role/RoleBinding) sur l'ARN du user fonctionne. L'agrĂ©gation K8s ne traverse pas la couche AWS. INC-047 conservĂ© : matĂ©riau d'entretien (diagnostic en couches, humilitĂ© technique).


🎯 Points clĂ©s Sprint 3

  1. Kubernetes est complexe : RBAC, ServiceAccount, probes, metrics...
  2. Envoy Gateway = ressources dynamiques : ELB, sec groups, ENIs → cleanup obligatoire
  3. Variables GitLab CI : ordre strict de prioritĂ© → toujours tester en feature branch
  4. Images Docker non-standard : ajout entrypoint: [""] obligatoire
  5. Migrations DB : utiliser init container, pas pod principal
  6. EKS Access Entries : deux couches distinctes IAM (DescribeCluster) + K8s RBAC (AccessEntry)
  7. Least privilege deploy : bootstrap = cluster-admin / deploy = edit ns uniquement
  8. Pinner les libs critiques en CI : pip install <lib> sans version = une régression upstream peut casser l'infra (cf INC-044, kubernetes 36.0.0)
  9. Reproduire en local : sortir de la boucle pipeline coûteuse, isoler couche par couche (curl > client Python > Ansible)
  10. Configuré n'est pas enforced : PSA (INC-045) et NetworkPolicy (INC-046) étaient appliqués mais inertes (ns sans labels / VPC CNI self-managed). Valider tout contrÎle de sécurité par test négatif sur le cluster réel, jamais par l'intention dans les manifests

📖 Recommandations pour Sprint 4+

Voir Recommandations pour les best practices consolidées.


🔗 Ressources utiles

  • GitLab CI docs: https://docs.gitlab.com/ee/ci/
  • Envoy Gateway: https://gateway.envoyproxy.io/
  • Kubernetes: https://kubernetes.io/docs/
  • AWS EKS: https://docs.aws.amazon.com/eks/

Pour les détails complets de chaque incident : voir 007-incidents-lessons-learned.md du repo.


Sprint 3 complet — 2026-05-19 âœ