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 ExternalSecret item in the chart + a secretAccessor grant in u2i-infra. Never committed.
  • Public / non-sensitive → plain envin the chart’s api.env, committed, per environment.

What is a secret?

ValueSecret?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)YesSecret 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 tokensYesSecret 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:

values/prod.yaml
# 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