Secrets & configuration
App inputs have two homes: Secret Manager (GSM → ExternalSecrets) for genuine secrets, and plain config (the workload chart’s api.env, committed) for everything public. Reserve the vault for real secrets.
The rule
- Secret → a GSM secret + an
ExternalSecretitem in the chart + asecretAccessorgrant inu2i-infra. Never committed. - Public / non-sensitive → plain
envin the chart’sapi.env, committed, per environment.
What is a secret?
| Value | Secret? | Home |
|---|---|---|
| OAuth client ID (web / macOS / iOS / Android) | No — public identifier (embedded in app binaries, visible in auth URLs) | Plain config |
| OAuth client SECRET (web / confidential client) | Yes | Secret Manager |
| Native-app OAuth (iOS / Android / desktop) | No secret at all — public client + PKCE (only the public client ID exists) | Plain config |
| DB URL, session-signing key, at-rest encryption key, API tokens | Yes | Secret Manager |
An OAuth client ID is a public identifier — it ships inside app binaries and appears in authorization URLs. Only a client secretis confidential, and native / mobile clients don’t even have one: they use PKCE, a per-flow one-time proof, so there is nothing secret to store.
Why not just put everything in Secret Manager?
It works, but storing a public value in the vault has three small costs and no security benefit:
- Cost.~$0.06 per active secret version per month + $0.03 / 10k access operations (small free tier). ESO polls every secret hourly, so each one adds access ops. Trivial per app, but it scales with secrets × envs × apps.
- Opacity.Secret values aren’t readable via
kubectl(RBAC) — so a public client ID becomes needlessly hard to inspect and debug. - Friction. Rotating a public id then means a GSM update + grant + ExternalSecret dance, instead of a one-line config change.
On the platform
Plain config lives in the app’s workload chart values (committed, one file per env). Secrets are created manually (break-glass) in GSM, then referenced by an ExternalSecret in the chart and granted in the u2i-infra rootsync. Example — Google OAuth for one app:
# helm/<app>/values/prod.yaml
api:
env:
# PUBLIC identifiers -> plain config (committed, per-env)
GOOGLE_WEB_CLIENT_ID: "748048200388-xxxx.apps.googleusercontent.com"
GOOGLE_MACOS_CLIENT_ID: "748048200388-yyyy.apps.googleusercontent.com"
gke-tenant-workload:
externalSecrets:
items:
# the ONE real secret -> Secret Manager + ExternalSecret + grant
- name: myapp-google-oauth-secret
gcpName: myapp-google-oauth-secret
key: GOOGLE_WEB_CLIENT_SECRET