Skip to content

ADR 032 — Privilèges des clés AWS de la CI (2026-08-30)

Statut

Accepté, livré le 2026-08-30. Issue #184, Sprint 7 (Remédiation & Fiabilité). Constats AW-2 et EK-16 de l'audit DevSecOps, action de remédiation 37.

Numéro 032 et non 031 : ADR 031 est réservée à #174 (remplacement du teardown programmé par une alerte EventBridge), décidée le 2026-08-29 mais pas encore écrite.

Cette ADR écarte la séparation des clés ECR, qui était l'option retenue au cadrage de l'issue. La raison de ce revirement est le cœur du document.

Contexte

Le pipeline applicatif s'authentifie auprès d'AWS avec des clés statiques rangées dans les variables CI du projet. L'audit relève que aucune des six variables porteuses d'un secret n'était cochée « protégée » (mesuré le 2026-08-30 via l'API GitLab).

Une variable protégée n'est servie qu'aux branches et tags protégés, ici develop et main. Non protégée, elle est servie à n'importe quelle branche de travail, donc à tout pipeline de merge request.

Variable masquée protégée (avant)
AWS_ACCESS_KEY_ID oui non
AWS_SECRET_ACCESS_KEY oui non
AWS_INFRA_ACCESS_KEY_ID oui non
AWS_INFRA_SECRET_ACCESS_KEY oui non
TF_VAR_db_password oui non
SECRET_KEY oui non

Masquée n'est pas protégée. Masquer cache la valeur dans les logs, protéger décide à qui elle est servie. Les deux répondent à des questions différentes, et la confusion entre les deux est à l'origine d'INC-001.

Les six variables ne pèsent pas le même poids, ce que le tableau de l'audit ne dit pas :

  • AWS_INFRA_* porte l'utilisateur fastapi-eks-gitlab-ci-infra et la policy terraform_ci : ec2:*, eks:*, rds:*, ecr:*, plus iam:CreateUser, iam:CreateAccessKey et iam:AttachUserPolicy. Avec ces trois derniers appels, on se fabrique un administrateur. C'est une prise de contrôle du compte AWS, pas un accès à un registre.
  • AWS_ACCESS_KEY_ID porte fastapi-eks-gitlab-ci et la policy ecr_push : push ECR plus eks:DescribeCluster. Portée déjà étroite.
  • TF_VAR_db_password est le mot de passe RDS, lu par Terraform seul.
  • SECRET_KEY signe les jetons JWT des tests (décision 3).

Décision

1. AWS_INFRA_* et TF_VAR_db_password passent en « protégée »

C'est le correctif qui compte, et il ne coûte rien.

Les jobs de .gitlab-ci-infra.yml se lancent manuellement depuis develop, qui est protégée. Ceux qui lisent TF_VAR_db_password sont les mêmes. Aucun pipeline de merge request ne les utilise : la protection ne casse rien.

Preuve que le mécanisme fonctionne : sept variables du projet sont déjà protégées et servies sans incident, CLOUDFLARE_API_TOKEN et GITLAB_PUSH_TOKEN en tête.

2. La paire ECR n'est PAS dédoublée, et reste non protégée

C'était l'option retenue au cadrage : créer un second utilisateur fastapi-eks-gitlab-ci-mr, lui donner une clé non protégée servie aux pipelines de merge request, et protéger la clé existante réservée aux jobs de déploiement.

Elle est écartée après écriture du code, sur une objection simple : les deux policies auraient été identiques.

ECR ne sait pas distinguer « poser le tag candidate- » de « poser le tag deployed- ». C'est le même appel ecr:PutImage, et il n'existe aucune condition IAM portant sur le nom d'un tag d'image. Les deux identités poussent sur les deux mêmes dépôts, donc elles autorisent exactement les mêmes appels.

Une clé de MR en lecture seule est par ailleurs impossible : build-candidate pousse réellement une image candidate dans ECR pendant le pipeline de MR.

Conséquence : la séparation n'aurait réduit la portée d'aucune compromission. Qui obtient la clé de MR fait tout ce que fait la clé de déploiement. Ce qu'elle achetait était réel mais d'une autre nature, et ne justifiait pas son coût ici :

  • la révocation indépendante : griller la clé de MR n'arrête pas les déploiements. Sur ce projet le gain est faible, régénérer la clé unique est un terraform taint de quelques minutes, et le cluster est détruit la plupart du temps ;
  • la traçabilité : CloudTrail (#187) aurait nommé gitlab-ci-mr ou gitlab-ci sur chaque PutImage. C'est du forensic, pas de la prévention.

Le coût, lui, était permanent : une identité IAM de plus, quatre variables CI, et une ancre YAML à ne pas oublier de mapper dans cinq jobs.

Ce qui protège réellement l'image déployée n'est pas la clé : c'est l'immutabilité du dépôt ECR (#64). Un tag déjà poussé ne peut pas être ré-poussé. Qui vole la clé peut polluer le registre avec de nouvelles images ; il ne peut ni remplacer deployed-<sha> que sert ArgoCD, ni faire déployer les siennes, le tag servi venant de l'overlay Git et donc d'une MR mergée.

À la place, la policy ecr_push est resserrée sur deux points :

  • plus de joker de dépôt. ${var.project_name}/* couvrait tout dépôt ECR créé demain, sans décision explicite. Les deux dépôts sont désormais nommés : fastapi-eks/fastapi et fastapi-eks/frontend ;
  • eks:DescribeCluster supprimé. Permission morte : plus aucun job de .gitlab-ci.yml ne parle au cluster depuis #139, le write-back update-image-tag n'écrit que dans Git. Seul .gitlab-ci-infra.yml fait aws eks update-kubeconfig, avec AWS_INFRA_*.

3. SECRET_KEY sort des variables CI

Elle ne sert qu'à run-tests. backend/app/config.py exige SECRET_KEY au démarrage de l'application et pytest importe app.main : sans elle, le job échoue (INC-001). Elle ne signe rien d'autre que des jetons de test.

La vraie clé de production est générée par random_password dans terraform/modules/secrets, stockée dans Secrets Manager et lue par ESO. Elle n'a jamais transité par la CI.

Une clé de test n'est pas un secret. Elle est écrite en clair dans le bloc variables: de .gitlab-ci.yml, exactement comme DB_PASSWORD: postgres l'était déjà, et la variable CI est supprimée.

Conséquences

  • La clé ECR reste servie aux branches de travail. C'est assumé et non masqué : le pipeline de MR doit pouvoir pousser une image candidate. La réponse à ce risque n'est pas une deuxième clé statique, c'est de supprimer les clés statiques (voir les alternatives).
  • Aucune rotation de la clé existante dans cette issue. Le jour où elle doit tourner : terraform taint sur aws_iam_access_key.gitlab_ci.
  • AWS_DEFAULT_REGION est déjà protégée et les pipelines de MR passent quand même. C'est la preuve que kaniko et trivy dérivent la région du nom d'hôte du registre ECR. Ne pas la déprotéger « par précaution ».
  • #184 ne ferme pas AW-2 à lui seul. Le constat demande aussi #185, la réduction de la politique IAM de la CI, qui exige une trace CloudTrail réelle sur plusieurs pipelines, donc une session avec l'infra allumée. AW-2 reste ouvert, et la publication du rapport d'audit avec lui.

Alternatives écartées

Deux jeux de clés ECR. Détaillée dans la décision 2 : écartée parce que les deux policies sont nécessairement identiques, IAM ne voyant pas le tag d'image.

Zéro clé AWS dans les pipelines de MR. kaniko en --no-push --tarPath puis trivy --input sur le tarball : plus aucune clé ECR nécessaire en MR. Écartée pour le coût, environ 280 Mo d'artefacts par pipeline de MR, et parce que le scan ne porterait plus sur l'image telle qu'elle existe dans le registre.

OIDC GitLab vers un rôle AWS. GitLab présente un jeton de courte durée, AWS le vérifie et rend des identifiants temporaires : plus aucune clé statique à stocker, et la condition de confiance du rôle peut porter sur la branche ou sur le fait que le pipeline soit une merge request. C'est la seule option qui sépare réellement les deux contextes d'exécution, et elle supprime le problème au lieu de le partitionner. Écartée ici parce qu'elle change le mode d'authentification de tous les jobs d'un coup et ne se valide pas sans infra montée. À rouvrir après

185.

Références

  • Issue #184, constats AW-2 / EK-16, action de remédiation 37
  • ADR 005 (pipeline de build, tableau des variables CI), #139 (write-back ciblé sur dev), #64 (dépôt ECR immutable), #187 (CloudTrail), #185 (réduction de la policy CI)
  • terraform/modules/iam/main.tf, .gitlab-ci.yml
  • INC-001 : SECRET_KEY protégée et absente d'une feature branch
  • INC-070 : sts:GetCallerIdentity réussit même sous un Deny explicite. Ne jamais bâtir un test négatif de permissions dessus