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
mainin 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.