Secrets Management¶
Secrets in Tomoda are never committed. They flow into the running process from one of three places depending on where the code is running:
| Environment | Source of secrets |
|---|---|
| Local (with GCP auth) | GCP Secret Manager via scripts/pull-secrets.sh |
| Local (without GCP auth) | Hard-coded defaults for infra creds; everything else is empty |
| GKE (dev / prod) | Kubernetes Secret resources, sourced from GCP Secret Manager |
Local development¶
Two ways to populate secrets for task dev:
Option 1: .env.local (manual, simpler)¶
Copy the template and fill in values:
cp backend/env.example.local backend/.env.local
# edit backend/.env.local with your real values
The backend's config loader will read this file when ENV=local. The template at backend/env.example.local covers DB credentials, JWT secret, encryption key, SMTP creds, Twilio creds, Stripe test keys, Google OAuth, WebAuthn, and the email API key.
Option 2: Pull from GCP Secret Manager (recommended)¶
If you have gcloud authenticated and the right IAM role on development-485000, the task dev target automatically pulls shared secrets at startup:
gcloud auth login
gcloud config set project development-485000
task dev
What happens:
- The
task devscript runsgcloud auth print-access-tokento detect auth. - If authenticated, it runs
eval $(./scripts/pull-secrets.sh --export). pull-secrets.shshells out togcloud secrets versions access latest --secret=<name>for each known secret and printsexport KEY=valuelines.- Those exports are evaluated into the current shell before
air/go run ./cmd/serverlaunches.
The list of secrets pulled lives in scripts/pull-secrets.sh. As of writing it includes JWT secret, encryption key, email API key, Stripe (secret + webhook), Google OAuth (web/iOS/Android), Google Places, Apple Sign In (client/service/team/key IDs + the P8 auth key), LINE OAuth, and Klipy.
No secrets touch disk
pull-secrets.sh is careful: it either exports values into the current shell (--export, default) or execs the backend directly with env vars set (--run). No .env file is ever written by the script.
If gcloud is not authenticated, task dev falls back to hard-coded local infra defaults:
DB_USER=tomoda
DB_PASSWORD=tomoda123
REDIS_PASSWORD=
AWS_ACCESS_KEY_ID=tomoda
AWS_SECRET_ACCESS_KEY=tomoda123
S3_ENDPOINT=http://localhost:9000
Application secrets (JWT, Stripe, Google OAuth, etc.) will be empty — most flows that depend on them will fail. This is fine for working purely on local data or UI.
--full mode¶
By default, pull-secrets.sh keeps the infra creds (DB user/password, Redis, S3) pointed at the local Docker Compose stack. Pass --full to additionally fetch the prod-style DB and Redis credentials from GCP Secret Manager — useful if you're trying to repro a prod-only issue against a tunnel.
Production secrets¶
In GKE, the backend Pod reads secrets from Kubernetes via envFrom:
envFrom:
- secretRef:
name: backend-secrets
- secretRef:
name: s3-uploader-secret
(from devops/k8s/apps/tomoda/base/backend-deployment.yaml)
The backend-secrets and s3-uploader-secret Secret resources themselves are not committed to either repository. They're materialized into the cluster by External Secrets Operator (ESO) which bridges GCP Secret Manager (via Workload Identity) and AWS Secrets Manager (via an IAM user) into Kubernetes secrets. For the full mechanism see DevOps → External Secrets and DevOps → Secrets Management.
Adding a new secret
Adding a new secret is a two-repo change:
- In
tomoda: add the field to the config struct andconfig.{ENV}.yaml, add a line inscripts/pull-secrets.shso local dev can pick it up, and document the env var. - In
devops: create the secret in GCP Secret Manager and add a key to thebackend-secretsKubernetesSecret(or whatever mechanism is in use).
Forgetting step 2 means the Pod will start without the value and the affected code path will silently fail.
Frontend "secrets" are not secrets¶
Anything prefixed EXPO_PUBLIC_* is shipped to clients — both in the static web bundle (Cloud Build bakes them in at build time) and in the mobile app binary. Treat the EXPO_PUBLIC_* env vars as public configuration, not secrets.
The values that are public-safe and live as EXPO_PUBLIC_*:
EXPO_PUBLIC_API_URL,EXPO_PUBLIC_WS_URLEXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID,EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID,EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID(OAuth client IDs are public by design)EXPO_PUBLIC_COMMIT_SHA,EXPO_PUBLIC_APP_VERSION
A Google OAuth client secret, a Stripe secret key, a JWT signing key — these must never have an EXPO_PUBLIC_ prefix and must never appear in frontend/. They belong to the backend.
Rotating a secret¶
- Generate a new value (
openssl rand -base64 32for symmetric secrets). gcloud secrets versions add <name> --data-file=-(in thedevelopment-485000project).- Restart the backend Pod (
kubectl rollout restart deployment/backend -n <ns>) so it picks up the new version. External Secrets refresh interval is typically a few minutes — see DevOps → External Secrets. - For asymmetric secrets (JWT, encryption key), follow the rotation runbook in DevOps → Secrets Management so existing tokens stay valid through the cutover.