AWS Setup Guide¶
PrĂ©requis¶
- Compte AWS actif
- IAM User avec AdministratorAccess
- MFA activé ET exigé par politique IAM sur
iamadmin(voir section 9). Sans session MFA, la clé statique ne peut plus rien faire - Dev Container configuré (voir README.md)
1. Configurer aws-vault¶
# Dans le Dev Container
export AWS_VAULT_BACKEND=file # déjà dans .bashrc
aws-vault add devops-portfolio
# AWS Access Key ID : AKIA...
# AWS Secret Access Key : xxxxxxx
# Choisir une passphrase pour chiffrer les credentials
2. Configurer ~/.aws/config¶
[profile devops-portfolio]
region = eu-west-3
mfa_serial = arn:aws:iam::TON_ACCOUNT_ID:mfa/TON_DEVICE
duration_seconds = 43200 # 12 heures
Récupérer l'ARN MFA :
AWS Console â IAM â Users â TON_USER
â Security credentials â Multi-factor authentication
â Copier l'ARN
3. Ouvrir une session AWS (workflow quotidien)¶
# Alias disponible dans le Dev Container
awslogin
# Ăquivalent complet
aws-vault exec devops-portfolio --duration=12h -- bash
# Entrer le code MFA (6 chiffres) une seule fois
# â credentials valides 12h dans le shell
Ces identifiants vivent dans ce shell, et il suffit dans presque tous les cas. Les faire sortir vers un fichier de session est le cas particulier, pas la rĂšgle â et il a ses piĂšges. Voir la section 12, qui dĂ©crit aussi quel outil vit de quel cĂŽtĂ© du pont
~/.aws.
4. VĂ©rifier la connexion¶
aws sts get-caller-identity
# {
# "UserId": "AIDA...",
# "Account": "199167114788",
# "Arn": "arn:aws:iam::199167114788:user/iamadmin"
# }
5. Workflow Terraform quotidien¶
# Matin â ouvrir la session
awslogin
# â entrer MFA une fois
# Lancer l'infrastructure
cd terraform/vpc && terraform apply -auto-approve
cd terraform/eks && terraform apply \
-var="vpc_id=VPC_ID" \
-var='private_subnet_ids=["SUBNET_1","SUBNET_2"]' \
-auto-approve
# Configurer kubectl
aws eks update-kubeconfig \
--region eu-west-3 \
--name fastapi-eks-cluster
# Vérifier le cluster
kubectl get nodes
# Soir â OBLIGATOIRE dĂ©truire pour Ă©viter les coĂ»ts
cd terraform/eks && terraform destroy \
-var="vpc_id=VPC_ID" \
-var='private_subnet_ids=["SUBNET_1","SUBNET_2"]' \
-auto-approve
cd terraform/vpc && terraform destroy -auto-approve
Ajouter la section workflow Terraform complet¶
DĂ©ploiement complet¶
cd terraform
terraform apply -auto-approve # create_nat_gateway=true dans tfvars
aws eks update-kubeconfig --region eu-west-3 --name fastapi-eks-cluster
kubectl get nodes
6. Commandes utiles¶
# Voir les sessions aws-vault actives
aws-vault list
# Supprimer une session expirée
aws-vault clear devops-portfolio
# Voir les credentials actuels
aws sts get-caller-identity
# Vérifier la région configurée
aws configure get region
7. Ressources AWS créées par Terraform¶
| Module | Ressources | Coût/heure |
|---|---|---|
| Backend | S3 bucket | ~0$ |
| VPC | VPC, subnets, IGW, NAT GW | ~0.05$/h |
| EKS | Cluster + nodes t3.medium | ~0.15$/h |
| RDS | db.t3.micro PostgreSQL | ~0.02$/h |
| ECR | Registry | ~0$ |
CoĂ»t total si allumĂ© 4h/jour : ~0.88$/jour â ~26$/mois
8. Budget Alert configurĂ©¶
AWS Console â Billing â Budgets
â devops-portfolio-budget : 50$
â Alertes : 5âŹ, 15âŹ, 20âŹ, 40⏠(forecasted)
¶
AWS Console â Billing â Budgets
â devops-portfolio-budget : 50$
â Alertes : 5âŹ, 15âŹ, 20âŹ, 40⏠(forecasted)
9. MFA obligatoire sur iamadmin (constat AW-9)¶
Depuis le 2026-08-29, une politique IAM refuse toute action de iamadmin en l'absence de MFA.
Avant, le MFA était une habitude de travail. C'est désormais un contrÎle.
Code : terraform/modules/iam/main.tf, ressource aws_iam_policy.force_mfa, appliquée par la
stack terraform/persistent.
Ce que ça change au quotidien¶
Rien, si tu ouvres ta session aws-vault le matin comme la section 3 le décrit. Dans une
session MFA, aws:MultiFactorAuthPresent vaut true, la condition du refus est fausse, et rien
n'est bloqué.
Ce qui est fermé, c'est le chemin de la clé d'accÚs statique utilisée seule, sans second
facteur. C'est exactement la menace du constat AW-9.
Deux dĂ©tails d'Ă©criture qui dĂ©cident de tout¶
BoolIfExists et non Bool. La clé de condition a trois états, pas deux : true en session
MFA, false en session sans MFA, et absente avec une clé de longue durée. Avec Bool, une
condition dont la clé est absente n'est pas évaluée : le refus ne se déclenche pas, et on croit
le contraire. C'est précisément le cas d'usage à bloquer.
Neuf actions exclues du refus, dont sts:GetSessionToken. C'est l'appel qui échange une
clé statique contre une session MFA, et il se fait forcément sans contexte MFA. Le refuser
fermerait la boucle : plus rien sans MFA, et plus de MFA obtenable. Les huit autres couvrent
l'enrĂŽlement, la lecture de son propre compte et le changement de mot de passe.
Preuve du contrĂŽle¶
Une politique posée n'est pas une politique qui mord. Les deux tests, joués le 2026-08-29 avec la
clé brute (env -u AWS_SESSION_TOKEN) :
| Test | Action | Attendu | Obtenu |
|---|---|---|---|
| Négatif | aws iam list-users |
refus | AccessDenied, la politique nommée dans le message |
| Positif | aws iam list-mfa-devices |
passe | la liste s'affiche, la porte de sortie fonctionne |
Le message de refus cite explicit deny in an identity-based policy: .../fastapi-eks-force-mfa,
donc la cause est nommée et non déduite.
Si tu perds ta session MFA¶
La clé statique garde le droit de s'enrÎler et d'appeler sts:GetSessionToken. Tu n'es donc
jamais enfermé dehors, c'est le sens des neuf exclusions.
đ Ne pas supprimer la clĂ© d'accĂšs de iamadmin¶
La question s'est posée le 2026-08-29 : une clé qui n'existe pas ne s'utilise avec aucun facteur,
donc la supprimer fermerait AW-9 plus fort qu'une politique. La réponse est non, et voici
pourquoi, pour ne pas rejouer le débat.
aws-vault stocke cette clé statique, chiffrée sur disque (section 1 de ce document). C'est
elle qu'il présente chaque matin à sts:GetSessionToken, avec le code MFA, pour obtenir la
session de 12 h :
clĂ© statique (chiffrĂ©e par aws-vault) + code MFA â sts:GetSessionToken â session 12 h
Cette clé est la racine de tout l'accÚs CLI : awslogin, Terraform, le teardown.
Une distinction Ă garder au clair, parce qu'elle est contre-intuitive :
| Suppression envisagée | Console web | CLI / API |
|---|---|---|
Le compte iamadmin |
cassée | cassée |
| Sa clé d'accÚs seulement | intacte | cassée, aws-vault en dépend |
La console n'utilise pas la clé d'accÚs, elle utilise mot de passe et MFA. Ce n'est donc pas la
console qui impose de garder la clé, c'est aws-vault.
Ce qui rendrait la suppression possible : une migration vers IAM Identity Center (ex-AWS SSO), oĂč la CLI obtient ses identifiants d'une connexion plutĂŽt que d'une clĂ© stockĂ©e. Chantier Ă part entiĂšre. Ne pas supprimer cette clĂ© avant.
10. CloudTrail, le socle de trace du compte (constat AW-1)¶
Depuis le 2026-08-29. Avant, le compte n'avait aucune trace d'appel API : rien n'aurait dit qu'une clé avait été utilisée, ni par qui, ni pour quoi.
Code : terraform/modules/cloudtrail/, appliqué par la stack terraform/persistent.
Pourquoi c'est un socle et pas une brique parmi d'autres¶
Sans trace, aucun service de détection ne sert à rien, puisqu'il n'y a rien à détecter dans.
Et concrÚtement, la réduction de la politique IAM de la CI (#185, constat AW-2) ne peut pas
se faire sans lui : elle doit s'appuyer sur la liste des appels réellement émis par ces clés.
Sans CloudTrail, elle devine. C'est pour ça que #187 passe avant #185 dans le sprint.
Ce qui est activĂ©, et ce qui ne l'est pas¶
| Réglage | Valeur | Pourquoi |
|---|---|---|
is_multi_region_trail |
true |
un appel dans une région non couverte ne laisserait rien |
include_global_service_events |
true |
sans ce drapeau, les appels IAM n'apparaissent pas, donc ceux qui comptent le plus ici |
enable_log_file_validation |
true |
empreinte signée de chaque fichier : permet de prouver qu'un journal n'a pas été altéré aprÚs coup |
| data events | non activés | facturés à l'événement. Les événements de gestion du premier trail d'un compte sont gratuits, et le constat porte sur « qui a appelé quoi » |
Le bucket de destination¶
Chiffré en AES256, tous les accÚs publics bloqués, et une politique de bucket qui n'autorise
que le service CloudTrail à écrire.
Cette politique porte une condition aws:SourceArn sur le trail. Sans elle, le service
CloudTrail d'un autre compte AWS pourrait écrire dans ce bucket : le principal est un
service, il n'identifie pas à lui seul le compte appelant. C'est le motif dit du « député
confus ».
Une expiration Ă 90 jours, posĂ©e dĂšs l'origine¶
Le mĂȘme audit corrige, Ă l'action 15, un groupe de logs Ă rĂ©tention illimitĂ©e qui facture Ă vie. CrĂ©er ce bucket sans terme reproduirait Ă neuf le dĂ©faut qu'on est en train de corriger.
La durée se rÚgle par log_retention_days.
Preuve du contrĂŽle, 2026-08-29¶
Un trail actif n'est pas un trail qui trace. La preuve demandée par l'audit est de retrouver un appel identifié, choisi à l'avance, dans les fichiers du bucket.
Appel témoin : aws iam get-user --user-name preuve-187-165742, sur un utilisateur qui n'existe
pas. Il ne crée rien, et son nom unique le rend retrouvable parmi des milliers de lignes.
Retrouvé dans
AWSLogs/<compte>/CloudTrail/us-east-1/2026/08/29/..._20260829T1700Z_....json.gz :
{
"eventTime": "2026-08-29T16:57:45Z",
"eventName": "GetUser",
"eventSource": "iam.amazonaws.com",
"awsRegion": "us-east-1",
"userIdentity": "arn:aws:iam::<compte>:user/iamadmin",
"sourceIPAddress": "<ip du poste>",
"requestParameters": { "userName": "preuve-187-165742" },
"errorCode": "NoSuchEntityException"
}
L'appel a été émis depuis eu-west-3 et il est enregistré en us-east-1. C'est la
démonstration concrÚte de la section précédente : IAM est un service global, ses événements
partent vers us-east-1, et un trail mono-région n'aurait rien vu de cet appel.
Deux détails qui coûtent du temps si on ne les connaßt pas :
- Un trail neuf ne livre pas tout de suite. Un premier témoin émis 4 minutes aprÚs la création du trail n'est jamais apparu, et n'apparaßtra pas. Compter jusqu'à 15 minutes de démarrage. Une fois chaud, la livraison a pris 6 minutes.
- Ne jamais prouver ceci avec
aws cloudtrail lookup-events. AWS conserve un historique d'Ă©vĂ©nements de 90 jours par dĂ©faut, sans aucun trail : cette commande rĂ©pondrait mĂȘme si le trail Ă©tait cassĂ©. Elle ne discrimine pas. La preuve vient du bucket, ou elle ne vient pas.
Ce que la trace a montrĂ© dĂšs le premier jour¶
En moins de quatre heures, la trace a répondu à une question qu'on ne pouvait pas poser la veille :
une activité réguliÚre, toutes les 20 à 40 minutes, dans des régions inutilisées
(eu-north-1, us-east-1), sur des services jamais employés ici (Batch, Glue, Ground Station,
Image Builder, Redshift).
Identifiée sans ambiguïté grùce au champ userIdentity : AWS Resource Explorer
(AWSServiceRoleForResourceExplorer), le service qui indexe les ressources du compte pour la
recherche. Légitime, rien à faire.
Le point n'est pas la découverte, c'est qu'elle était invisible jusqu'à ce jour. C'est
exactement ce que décrit le constat AW-1.
L'arbitrage sur le chiffrement, pour qu'il puisse ĂȘtre rĂ©examinĂ©¶
tfsec demande une clĂ© KMS dĂ©diĂ©e sur le trail. ĂcartĂ©, avec le motif Ă©crit dans le code :
- les journaux sont chiffrés au repos, le bucket porte
SSE-S3 - ce que KMS ajouterait, c'est une seconde barriÚre d'autorisation, utile contre un attaquant qui aurait déjà la lecture S3
- coût d'environ 1 $/mois par clé, sur un budget projet de 50 $
- cohĂ©rence : le bucket de state Terraform, qui porte des donnĂ©es plus sensibles que ces journaux, a tranchĂ© le mĂȘme arbitrage dans le mĂȘme sens
Si le compte devient multi-utilisateur, cet arbitrage se rouvre.
11. La rĂ©tention du journal du control plane EKS (constat AW-6)¶
Action 15 de l'audit, seconde moitié de l'issue #187.
Le control plane est la partie du cluster gérée par AWS : le serveur d'API, l'authentifieur,
le contrÎleur. Le module EKS lui demande d'écrire trois familles de journaux (api, audit,
authenticator) dans CloudWatch Logs, via enabled_cluster_log_types.
Le constat : un dĂ©faut, pas une dĂ©cision¶
Ces journaux atterrissent dans un log group (le conteneur qui regroupe des journaux de mĂȘme
origine) nommé /aws/eks/fastapi-eks-cluster/cluster. EKS le crée tout seul à la premiÚre
écriture, et un groupe créé ainsi naßt avec une rétention à None.
None veut dire conservé pour toujours. Personne n'a choisi cette durée, c'est le défaut.
Deux conséquences se cumulent :
- le groupe survit à la destruction du cluster, donc il n'est jamais nettoyé par le teardown du soir ;
- il facture Ă vie, sur un projet dont le cluster n'existe que quelques heures par semaine.
Pourquoi la ressource vit dans la stack persistent¶
CloudTrail est dans persistent parce qu'une trace qui disparaĂźt au teardown ne trace rien. Ce
groupe-ci y est pour la raison inverse : il survit déjà , et c'est justement le problÚme.
Le déclarer dans ephemeral le ferait détruire à chaque terraform destroy du soir. On
effacerait le journal du control plane Ă chaque fin de session â or cette trace qui survit Ă la
destruction du cluster est précisément le point fort mesuré par l'audit face au homelab.
La stack persistent n'est jamais dĂ©truite. C'est le seul endroit oĂč la ressource est cohĂ©rente.
Ce que Terraform ajoute par rapport Ă la commande¶
La commande d'urgence existe et fait le travail :
aws logs put-retention-policy \
--log-group-name /aws/eks/fastapi-eks-cluster/cluster \
--retention-in-days 90
Mais elle est jouée une fois, par une personne, et rien ne la rejoue. La case de l'issue #187 demande explicitement les deux : la rétention visible cÎté AWS, et portée par le code.
Le paramÚtre se rÚgle par eks_log_retention_days, à 90 jours par défaut. Quatre-vingt-dix jours
couvrent un trimestre d'investigation a posteriori, ce qui est la durée déjà retenue pour
l'expiration du bucket CloudTrail â mĂȘme question, mĂȘme rĂ©ponse.
L'adoption du groupe existant, Ă jouer une fois¶
Le groupe existe déjà cÎté AWS. Un terraform apply voudrait donc le créer et sortirait
ResourceAlreadyExistsException. Il faut l'adopter dans l'état Terraform avant :
cd terraform/persistent
terraform import aws_cloudwatch_log_group.eks_cluster /aws/eks/fastapi-eks-cluster/cluster
terraform apply
L'état de persistent n'étant jamais détruit, cet import ne se rejoue pas.
Un bloc import {} aurait rendu ce geste dĂ©claratif, donc impossible Ă oublier. ĂcartĂ© aprĂšs
mesure : le parser HCL de tfsec ne connaĂźt pas ce type de bloc et sort Unsupported block
type, ce qui fait échouer le scan de tout terraform/. Le job tfsec-scan étant
allow_failure: true, la couverture de sécurité de l'infrastructure entiÚre aurait disparu sans
faire rougir la moindre MR. Un garde-fou déclaratif qui aveugle un autre garde-fou n'est pas un
gain.
Preuve du contrĂŽle, 2026-09-01¶
Le contrÎle n'est pas « la ressource est dans le code », c'est la valeur lue cÎté AWS :
aws logs describe-log-groups \
--log-group-name-prefix /aws/eks/fastapi-eks-cluster \
--query 'logGroups[].[logGroupName,retentionInDays]' --output table
Avant, puis aprĂšs :
| /aws/eks/fastapi-eks-cluster/cluster | None |
| /aws/eks/fastapi-eks-cluster/cluster | 90 |
terraform apply : 0 added, 1 changed, 0 destroyed, en updated in-place. Le groupe n'a pas
Ă©tĂ© recréé, les journaux existants sont intacts. Un must be replaced aurait dĂ» faire arrĂȘter.
L'ordre des deux gestes se lit dans le plan. put-retention-policy a été joué avant
terraform import, donc Terraform a importé une ressource déjà conforme : le plan ne portait que
les trois tags, la rétention restant dans les « unchanged attributes ». Dans l'autre ordre,
l'apply aurait eu Ă corriger la rĂ©tention lui-mĂȘme.
L'apply a été joué en -target=aws_cloudwatch_log_group.eks_cluster. La stack persistent
porte aussi module.secrets : le -target le sort du graphe, donc aucune variable ne pouvait
atteindre un secret. -target est déconseillé en usage courant, c'est ici la propriété cherchée.
Pourquoi cette action attendait le 01/09¶
Le contenu sans terme de ce groupe est la matiÚre du scénario purple team PT-7 du rapport d'audit. On ne raccourcit pas la conservation d'une preuve avant d'avoir rendu le rapport qui s'appuie dessus.
Le rapport étant livré, la contrainte tombe. Poser 90 jours ne supprime d'ailleurs rien aujourd'hui : cela borne ce qui sera conservé à partir de maintenant.
12. Le pont ~/.aws, et quand un fichier de session est vraiment nĂ©cessaire¶
Cette section existe parce que la procédure était connue et nulle part écrite. Elle a coûté une soirée le 2026-08-31, sur trois piÚges qui se ressemblent tous.
L'outillage est rĂ©parti, aucun environnement n'a tout¶
WSL Dev Container
âââ âââââââââââââ
glab â docker â aws-vault â terraform â aws â
aws â terraform â (pas de glab, pas de docker)
dépÎt : ~/Project-DEVOPS-FASTAPI-2026 dépÎt : /workspace
â â
âââââââââââ ~/.aws âââââââââââââ
monté sur /home/devops/.aws
aws-vault (le coffre qui garde la clé AWS chiffrée sur disque) n'existe que dans le Dev
Container. glab n'existe que dans WSL. Le dossier ~/.aws est monté des deux cÎtés : c'est
le seul point de passage entre les deux.
Les deux terraform ne sont pas non plus la mĂȘme version : 1.14.3 en WSL, 1.15 dans la CI
et dans le Dev Container. Un state écrit par 1.15 ne se lit pas avec 1.14. Depuis WSL, passer
par l'image de la CI plutĂŽt que par le binaire local :
docker run --rm -v "$PWD":/src -w /src/terraform/persistent \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
-e AWS_REGION -e AWS_DEFAULT_REGION \
--entrypoint terraform hashicorp/terraform:1.15@sha256:adae4566... plan -input=false
Les -e NOM sans valeur reprennent celle du shell : aucun secret ne passe par la ligne de
commande.
Conséquence directe, mesurée le 2026-08-31 : un export TF_VAR_x="$(glab variable get ...)" joué
dans le Dev Container échoue en silence. glab n'y est pas, la substitution rend une chaßne
vide, et Terraform accepte TF_VAR_x="" comme une valeur fournie. Les secrets sont alors calculés
avec du vide, sans une seule erreur à l'écran.
Le cas normal : pas de fichier du tout¶
awslogin ouvre un nouveau shell avec les identifiants déjà dedans, valables 12 h :
awslogin # = aws-vault exec devops-portfolio --duration=12h -- bash
Tout ce qui est lancé depuis ce shell hérite des identifiants. Terraform et aws compris.
C'est le cas par dĂ©faut, et il ne demande aucun fichier de session. Si le travail tient dans ce shell, s'arrĂȘter ici.
Le cas particulier : faire sortir les identifiants de ce shell¶
~/.aws/session-aws.env sert Ă une seule chose : rendre les identifiants utilisables ailleurs
que dans le shell awslogin â depuis WSL, ou par un agent qui n'ouvre pas ce shell.
Trois piÚges, tous rencontrés :
| PiĂšge | Ce qu'on voit | La rĂšgle |
|---|---|---|
Pas de préfixe export |
Terraform ne voit pas la variable | aws-vault --format=env produit CLE=valeur, que source définit dans le shell sans la transmettre aux processus enfants |
| Fichier tronqué à 0 octet | le fichier existe et est vide | aws-vault export refuse de tourner à l'intérieur d'une session aws-vault, alors que le > a déjà vidé le fichier |
| Jetons périmés | ExpiredToken sur une commande qui marchait hier |
les jetons STS durent 36 h au maximum |
| Nom de profil trompeur | profile iamadmin: credentials missing |
le profil iamadmin ne porte aucune clĂ©. Celui qui rĂ©sout vers user/iamadmin s'appelle devops-portfolio â aws-vault list le montre, colonne Credentials |
| Déjà dans un sous-shell | in an existing aws-vault subshell |
les identifiants sont déjà dans l'environnement : les recopier. Le unset AWS_VAULT que le message propose ouvre une session STS de plus, avec MFA à ressaisir, pour ce qu'on a déjà |
export nu en tĂȘte de fichier |
chaque source crache un declare -x de tout l'environnement |
aws-vault sort une ligne vide, qu'un sed 's/^/export /' transforme en export sans argument. Filtrer sur ^[A-Z_]+= avant de préfixer |
Les deux premiers ont la mĂȘme parade : Ă©crire dans un fichier temporaire, et ne le dĂ©placer Ă sa
place qu'une fois la collecte rĂ©ussie. Un > en tĂȘte de commande dĂ©truit l'ancien contenu avant
de savoir si le nouveau existera.
Le script qui tranche entre les deux cas¶
~/.aws/gen-session-aws.sh â hors dĂ©pĂŽt, il vit dans ~/.aws, donc visible des deux cĂŽtĂ©s du
pont. Ăcrit le 2026-09-01, aprĂšs avoir repayĂ© les mĂȘmes piĂšges une session de plus.
bash ~/.aws/gen-session-aws.sh # profil par défaut : iamadmin
bash ~/.aws/gen-session-aws.sh devops-portfolio
Il choisit sa branche tout seul, sur $AWS_VAULT :
$AWS_VAULT |
Situation | Ce qu'il fait |
|---|---|---|
| non vide | on est dans un shell awslogin |
recopie les variables AWS_* de l'environnement, aucun appel Ă aws-vault, pas de MFA |
| vide | shell normal | aws-vault export --format=env <profil> |
Puis, dans les deux cas : collecte avant toute redirection (le > ne peut pas vider le
fichier sur un échec), abandon si la collecte est vide, filtrage des lignes qui ne sont pas des
affectations, préfixe export, chmod 600.
Il termine en affichant l'identité réellement obtenue et sa date d'expiration :
/home/devops/.aws/session-aws.env ecrit : 6 variables
identite : arn:aws:iam::199167114788:user/iamadmin
expire : 2026-09-02T00:03:16Z
â ïž C'est la ligne identite qui fait foi, jamais le profil demandĂ©. Trois profils existent
pour deux identités utiles, et l'un d'eux (iamadmin) est vide et détourne du bon. Un nom de
profil n'est pas une identité : seul sts get-caller-identity tranche.
Les variables Terraform, et celle qui n'est pas dans GitLab¶
Mis Ă jour par l'ADR 033 (#198) :
persistentne rĂ©clame plus qu'une variable,TF_VAR_cost_alert_email. Les quatre ci-dessous restent utiles âTF_VAR_db_passwordpour la stackephemeral(RDS), les trois autres pourscripts/seed-secrets.shâ mais Terraform ne les lit plus surpersistent. Voir la section 14.
La stack persistent réclamait cinq variables sans valeur par défaut. Quatre vivent dans les
variables CI du projet et se lisent avec glab, donc depuis WSL :
TF_VAR_db_password TF_VAR_slack_webhook_url
TF_VAR_argocd_admin_bcrypt TF_VAR_argocd_gitlab_client_secret
La cinquiĂšme, TF_VAR_cost_alert_email, n'y est pas â le dĂ©pĂŽt est public, et c'est Ă©crit dans
terraform/persistent/variables.tf. Elle se saisit Ă la main.
Les cinq sont rĂ©clamĂ©es mĂȘme avec -target : Terraform valide la configuration entiĂšre avant
d'appliquer le filtre.
Le garde-fou Ă poser avant de poser des valeurs¶
â ïž Le motif de ce garde-fou a changĂ© avec l'ADR 033. Avant, une variable vide passĂ©e Ă
terraform apply sur persistent écrasait un secret. Ce n'est plus possible : module.secrets
ne reçoit plus aucune valeur. Le risque a déménagé vers scripts/seed-secrets.sh, qui porte
le mĂȘme contrĂŽle en interne et refuse d'Ă©crire si une des quatre est vide.
Le garde-fou reste utile Ă la main, avant de lancer le script :
for v in TF_VAR_db_password TF_VAR_slack_webhook_url TF_VAR_argocd_admin_bcrypt \
TF_VAR_argocd_gitlab_client_secret TF_VAR_cost_alert_email; do
[ -n "${!v:-}" ] || echo "VIDE : $v"
done
â ïž Ce contrĂŽle vĂ©rifie que la variable n'est pas vide, pas qu'elle est juste. Une valeur bidon passerait. Ne jamais en mettre une pour « faire passer » la commande.
Deux rĂ©flexes qui vont avec¶
terraform plan -input=falseplutĂŽt queterraform plan. Terraform prend le verrou d'Ă©tat avant de demander une variable manquante : un plan interrompu Ă l'invite laisse le verrou posĂ©. Avec ce drapeau, il Ă©choue au lieu d'attendre. Sortie parforce-unlock, aprĂšs avoir vĂ©rifiĂ© qu'aucun processus ne tourne âterraform-ls, le serveur de langage de l'Ă©diteur, ne compte pas.kubectlest un autre sujet et un autre fichier. L'identitĂ© du cluster estyouss_admin, pasiamadmin: voirdocs/validation-runbook.mdsection 0.
13. La politique de mot de passe du compte (constat AW-7)¶
Action 9 de l'audit. Ă lire avec la section 9 : mĂȘme surface, mĂȘme utilisateur.
Ce que c'est¶
La politique de mot de passe IAM est un réglage unique et global. Une seule peut exister par compte AWS, et elle s'applique à tout mot de passe de console créé dessus, quel que soit l'utilisateur.
Elle impose la longueur, la présence de majuscules, minuscules, chiffres, symboles, l'expiration et la non-réutilisation.
Le constat, et la question que le contrĂŽle ne pose pas¶
Sept contrÎles CIS en échec, un par rÚgle : le réglage n'existait pas du tout, pour personne.
Reste la question que l'outil ne pose jamais â un rĂ©glage absent, qui protĂ©gerait-il s'il
existait ? D'oĂč la vĂ©rification manquante, aws iam get-login-profile, la commande qui dit si un
utilisateur IAM possÚde un mot de passe de console. Rejouée sur les cinq utilisateurs du compte
le 2026-08-19 :
| Utilisateurs du compte | Avec mot de passe console |
|---|---|
| 5 | 1 |
Un seul, iamadmin. Les quatre autres sont des comptes machine : ils s'authentifient par clé
d'accĂšs, sans mot de passe de console, donc sans rien qu'une telle politique puisse encadrer.
Ce qui change la lecture du constat : il ne concerne qu'un compte, et c'est celui qui porte
AdministratorAccess. Un constat étroit et net vaut mieux qu'un constat large et creux.
Ce qui est posĂ©¶
| RĂšgle | Valeur |
|---|---|
| Longueur minimale | 14 |
| Majuscule, minuscule, chiffre, symbole | exigés |
| Réutilisation | interdite sur les 24 derniers |
| L'utilisateur peut changer son mot de passe | oui |
| Expiration | 90 jours â voir l'arbitrage ci-dessous |
L'arbitrage sur l'expiration, pour qu'il puisse ĂȘtre rĂ©examinĂ©¶
C'est le seul des sept réglages qui se discute.
Le NIST SP 800-63B déconseille la rotation périodique depuis 2017. Elle pousse aux mots de
passe dĂ©rivĂ©s â Motdepasse1!, puis Motdepasse2! â et dĂ©grade ce qu'elle prĂ©tend renforcer.
CIS AWS Foundations v3.0.0 a retiré ce contrÎle pour cette raison.
Elle est retenue quand mĂȘme, sur deux motifs propres Ă ce compte :
- le référentiel outillé du projet compte sept contrÎles, et un huitiÚme rouge sans explication coûterait plus à relire que la rÚgle ne coûte à subir ;
- le seul compte concerné est protégé par MFA (section 9), donc le mot de passe seul n'ouvre rien.
Si le projet s'aligne explicitement sur CIS v3.0.0, cette ligne se retire du Terraform.
Preuve du contrĂŽle, 2026-09-01¶
Le contrĂŽle n'est pas le plan Terraform, c'est la valeur lue sur AWS :
aws iam get-account-password-policy
{
"PasswordPolicy": {
"MinimumPasswordLength": 14,
"RequireSymbols": true,
"RequireNumbers": true,
"RequireUppercaseCharacters": true,
"RequireLowercaseCharacters": true,
"AllowUsersToChangePassword": true,
"ExpirePasswords": true,
"MaxPasswordAge": 90,
"PasswordReusePrevention": 24
}
}
terraform apply -target=module.iam.aws_iam_account_password_policy.main :
1 to add, 0 to change, 0 to destroy.
hard_expiry, le rĂ©glage qui pouvait fermer le compte¶
HardExpiry n'apparaĂźt pas dans la sortie ci-dessus : AWS l'omet quand il vaut false.
Ce champ décide de ce qui arrive quand un mot de passe expire :
| Valeur | Effet Ă l'expiration |
|---|---|
false |
l'utilisateur se connecte encore, et change son mot de passe lui-mĂȘme |
true |
il est verrouillé dehors, seul un administrateur peut le débloquer |
Sur ce compte, true serait un piĂšge : iamadmin est le seul compte humain, et c'est lui
l'administrateur. Personne ne pourrait le débloquer. Combiné aux 90 jours ci-dessus, la porte du
compte AWS se refermerait sans clef.
false est déjà le comportement par défaut. Il est néanmoins écrit explicitement dans le
Terraform : on ne fait pas reposer l'accÚs au compte sur un défaut implicite, que le prochain
lecteur ne verra pas. La ligne coûte moins cher que la panne.
14. Les valeurs des secrets, posĂ©es hors Terraform (ADR 033)¶
Depuis l'ADR 033 (#198), Terraform déclare les sept secrets Secrets Manager mais n'en
connaßt plus la valeur. random_password et aws_secretsmanager_secret_version ont été retirés
du module, parce qu'ils stockaient la valeur en clair dans le state.
Le partage tient en une phrase : Terraform crée le contenant, scripts/seed-secrets.sh pose le
contenu.
Ce que ça change au quotidien¶
| Situation | Avant | AprĂšs |
|---|---|---|
apply from-scratch de persistent |
les valeurs naissent seules | les sept secrets naissent vides |
| SymptĂŽme si on oublie de les poser | â | ESO ne rĂ©sout pas, pods en CreateContainerConfigError |
Variables réclamées par persistent |
cinq | une (TF_VAR_cost_alert_email) |
Le symptÎme d'oubli ne dit pas ce qui manque. C'est pour ça que le contrÎle ci-dessous fait partie du montage, et pas d'un aide-mémoire.
Poser les valeurs¶
# 4 valeurs externes attendues dans l'environnement (gen-tfvars.sh les exporte déjà )
bash scripts/seed-secrets.sh --check # n'écrit rien, dresse l'état des sept secrets
bash scripts/seed-secrets.sh --seed # pose les valeurs MANQUANTES, puis relit
â ïž Depuis le 2026-09-06, les SEPT secrets portent une politique de ressource qui refuse la lecture Ă
iamadmin(#193, Ă©tendu depuisgrafanaseul). Le script sort alorsABANDONdĂšs le premier secret rencontrĂ© et s'arrĂȘte lĂ , sans vĂ©rifier les six suivants. Ce n'est pas une panne, c'est la barriĂšre qui fonctionne.ConsĂ©quence pratique : sous
iamadmin,--checkne rend plus aucune information utile. Le rÎle n'est plus un contournement occasionnel, c'est le mode normal d'usage du script.Il faut assumer le rÎle prévu pour ça :
export SECRETS_READER_ROLE_ARN=$(cd terraform/persistent && terraform output -raw secrets_reader_role_arn) bash scripts/seed-secrets.sh --checkLe script affiche l'identité effective en premiÚre ligne : elle doit se lire
assumed-role/fastapi-eks-secrets-reader/..., et nonuser/iamadmin. C'est elle qui tranche, pas la présence de la variable.
iamadminpeut s'attribuer ce rĂŽle : la barriĂšre ne rend pas la lecture impossible, elle la rend dĂ©libĂ©rĂ©e et tracĂ©e dans CloudTrail.â ïž Le tĂ©moin du 2026-09-04 n'existe plus. Il reposait sur
fastapi-eks/app, alors non protĂ©gĂ© et lisible sans le rĂŽle. Un tĂ©moin est un cas dont on connaĂźt dĂ©jĂ la rĂ©ponse : il ne teste pas la barriĂšre, il teste le test. Sans lui, un refus peut aussi bien venir d'une clĂ© expirĂ©e.Ce n'est pas une rĂ©gression, c'est un tĂ©moin gratuit qui disparaĂźt. Le remplaçant est plus juste : comparer avec et sans le rĂŽle sur le MĂME secret, une seule variable changeant, l'identitĂ©. L'ancien changeait de secret et supposait qu'un secret reste non protĂ©gĂ©, ce qui Ă©tait provisoire par nature.
aws secretsmanager get-secret-value --secret-id fastapi-eks/prod # sans le rÎle -> AccessDenied aws secretsmanager get-secret-value --secret-id fastapi-eks/prod # avec le rÎle -> passeSeul changement réel au quotidien : il faut désormais y penser. Avant, un secret non protégé traßnait et rendait le service tout seul.
Mesuré le 2026-09-06, les trois sens :
iamadminrefusé surapp,dev,argocdetgrafana; les sept lisibles viasecrets-reader; les septExternalSecretresynchronisés par ESO aprÚs la pose. Détail et schémas : Politique de ressource sur les secrets.
--seed ne réécrit jamais une valeur déjà posée. La réécriture demande --force, et c'est
une rotation, pas une réparation : changer fastapi-eks/{dev,staging,prod} change le mot de
passe attendu des users PostgreSQL, et le Job de bootstrap DB doit ĂȘtre rejouĂ© derriĂšre.
Les huit valeurs générées le sont par openssl rand -hex : alphanumérique comme le
special = false d'avant, et 256 bits pour les SECRET_KEY, la taille exacte d'une clé HS256.
La bascule elle-mĂȘme, Ă ne jouer qu'une fois¶
â ïž L'ordre est contraint, et il n'y a pas d'apply au milieu. Les sept secrets portent
recovery_window_in_days = 0 : une suppression est immédiate et sans retour.
# 1. Sortir les 15 ressources porteuses de valeur, en UNE commande
cd terraform/persistent
terraform state rm \
'module.secrets.random_password.secret_key' \
'module.secrets.random_password.grafana_admin' \
'module.secrets.random_password.db_env["dev"]' \
'module.secrets.random_password.db_env["staging"]' \
'module.secrets.random_password.db_env["prod"]' \
'module.secrets.random_password.secret_key_env["dev"]' \
'module.secrets.random_password.secret_key_env["staging"]' \
'module.secrets.random_password.secret_key_env["prod"]' \
'module.secrets.aws_secretsmanager_secret_version.app' \
'module.secrets.aws_secretsmanager_secret_version.grafana' \
'module.secrets.aws_secretsmanager_secret_version.alertmanager_slack' \
'module.secrets.aws_secretsmanager_secret_version.argocd' \
'module.secrets.aws_secretsmanager_secret_version.env["dev"]' \
'module.secrets.aws_secretsmanager_secret_version.env["staging"]' \
'module.secrets.aws_secretsmanager_secret_version.env["prod"]'
# 2. SEULEMENT ensuite, prendre le code qui retire les blocs (la MR de #198)
# 3. Le plan doit ĂȘtre vide
terraform plan -input=false # -> No changes
Jouer 2 avant 1, ou glisser un apply entre les deux, détruit les sept versions chez AWS :
Terraform les a encore dans son state et ne les voit plus dans la configuration.
La preuve, et ce qu'elle ne couvre pas¶
Un plan vide ne prouve pas que le state est propre. La vérification se fait sur le fichier :
aws s3 cp s3://yk-devops-terraform-state/fastapi-eks/persistent/terraform.tfstate - \
| jq -r '.resources[] | select(.type=="random_password" or .type=="aws_secretsmanager_secret_version") | .type'
# -> aucune ligne
â ïž Le state reste nĂ©anmoins porteur de secrets. module.iam crĂ©e trois aws_iam_access_key
(gitlab_ci, gitlab_ci_infra, cockpit_panel) dont l'attribut secret est stocké en clair par
construction du provider. Le nettoyage ci-dessus vaut au périmÚtre des sept secrets
applicatifs, pas au-delĂ . Les anciennes versions du bucket portent par ailleurs les valeurs
d'avant : c'est #199.
â ïž RĂšgles d'or¶
1. Jamais laisser EKS/NAT allumés la nuit
2. Jamais committer de credentials AWS
3. Toujours utiliser aws-vault (jamais aws configure)
4. Vérifier aws-vault list avant de partir le soir
5. Ne jamais tester un refus IAM avec `sts:GetCallerIdentity` : il rĂ©ussit mĂȘme quand une politique le refuse explicitement, donc il ne discrimine rien
15. Les clĂ©s d'accĂšs de la CI, créées hors Terraform (ADR 034)¶
Depuis l'ADR 034 (#202), Terraform déclare les trois identités CI mais n'en fabrique
plus les clés. aws_iam_access_key et les six outputs correspondants ont été retirés du module,
parce que le provider stocke l'attribut secret en clair dans le state â il ne peut pas
faire autrement, l'API IAM ne rend le secret qu'une fois, à la création.
Le partage tient en une phrase, symétrique de celle de la section 14 : Terraform déclare l'identité, il n'en connaßt jamais les identifiants.
Les trois identitĂ©s concernĂ©es¶
| Utilisateur IAM | Sert Ă | Variables CI |
|---|---|---|
fastapi-eks-gitlab-ci |
push ECR | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
fastapi-eks-gitlab-ci-infra |
terraform apply de l'infra |
AWS_INFRA_ACCESS_KEY_ID, AWS_INFRA_SECRET_ACCESS_KEY |
fastapi-eks-cockpit-panel |
lecture d'état pour le cockpit | hors GitLab |
Ce que ça change au quotidien¶
| Situation | Avant | AprĂšs |
|---|---|---|
apply from-scratch de persistent |
les trois clés naissent seules | les utilisateurs naissent sans clé |
| SymptĂŽme si on oublie de les crĂ©er | â | premier job AWS en NoCredentials |
| Rotation d'une clé | terraform taint + apply |
aws iam create-access-key, hors Terraform |
Le symptĂŽme d'oubli est au moins explicite, contrairement Ă celui de la section 14 : un
NoCredentials nomme le problÚme, un ESO qui ne résout pas ne dit pas ce qui manque.
CrĂ©er une clĂ© (montage from-scratch, ou rotation)¶
â ïž L'API IAM ne rend le secret qu'une seule fois. Il n'y a pas de « repose » comme pour un secret Secrets Manager : perdre la valeur entre sa crĂ©ation et sa pose dans GitLab oblige Ă supprimer la clĂ© et Ă recommencer.
# 1. Créer la clé et capturer la valeur DANS LA MEME COMMANDE
aws iam create-access-key --user-name fastapi-eks-gitlab-ci-infra \
--query 'AccessKey.[AccessKeyId,SecretAccessKey]' --output text
# 2. Poser les deux valeurs dans les variables CI (Settings > CI/CD > Variables)
# AWS_INFRA_* sont Masked ET Protected (#184) : elles n'existent que sur les
# branches protégées. Une MR depuis une branche de travail sortira NoCredentials,
# c'est structurel et ce n'est pas une panne.
# 3. Prouver la nouvelle clé par un pipeline VERT avant de toucher à l'ancienne
# 4. Seulement alors, supprimer l'ancienne
aws iam delete-access-key --user-name fastapi-eks-gitlab-ci-infra --access-key-id AKIA_ANCIENNE
L'ordre des étapes 3 et 4 n'est pas négociable. Un utilisateur IAM accepte deux clés simultanées : c'est exactement ce qui permet de valider la nouvelle avant de retirer l'ancienne. Supprimer d'abord, c'est se retrouver sans CI si la nouvelle valeur a été mal collée.
La rotation vient APRĂS la parade, jamais avant¶
Tant que Terraform fabriquait la valeur, la tourner la faisait revenir dans le state au plan
suivant : on tuait des copies anciennes pour en créer une nouvelle, vivante. C'est la raison
pour laquelle la rotation de #202 se joue une fois aws_iam_access_key sorti du code.
La preuve, et ce qu'elle ne couvre pas¶
Le critÚre de succÚs est la vérification d'absence sur le fichier de state téléchargé,
section outputs comprise â jamais la sortie du plan, jamais le succĂšs de l'apply.
terraform state pull > /tmp/state.json
# 0 ressource aws_iam_access_key, 0 output *_secret_access_key, et la valeur absente du fichier
Ce qui n'est pas couvert : les anciennes versions du state portent toujours les valeurs d'avant la parade. Le bucket est versionnĂ©, et c'est le sujet de #199. La parade arrĂȘte le flux, elle n'efface pas le stock.