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
- One account per function — don't share accounts across services
- Minimal roles — use the most restrictive role that works (e.g.,
storage.objectViewernotstorage.admin) - No
owneroreditoron runtime accounts — only Terraform gets broad permissions - Runtime accounts cannot deploy — deploy accounts cannot access secrets
- Document every role — if it's not in this table, it shouldn't exist
Production (zyntem-prod)
| Service Account | Purpose | Roles | Why |
|---|---|---|---|
fiscalapi-run-prod | Core API Cloud Run runtime | cloudsql.client | Connect to Cloud SQL |
secretmanager.secretAccessor | Read DB URL, Stripe keys, etc. | ||
storage.objectViewer | Read receipts from GCS | ||
mgmt-plane-run-prod | Management Plane Cloud Run runtime | cloudsql.client | Connect to Cloud SQL |
secretmanager.secretAccessor | Read DB URL, management key | ||
storage.admin | Upload/serve release binaries (signed URLs) | ||
fiscalapi-deploy-prod | CI deploys Core API | run.developer | Deploy new revisions (not admin — can't delete services) |
artifactregistry.writer | Push Docker images | ||
mgmt-deploy-prod | CI deploys Management Plane | run.developer | Deploy new revisions |
artifactregistry.writer | Push Docker images | ||
github-actions-prod | GitHub Actions (WIF) | run.developer | Deploy services |
artifactregistry.writer | Push images | ||
iam.serviceAccountUser | Impersonate deploy SAs | ||
terraform-prod | Terraform IaC | editor | Create/modify all resources |
iam.serviceAccountAdmin | Manage service accounts | ||
resourcemanager.projectIamAdmin | Manage IAM bindings | ||
scheduler-prod | Cloud Scheduler | run.invoker | Invoke Cloud Run endpoints (batch processing) |
firebase-deploy-prod | Docs Firebase Hosting deploy | firebasehosting.admin | Deploy to Firebase Hosting |
Staging (zyntem-dev)
Same structure as production. Key differences:
| Service Account | Roles | Notes |
|---|---|---|
fiscalapi-run-dev | cloudsql.client, secretmanager.secretAccessor, iam.serviceAccountUser, storage.objectAdmin | storage.objectAdmin (not just Viewer) for dev receipt testing |
mgmt-plane-run-dev | cloudsql.client, secretmanager.secretAccessor, storage.admin | Same as prod |
fiscalapi-deploy-dev | run.admin | Admin (not developer) for dev flexibility |
github-actions | run.admin | Admin for dev flexibility |
terraform | 17 roles (full infra management) | Same pattern as prod |
fiscalapi-scheduler-dev | run.invoker | Same 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
- Identify the narrowest role that satisfies the requirement
- Add it via Terraform (not
gcloudCLI) so it's tracked in IaC - Update this document
- Get it reviewed — over-privileged roles are a security risk
Common Mistakes to Avoid
- Don't use
roles/editoron 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-devSA — it's a legacy catch-all, use specific SAs instead