Secrets & Identity Management
This page describes the design of secret management and identity/access control across the Shokunin Platform. It covers how secrets are stored, who can access them, how they flow to the places that need them, and how new developers are onboarded.
Three Core Principles
Every decision in this design follows three rules:
-
GCP Secret Manager is the single source of truth. No secret has an authoritative home anywhere else. GitHub Actions secrets, Vercel environment variables, and local
.env files are all derived copies populated from Secret Manager.
-
Terraform is the pipe, not the vault. Terraform reads values from Secret Manager (or from
TF_VAR_* environment variables during bootstrapping) and distributes them outward. It never invents secret values.
-
A human manually sets a secret exactly once. After initial creation, all distribution and rotation is automated.
Secret Namespaces
All secrets live in GCP Secret Manager in project shokunin-480309. They are organised into two namespaces by naming convention and access scope.
Naming pattern: shokunin-{env}-{component}
These secrets are shared across the team and consumed by CI/CD, Vercel runtime, and Cloud Run services.
| Secret ID | Purpose | How the value is set |
|---|
shokunin-dev-dolt-db-password | Dolt MySQL root password | TF_VAR_dolt_db_password on first terraform apply |
shokunin-dev-beads-api-key | Beads API authentication key | TF_VAR_beads_api_key on first terraform apply |
shokunin-dev-firebase-admin-key | Firebase Admin SDK service account JSON | Terraform creates and stores the SA key automatically |
shokunin-dev-firebase-browser-api-key | Firebase client SDK API key | Human runs gcloud secrets versions add once |
shokunin-dev-liveblocks-secret-key | Liveblocks server-side key | Human runs gcloud secrets versions add once |
shokunin-dev-eraser-api-token | Eraser diagram API token (shared) | Human runs gcloud secrets versions add once |
shokunin-dev-basehub-token | BaseHub CMS token (shared) | Human runs gcloud secrets versions add once |
shokunin-dev-vercel-caller-sa-key | SA key for Vercel → Cloud Run auth | Terraform creates and stores the SA key automatically |
All platform secrets carry a standard label set:
managed_by = "terraform"
environment = "dev"
namespace = "platform"
required = "true" | "false"
consumer = "vercel" | "cloud-run" | "local-dev" | "ci"
purpose = "db-password" | "api-key" | "sa-key" | "sdk-token"
The required = "true" label drives monitoring alerts — any required secret with zero active versions triggers an alert.
Tenant namespace
Naming pattern: shokunin-{env}-{tenant_id}-{component}
Tenant-scoped secrets are provisioned per tenant by the tenant Terraform module. They are isolated from platform secrets and from each other.
Identity Map
The table below describes every actor in the system, how it authenticates, and what it can access.
| Actor | Authenticates as | Can access |
|---|
| Human developer | GCP user account (gcloud auth login) | Secret Manager platform namespace (read-only); SA impersonation of dev-platform-sa for Terraform |
| Local Terraform | dev-platform-sa via short-lived token (SA impersonation) | Secret Manager (admin); GitHub via TF_VAR_github_token; Vercel via TF_VAR_vercel_token |
| GitHub Actions → GCP | shokunin-dev-gha-sa via Workload Identity Federation (keyless) | Artifact Registry (write); Cloud Run (deploy) |
| GitHub Actions → Vercel | VERCEL_TOKEN stored in GitHub secrets (populated by Terraform) | Vercel API (deploy) |
| Vercel runtime → Cloud Run | vercel-caller-sa JSON key in Vercel env var (populated by Terraform) | Cloud Run service (invoke) |
| Cloud Run service | shokunin-dev-run-sa workload identity | Secret Manager (accessor on Dolt password, Beads API key); Dolt database |
| Dolt VM | shokunin-dev-dolt-vm-sa workload identity | Secret Manager (accessor on Dolt password) |
| Firebase Admin SDK | shokunin-dev-firebase-admin SA key in Vercel env var (populated by Terraform) | Firebase Authentication; Firestore admin |
| OpenCode agent | opencode-agent SA (read-only) | GCP project viewer, logging viewer, monitoring viewer |
Service accounts
Every service account is managed in Terraform under platform/environments/foundation/ (project-level SAs) or platform/modules/iam/ (environment-level SAs).
| Service account | Purpose |
|---|
shokunin-dev-platform-sa | Runs Terraform for dev-shared and developer sandbox environments |
shokunin-dev-tenant-sa | Runs Terraform for per-tenant resources |
shokunin-dev-gha-sa | GitHub Actions CI/CD deployments (WIF, no key) |
shokunin-dev-run-sa | Cloud Run service identity |
shokunin-dev-dolt-vm-sa | Dolt VM identity |
shokunin-dev-codespace-iap | IAP tunnel access for developer containers |
shokunin-dev-firebase-admin | Firebase Admin SDK operations |
shokunin-dev-vercel-caller-sa | Vercel runtime → Cloud Run authentication |
opencode-agent | Read-only AI coding assistant diagnostics |
Secret Flow Pipeline
GCP Secret Manager is the authoritative store. Terraform reads values from it and distributes them to every consumer. The local .env is populated by a developer script (scripts/env-sync) that reads from the same source.
Human sets value once (gcloud secrets versions add OR TF_VAR_* env var)
│
▼
GCP Secret Manager ←── single source of truth
│
├── [terraform apply] ──► GitHub Actions secrets
│ VERCEL_TOKEN
│ VERCEL_ORG_ID
│ VERCEL_PROJECT_ID
│
├── [terraform apply] ──► Vercel environment variables
│ GOOGLE_SERVICE_ACCOUNT_KEY (vercel-caller SA key)
│ GOOGLE_FIREBASE_ADMIN_KEY (firebase-admin SA key)
│ NEXT_PUBLIC_FIREBASE_* (browser SDK config)
│ LIVEBLOCKS_SECRET_KEY
│ BEADS_API_KEY
│ ERASER_API_TOKEN
│ BASEHUB_TOKEN
│
├── [secret_key_ref] ──► Cloud Run runtime env vars (at container start)
│ DOLT_PASSWORD
│ BEADS_API_KEY
│
└── [scripts/env-sync] ──► Local .env file
Developer authenticates with their GCP user account.
No manual secret handling required.
Bootstrap exceptions
Two secrets cannot be stored in Secret Manager because accessing Secret Manager itself requires a credential — a circular dependency. These are the only intentional exceptions to Principle 1:
| Secret | Where it lives | Why |
|---|
TF_VAR_github_token | Developer’s shell environment only | Terraform needs this to authenticate to GitHub to write GitHub secrets. Storing it in Secret Manager would require a working Terraform apply to retrieve it. |
TF_VAR_vercel_token | Developer’s shell environment only | Same reason — Terraform needs this to authenticate to Vercel before it can write Vercel env vars. |
These are set by the operator before running terraform apply and are never committed or persisted:
export TF_VAR_github_token="ghp_..."
export TF_VAR_vercel_token="..."
./terraform/scripts/tf dev-shared apply
Workload Identity Federation (WIF)
GitHub Actions authenticates to GCP without any stored credentials using Workload Identity Federation. The GitHub OIDC token is exchanged for a short-lived GCP access token at runtime.
GitHub Actions workflow
│ GitHub OIDC token
▼
GCP Security Token Service
│ Short-lived access token
▼
shokunin-dev-gha-sa (impersonated)
│
├── Artifact Registry (push Docker images)
└── Cloud Run (deploy services)
WIF is configured in terraform/platform/environments/foundation/wif.tf. The pool is scoped to the Horizon-AI-dev GitHub organisation — tokens from other organisations are rejected.
WIF covers GitHub Actions → GCP only. Deployments from GitHub Actions to Vercel still require a VERCEL_TOKEN in GitHub secrets — Vercel does not currently support OIDC federation for inbound API authentication.
Secret and IAM management is split across four Terraform modules:
| Module | What it manages |
|---|
platform/environments/foundation/ | State bucket; project-level SAs (dev-platform-sa, dev-tenant-sa, opencode-agent); WIF pool and provider; project IAM bindings |
platform/modules/iam/ | Environment-level SAs (Cloud Run, Dolt VM, Codespace IAP, GHA, Firebase Admin, Vercel caller); IAM bindings scoped to resources |
platform/modules/secret-manager/ | Secret Manager containers and versions for all platform secrets |
platform/modules/github-secrets/ | Pushes secrets from Secret Manager → GitHub Actions secrets |
platform/modules/vercel-env/ | Pushes secrets from Secret Manager → Vercel environment variables |
SA key lifecycle
Two service account keys are managed entirely by Terraform — no manual gcloud iam service-accounts keys create steps:
vercel-caller-sa key — created in iam module, stored in shokunin-dev-vercel-caller-sa-key, pushed to Vercel as GOOGLE_SERVICE_ACCOUNT_KEY by the vercel-env module.
firebase-admin-sa key — created in iam module, stored in shokunin-dev-firebase-admin-key, pushed to Vercel as GOOGLE_FIREBASE_ADMIN_KEY by the vercel-env module.
To rotate a key: run terraform apply. Terraform replaces the google_service_account_key resource, updates Secret Manager, and re-pushes to Vercel in a single operation.
Developer Onboarding
Adding a new developer is a two-step process split between an admin and the developer.
Step 1 — Admin: grant bootstrap access
Create the developer’s identity file and grant them the permission needed to run their own sandbox terraform.
# 1. Create the developer's tfvars (no secrets — identity only)
cat > terraform/config/developers/alice.tfvars <<EOF
developer_id = "alice"
developer_email = "alice@horizon.ai"
EOF
git add terraform/config/developers/alice.tfvars
git commit -m "add developer: alice"
git push
# 2. Add alice@horizon.ai to developer_emails in foundation.tfvars, then apply
./scripts/tf foundation apply
Foundation grants serviceAccountTokenCreator on shokunin-dev-platform-sa to the developer’s Google account. This is the bootstrap permission that allows ./scripts/dev-setup to apply the developer’s own sandbox — without it, SA impersonation fails and dev-setup exits immediately.
serviceAccountTokenCreator lives in foundation (not in dev/main.tf) to break a circular dependency: dev/main.tf is applied via scripts/tf, which requires this very permission to impersonate the platform SA.
Step 2 — Developer: one-command local setup
# Authenticate with GCP (once per machine/container)
gcloud auth login
gcloud auth application-default login
# Run the setup script
./scripts/dev-setup alice alice@horizon.ai
dev-setup handles everything in order:
- Verifies prerequisites (
gcloud, terraform, bun) and that gcloud is authenticated
- Creates
config/developers/alice.tfvars if it does not already exist
- Runs
./scripts/tf dev alice apply — creates the developer’s Firestore database and grants Secret Manager read access
- Calls
scripts/env-sync to populate .env from Secret Manager
- Runs a health check to verify the environment is working
scripts/env-sync
env-sync is the developer’s tool for keeping their local .env in sync with Secret Manager. It:
- Authenticates using the developer’s own GCP user account (no SA key required)
- Reads all secrets from the
shokunin-dev-* platform namespace
- Writes values to
.env, skipping any that are already set
- Prints a summary: what was updated, what was skipped, what is missing a version
# Re-sync at any time (safe to re-run)
./scripts/env-sync
env-sync requires gcloud auth login to have been run first. The developer’s GCP account must have been added to the platform by an admin (Step 1 above) before env-sync will have access to secrets.
Secret Monitoring
A Cloud Monitoring dashboard deployed via Terraform provides visibility into the state of all secrets:
- All platform secrets are listed with their version count and last rotation date
- Required secrets (
required = "true" label) with zero active versions trigger an alert
- Expiring secrets — any secret version with an
expire_time set triggers an alert 30 days before expiry
The dashboard URL is output from Terraform after dev-shared apply. Developers can access it directly in the GCP Console under Monitoring → Dashboards.
Runbooks
Adding a new shared secret
- Create the Secret Manager container in
platform/modules/secret-manager/main.tf
- Add the appropriate label (
required, consumer, purpose)
- Add a
vercel_project_environment_variable resource in platform/modules/vercel-env/main.tf if the secret is consumed by Vercel
- Run
./terraform/scripts/tf dev-shared apply
- Set the initial value:
gcloud secrets versions add shokunin-dev-<name> --data-file=-
Rotating a secret
For secrets whose values are managed externally (not SA keys):
# Add a new version (GCP automatically makes it the latest active version)
echo -n "new-value" | gcloud secrets versions add shokunin-dev-<name> --data-file=-
# Re-push to consumers
./terraform/scripts/tf dev-shared apply
For SA key secrets (vercel-caller-sa-key, firebase-admin-key): simply run terraform apply — Terraform recreates the key, updates Secret Manager, and re-pushes to Vercel.
Removing a developer’s access
Delete or remove the IAM binding in their developer .tfvars and re-apply. The developer’s GCP user account will lose secretmanager.secretAccessor on the platform namespace immediately.