Skip to content

Access Control

Who can do what across the platform. This page is partly aspirational — current GCP IAM bindings should be audited periodically against the policy described here.

Argo CD — SSO via Dex

Argo CD's web UI (https://argo-app.tomoda.life) authenticates through Dex with Google OAuth as the upstream identity provider. Logins are restricted to the tomoda.life Google Workspace domain — anyone outside the domain is rejected at the OAuth callback.

User --> argo-app.tomoda.life
          |
          v
       oauth2-proxy (domain allow-list)
          |
          v
       Argo CD <-- Dex <-- Google OAuth

Access to the host is gated by oauth2-proxy at the ingress layer (same domain restriction), so even unauthenticated traffic never reaches Argo CD's own login page.

Argo CD RBAC

Within Argo CD, RBAC is policy-driven via the argocd-rbac-cm ConfigMap. Today there are two effective groups:

Role Permissions Members
role:admin Full control of all apps and projects A small set of named operators
role:readonly Sync status visibility, no actions Everyone else in the Workspace

Audit current RBAC:

kubectl get configmap argocd-rbac-cm -n argocd -o yaml

Cloud Build approval permissions

Dev Cloud Build triggers require manual approval — see Deploy. The IAM role that grants approval rights is roles/cloudbuild.builds.approver.

# Who can approve dev builds?
gcloud projects get-iam-policy development-485000 \
  --flatten="bindings[].members" \
  --filter="bindings.role:roles/cloudbuild.builds.approver" \
  --format="table(bindings.members)"

Restrict this to the on-call rotation — anyone with approver can push code to dev.

Prod builds trigger on Git tag push and do not require approval (the tag is the gate). Tag push permission is controlled at the GitHub repo level — only release managers should have push access to refs/tags/v*.

GCP IAM

Service accounts (used by infrastructure)

Service account Where bound Purpose
cloudbuild-worker-sa Cloud Build triggers Build images, push to Artifact Registry, deploy to GKE (container.developer)
argocd-image-updater-sa Argo CD Image Updater (Workload Identity to argocd/argocd-image-updater) Reads Artifact Registry tags
cnpg-backup-sa CNPG (Workload Identity to data/postgres-dev and data/postgres-prod) Writes WAL + base backups to GCS
photon-indexer Photon-indexer CronJob (Workload Identity) Writes built indexes to the public GCS bucket
Default compute SA GKE nodes Reads Artifact Registry (artifactregistry.reader)

These are managed in Terraform under infrastructure/gcp/. Don't add roles manually via the console — they will drift and Terraform will revert them.

Human IAM

Audit periodically (no fixed cadence enforced — recommended quarterly):

# All bindings for a human (replace email)
gcloud projects get-iam-policy development-485000 \
  --flatten="bindings[].members" \
  --filter="bindings.members:user:alice@tomoda.life" \
  --format="table(bindings.role)"

# All members of a high-privilege role
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)"

Target state:

Role Who
roles/container.admin (GKE admin) Platform team
roles/artifactregistry.writer Cloud Build SA only (humans use it transitively via build approval)
roles/secretmanager.admin Platform team
roles/secretmanager.secretAccessor Platform team + on-call (read-only)
roles/cloudbuild.builds.approver Platform team + on-call
roles/owner, roles/editor Avoid — prefer least-privilege roles

If you find roles/owner or roles/editor on human accounts, raise it for review.

AWS IAM

Two IAM users per environment, both managed in infrastructure/aws/iam_uploader.tf:

User Purpose Permissions
tomoda-uploader-{env} Backend uploads assets to S3 s3:PutObject, s3:PutObjectAcl on tomoda-assets-{env}/* only
tomoda-eso-reader-{env} ESO reads uploader creds from AWS SM secretsmanager:GetSecretValue on the uploader's secret only

Both are PutObject-only on their target bucket. No s3:GetObject, no s3:DeleteObject, no s3:List*. If the backend needs to read assets back, it goes through CloudFront, not directly via S3.

The uploader's access keys are stored in AWS Secrets Manager and pulled into K8s via External Secrets. The ESO reader's keys are stored in a one-time-created K8s Secret (aws-eso-credentials in external-secrets) — see Secrets Management.

Recommendations

  • Audit GCP IAM quarterly. Make this a calendar event for the platform lead.
  • Use Google Groups for human bindings where possible. Binding to a group means revoking access in one place (the group) when someone leaves.
  • Avoid long-lived AWS access keys for humans. Use AWS SSO with named permission sets.
  • MFA enforced at the Google Workspace level — required for everyone.
  • Branch protection on main in the tomoda app repo so unreviewed code can't reach Cloud Build.

This page is the policy. The implementation lives in infrastructure/gcp/*.tf and infrastructure/aws/*.tf — pull requests there are the source of truth for who has what.