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