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'utilisateurfastapi-eks-gitlab-ci-infraet la policyterraform_ci:ec2:*,eks:*,rds:*,ecr:*, plusiam:CreateUser,iam:CreateAccessKeyetiam: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_IDportefastapi-eks-gitlab-ciet la policyecr_push: push ECR pluseks:DescribeCluster. Portée déjà étroite.TF_VAR_db_passwordest le mot de passe RDS, lu par Terraform seul.SECRET_KEYsigne 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 taintde quelques minutes, et le cluster est détruit la plupart du temps ; - la traçabilité : CloudTrail (#187) aurait nommé
gitlab-ci-mrougitlab-cisur chaquePutImage. 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/fastapietfastapi-eks/frontend; eks:DescribeClustersupprimé. Permission morte : plus aucun job de.gitlab-ci.ymlne parle au cluster depuis #139, le write-backupdate-image-tagn'écrit que dans Git. Seul.gitlab-ci-infra.ymlfaitaws eks update-kubeconfig, avecAWS_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 taintsuraws_iam_access_key.gitlab_ci. AWS_DEFAULT_REGIONest 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-2reste 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_KEYprotégée et absente d'une feature branch - INC-070 :
sts:GetCallerIdentityréussit même sous unDenyexplicite. Ne jamais bâtir un test négatif de permissions dessus