Skip to content

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.