Politique de ressource sur les secrets (l'administrateur qui ne peut plus lire)¶
En une phrase. Jusqu'ici, n'importe quel chemin menant à
AdministratorAccesslisait les mots de passe de production. Depuis le 2026-09-06, les sept secrets refusent par défaut et ne nomment que leurs lecteurs légitimes.
Issue #193, constat AW-10 de l'audit, action de remédiation 11.
1. Les deux endroits où AWS écrit un droit¶
C'est la seule notion à avoir en tête, tout le reste en découle.
Une policy d'identité est collée sur qui demande : un utilisateur, un rôle. Elle dit « ce rôle peut lire ce secret ».
Une politique de ressource est collée sur ce qui est demandé : ici le secret lui-même. Elle dit « ce secret n'est lisible que par ces principals ».
Un principal, c'est simplement l'identité qui appelle : un utilisateur IAM, ou un rôle assumé par un pod ou par un humain.
Pourquoi les deux, et pas seulement la première
Parce qu'une policy d'identité ne peut pas exclure un administrateur.
AdministratorAccess autorise tout, par construction. Ajouter des restrictions
ailleurs n'y change rien.
La politique de ressource est le seul niveau où un administrateur s'exclut lui-même. C'est tout l'objet de #193.
2. Avant : la seule barrière était du côté du demandeur¶
flowchart LR
subgraph P["Qui demande"]
ESO["Contrôleur ESO<br/>role eso-irsa"]
DEV["Pod de dev<br/>role app-dev-irsa"]
ADM["Humain admin<br/>user iamadmin"]
end
subgraph S["fastapi-eks/prod"]
SEC["mot de passe<br/>de PRODUCTION"]
end
ESO -->|"policy d'identité : oui"| SEC
DEV -->|"policy d'identité : non"| SEC
ADM -->|"AdministratorAccess : OUI"| SEC
style ADM fill:#ffdddd,stroke:#cc0000
style SEC fill:#fff3cd,stroke:#856404
Deux faiblesses, de gravité croissante.
L'administrateur passe toujours. Rien, nulle part, ne dit non à
AdministratorAccess. Et l'audit avait relevé que la CI pouvait devenir
administrateur en un appel (#203) : le chemin existait vraiment.
Le refus du pod de dev tient à un seul fichier. Il vient de la policy
d'identité de app-dev-irsa. Élargis-la par accident, et le secret de prod
devient lisible. Le secret lui-même n'a pas son mot à dire.
3. Après : le secret refuse d'abord, et n'ouvre qu'aux nommés¶
flowchart LR
subgraph P["Qui demande"]
ESO["Contrôleur ESO<br/>role eso-irsa"]
DEV["Pod de dev<br/>role app-dev-irsa"]
PRD["Pod de prod<br/>role app-prod-irsa"]
ADM["Humain admin<br/>user iamadmin"]
RDR["Rôle secrets-reader<br/>assumé volontairement"]
end
GATE{"Politique de ressource<br/>Deny sauf principals nommés"}
subgraph S["fastapi-eks/prod"]
SEC["mot de passe<br/>de PRODUCTION"]
end
ESO --> GATE
DEV --> GATE
PRD --> GATE
ADM --> GATE
RDR --> GATE
GATE -->|"nommé : passe"| SEC
GATE -.->|"non nommé : REFUS"| X["AccessDenied<br/>explicit deny in a<br/>resource-based policy"]
style GATE fill:#d4edda,stroke:#155724
style X fill:#ffdddd,stroke:#cc0000
Pour fastapi-eks/prod, les principals nommés sont exactement trois :
le contrôleur ESO, le rôle app-prod-irsa, et le rôle secrets-reader.
iamadmin n'y est pas. app-dev-irsa non plus.
4. Deux familles de secrets, deux listes¶
C'est le point de conception de l'extension du 2026-09-06.
| Famille | Secrets | Lecteurs nommés |
|---|---|---|
| Plateforme | app, grafana, alertmanager-slack, argocd |
ESO, secrets-reader |
| Environnement | dev, staging, prod |
ESO, secrets-reader, et le seul app-<env>-irsa correspondant |
Une liste unique aurait été plus simple à écrire. Elle aurait nommé les trois rôles
applicatifs sur les sept secrets, y compris sur grafana, où ils n'ont rien à faire.
Le cloisonnement aurait tenu quand même, par les policies d'identité. Mais une politique qui nomme des lecteurs qui ne lisent jamais se lit mal à l'audit, et devient fausse le jour où une policy d'identité s'élargit.
La règle à retenir
Une liste par famille dit ce qu'elle veut dire. C'est le critère de choix, pas la longueur du code.
5. Trois détails de rédaction qui décident de tout¶
Le refus porte sur GetSecretValue seul, jamais sur *.
Un Deny * couvrirait aussi DeleteResourcePolicy, et verrouillerait le secret
définitivement : plus personne ne pourrait retirer la politique, y compris celui
qui vient de la poser.
Les deux formes d'ARN sont listées pour chaque rôle.
Selon le contexte, AWS renvoie role/mon-role ou assumed-role/mon-role/session.
N'en nommer qu'une casse ESO sur un détail de forme.
C'est un Deny conditionné, pas un NotPrincipal.
La documentation AWS déconseille NotPrincipal. Effet de bord précieux : comme
aucun Principal n'est nommé, AWS ne vérifie pas que les rôles existent. La
politique survit donc au teardown, alors que le rôle ESO naît avec le cluster.
6. Ce que ça ne protège pas, et il faut le dire¶
iamadmin peut s'attribuer le rôle secrets-reader. La barrière ne rend pas
la lecture impossible.
Elle la rend délibérée et tracée : il faut un geste explicite, et CloudTrail l'enregistre (#187). Sur un compte à un seul humain administrateur, c'est le maximum atteignable sans séparer les identités.
flowchart LR
ADM["iamadmin"] -->|"lecture directe"| NO["REFUS"]
ADM -->|"assume secrets-reader<br/>geste volontaire"| OK["lecture autorisée"]
OK --> CT["CloudTrail<br/>AssumeRole enregistré"]
style NO fill:#ffdddd,stroke:#cc0000
style CT fill:#d1ecf1,stroke:#0c5460
C'est aussi pour ça que scripts/seed-secrets.sh sort ABANDON sous iamadmin :
ce n'est pas une panne, c'est la barrière qui fonctionne. Il faut exporter
SECRETS_READER_ROLE_ARN (voir aws-setup.md §14).
7. Vérifier — protocole et résultats du 2026-09-06¶
Le principe du projet s'applique : la preuve est la vérification d'absence, jamais
le succès de l'apply. Quatre contrôles, dans cet ordre.
Le témoin positif d'abord¶
Sans lui, un AccessDenied ne prouve rien : il pourrait venir d'une identité
cassée ou d'un problème réseau.
kubectl get externalsecret -A
Les sept ExternalSecret sont sortis SecretSynced / Ready=True, avec un
LAST SYNC à 19 secondes après un force-sync déclenché après la pose de la
politique. Un état hérité aurait affiché plusieurs minutes.
Le refus de l'administrateur¶
aws secretsmanager get-secret-value --secret-id fastapi-eks/app # sous iamadmin
AccessDeniedException
Mesuré sur app, dev, argocd et grafana. Les quatre refusés.
Les lectures légitimes passent encore¶
export SECRETS_READER_ROLE_ARN=$(cd terraform/persistent && terraform output -raw secrets_reader_role_arn)
bash scripts/seed-secrets.sh --check
identite : arn:aws:sts::...:assumed-role/fastapi-eks-secrets-reader/...
OK fastapi-eks/app (2 cles)
... les sept
L'identité affichée en première ligne est ce qui tranche, pas la présence de la variable.
Le cloisonnement entre environnements¶
Le contrôle le plus parlant. Un pod jetable, monté dans fastapi-dev avec le
vrai ServiceAccount de l'application, donc la vraie identité AWS de dev.
# son propre secret : doit passer
aws secretsmanager get-secret-value --secret-id fastapi-eks/dev
-> arn:aws:secretsmanager:eu-west-3:...:secret:fastapi-eks/dev-D9XyXa
# le secret de prod : doit être refusé
aws secretsmanager get-secret-value --secret-id fastapi-eks/prod
-> AccessDeniedException ... is not authorized to perform:
secretsmanager:GetSecretValue on resource: fastapi-eks/prod
with an explicit deny in a RESOURCE-BASED POLICY
Le mot qui compte
explicit deny in a resource-based policy. AWS dit lui-même que le refus vient
de la politique posée sur le secret, et non de la policy d'identité. C'est
la barrière de #193 qui mord, et rien d'autre.
La même identité, dans la même seconde, lit son propre secret. Seule la cible change.
8. Deux pièges rencontrés en montant ce test¶
Ils n'ont rien à voir avec les secrets, et ils coûtent du temps.
Un pod sans le bon label ne joint pas AWS du tout. Le namespace porte une
default-deny-all, et la règle d'egress ne cible que app=fastapi. Le premier pod
sonde a échoué sur Could not connect to the endpoint URL, ce qui ressemble à un
problème AWS et n'en est pas un. Contrôle réseau validé au passage.
Poser ce label est un piège à son tour. Le Service fastapi sélectionne
exactement app=fastapi : la sonde serait entrée dans ses endpoints, et
api-dev aurait renvoyé des 502 le temps du test. La parade est une
readinessProbe qui échoue toujours — le pod reste NotReady, donc hors des
endpoints, tout en portant le label que la NetworkPolicy exige.
kubectl -n fastapi-dev get endpoints fastapi # vérifié AVANT de continuer
Voir aussi¶
- Identités et secrets par environnement — le mécanisme IRSA qui donne un rôle AWS à un pod
- Bases par environnement — la barrière côté PostgreSQL, celle que #193 complète
docs/aws-setup.md§14 — poser les valeurs des secrets, et le rôle à assumer pour les lire