IAM¶
Identity and access management across GCP and AWS. Service accounts are managed in Terraform — human IAM should be audited periodically.
GCP service accounts¶
All defined in infrastructure/gcp/.
cnpg-backup-sa¶
Used by CNPG to write WAL + base backups to GCS.
GCP SA: cnpg-backup-sa@development-485000.iam.gserviceaccount.com
Workload Identity bound to:
- data/postgres-dev (K8s SA)
- data/postgres-prod (K8s SA)
Roles:
- roles/storage.objectAdmin on gs://tomoda-db-backups-development-485000
The Cluster CR's serviceAccountTemplate annotates the per-cluster K8s SA with iam.gke.io/gcp-service-account: cnpg-backup-sa@..., which is what completes the Workload Identity link.
photon-indexer¶
Used by the photon-indexer image when it (eventually, once un-suspended) runs as a CronJob inside the cluster. Today the same SA is also used directly from the build VM during ad-hoc index builds.
Workload Identity bound to: photon-indexer K8s SA in the photon-indexer namespace
Roles:
- roles/storage.objectAdmin on gs://<project-id>-photon-index
The Photon index bucket has a special allUsers reader binding — the OSM data is public, and the in-cluster Photon pod downloads it anonymously over HTTPS. This requires an organisation policy override (storage.publicAccessPrevention exception) on the project. Document this in any IAM audit so future operators don't try to "fix" it.
cloudbuild-worker-sa¶
Used by Cloud Build triggers to build images, push to Artifact Registry, and (optionally) interact with GKE.
GCP SA: cloudbuild-worker-sa@development-485000.iam.gserviceaccount.com
Roles:
- roles/logging.logWriter (write build logs)
- roles/artifactregistry.writer (push images)
- roles/container.developer (deploy to GKE, if used)
The default Cloud Build SA (287267207777@cloudbuild.gserviceaccount.com) has
roles/iam.serviceAccountUser on this SA so it can act-as during builds.
The SA does not have roles/secretmanager.secretAccessor today. If a build step ever needs a secret, grant the role narrowly to the specific secret resource — not project-wide.
argocd-image-updater-sa¶
Used by the Argo CD Image Updater to discover new image SHAs.
GCP SA: argocd-image-updater-sa@development-485000.iam.gserviceaccount.com
Workload Identity bound to: argocd/argocd-image-updater (K8s SA)
Roles:
- roles/artifactregistry.reader (metadata + tags only)
Read-only on Artifact Registry. Image Updater never pushes images.
GKE node default SA¶
SA: 287267207777-compute@developer.gserviceaccount.com
Roles:
- roles/artifactregistry.reader (pull images on behalf of pods)
This is the default compute SA — the same identity GKE nodes run as. Keeping its role list short is important since every pod without an explicit Workload Identity binding inherits its access. Today it only has artifactregistry.reader.
AWS IAM¶
All defined in infrastructure/aws/iam_uploader.tf. Two users per environment.
tomoda-uploader-{env}¶
Backend asset uploader. PutObject-only.
Path: /system/
Access key stored in: aws_secretsmanager_secret.s3_uploader_creds
Inline policy:
Effect: Allow
Action: [ s3:PutObject, s3:PutObjectAcl ]
Resource: arn:aws:s3:::tomoda-assets-{env}/*
Note what is not in the policy:
- No
s3:GetObject(commented out in Terraform — backend reads via CloudFront) - No
s3:DeleteObject - No
s3:ListBucket
Compromised access keys can only write new objects into the assets bucket; they cannot enumerate, read back, or destroy.
tomoda-eso-reader-{env}¶
The IAM user ESO uses to read the uploader's credentials from AWS Secrets Manager.
Inline policy:
Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: <ARN of the uploader's secret only>
The reader can fetch exactly one secret. It can't list other secrets, can't write, can't decrypt anything else.
Its access keys live in the aws-eso-credentials K8s Secret in the external-secrets namespace (created once during setup — see Secrets Management).
Workload Identity diagram¶
flowchart LR
subgraph GKE[GKE Cluster]
direction TB
KSAa[K8s SA<br/>data/postgres-dev]
KSAb[K8s SA<br/>data/postgres-prod]
KSAc[K8s SA<br/>argocd/argocd-image-updater]
KSAd[K8s SA<br/>photon-indexer/*]
end
subgraph GCP[GCP Project]
direction TB
GSAa[cnpg-backup-sa]
GSAb[argocd-image-updater-sa]
GSAc[photon-indexer]
end
KSAa -- WI --> GSAa
KSAb -- WI --> GSAa
KSAc -- WI --> GSAb
KSAd -- WI --> GSAc
GSAa --> GCS1[(GCS: tomoda-db-backups)]
GSAb --> AR[(Artifact Registry<br/>read-only)]
GSAc --> GCS2[(GCS: photon-index<br/>public read)]
Auditing¶
# Every binding on the GCP project
gcloud projects get-iam-policy development-485000 \
--format=json > iam-audit.json
# Just owners + editors (these should be empty for humans)
gcloud projects get-iam-policy development-485000 \
--flatten="bindings[].members" \
--filter="bindings.role:roles/owner OR bindings.role:roles/editor" \
--format="table(bindings.members,bindings.role)"
For AWS:
aws iam list-users --query 'Users[?starts_with(Path, `/system/`)].UserName'
aws iam list-attached-user-policies --user-name tomoda-uploader-prod
aws iam list-user-policies --user-name tomoda-uploader-prod
Any IAM change should land via Terraform — manual changes drift and will be reverted on the next terraform apply.