Skip to main content

Service Accounts

All GCP service accounts follow least privilege — each account has only the roles required for its specific function. Never add roles "just in case." If a service needs a new permission, add the narrowest role that satisfies the requirement and document it here.

Design Principles

  1. One account per function — don't share accounts across services
  2. Minimal roles — use the most restrictive role that works (e.g., storage.objectViewer not storage.admin)
  3. No owner or editor on runtime accounts — only Terraform gets broad permissions
  4. Runtime accounts cannot deploy — deploy accounts cannot access secrets
  5. Document every role — if it's not in this table, it shouldn't exist

Production (zyntem-prod)

Service AccountPurposeRolesWhy
fiscalapi-run-prodCore API Cloud Run runtimecloudsql.clientConnect to Cloud SQL
secretmanager.secretAccessorRead DB URL, Stripe keys, etc.
storage.objectViewerRead receipts from GCS
mgmt-plane-run-prodManagement Plane Cloud Run runtimecloudsql.clientConnect to Cloud SQL
secretmanager.secretAccessorRead DB URL, management key
storage.adminUpload/serve release binaries (signed URLs)
fiscalapi-deploy-prodCI deploys Core APIrun.developerDeploy new revisions (not admin — can't delete services)
artifactregistry.writerPush Docker images
mgmt-deploy-prodCI deploys Management Planerun.developerDeploy new revisions
artifactregistry.writerPush Docker images
github-actions-prodGitHub Actions (WIF)run.developerDeploy services
artifactregistry.writerPush images
iam.serviceAccountUserImpersonate deploy SAs
terraform-prodTerraform IaCeditorCreate/modify all resources
iam.serviceAccountAdminManage service accounts
resourcemanager.projectIamAdminManage IAM bindings
scheduler-prodCloud Schedulerrun.invokerInvoke Cloud Run endpoints (batch processing)
firebase-deploy-prodDocs Firebase Hosting deployfirebasehosting.adminDeploy to Firebase Hosting

Staging (zyntem-dev)

Same structure as production. Key differences:

Service AccountRolesNotes
fiscalapi-run-devcloudsql.client, secretmanager.secretAccessor, iam.serviceAccountUser, storage.objectAdminstorage.objectAdmin (not just Viewer) for dev receipt testing
mgmt-plane-run-devcloudsql.client, secretmanager.secretAccessor, storage.adminSame as prod
fiscalapi-deploy-devrun.adminAdmin (not developer) for dev flexibility
github-actionsrun.adminAdmin for dev flexibility
terraform17 roles (full infra management)Same pattern as prod
fiscalapi-scheduler-devrun.invokerSame as prod

Workload Identity Federation (WIF)

GitHub Actions authenticates via WIF — no long-lived keys stored in GitHub secrets (except Firebase deploy which requires a JSON key).

Pool: fiscalapi-cicd (both projects) Provider: github (OIDC, issuer: token.actions.githubusercontent.com) Condition: assertion.repository == 'javipelopi-dev/fiscalization'

Only the javipelopi-dev/fiscalization repository can impersonate the service accounts. No other repo, no forks.

Adding a New Role

  1. Identify the narrowest role that satisfies the requirement
  2. Add it via Terraform (not gcloud CLI) so it's tracked in IaC
  3. Update this document
  4. Get it reviewed — over-privileged roles are a security risk

Common Mistakes to Avoid

  • Don't use roles/editor on runtime accounts — it grants write access to nearly everything
  • Don't share deploy SAs across services — if one CI pipeline is compromised, only that service is affected
  • Don't add roles/owner — only the human account should have owner
  • Don't create service account keys except for Firebase deploy (use WIF instead)
  • Don't add permissions to zyntem-dev SA — it's a legacy catch-all, use specific SAs instead