Secrets
Secrets
Every credential the platform runs on lives in one Kubernetes Secret in your namespace, named <release>-platform-env. The API reads its whole environment from it; the auth service, the email worker and the web tier each pull the specific keys they need. The license agent reads and writes it through the Kubernetes API. Nothing in the chart talks to a secret store, and nothing leaves your cluster.
What the chart generates
On a first install, with no values set, the chart mints these and writes them into that Secret:
| Key | What it is |
|---|---|
JWT_SECRET | Signs session tokens. The auth container gets the same value as its GOTRUE_JWT_SECRET. |
POSTGRES_PASSWORD | The platform's database role. |
AUTH_ADMIN_PASSWORD | The supabase_auth_admin role the auth service connects as. |
CACHE_PASSWORD | The bundled Redis, when redis.mode=bundled. |
NUXT_SESSION_PASSWORD | The web tier's session cookie key. |
DEPLOYMENTS_SERVICE_KEY | The API key this platform presents to its own downstream deployments. Only relevant if you are an MVNE provisioning MVNOs. |
GOTRUE_HOOK_SEND_EMAIL_SECRETS | Signs the auth service's send-email webhook. Generated only when auth.emailHook.enabled=true. |
You can supply any of them instead. secrets.jwtSecret and app.sessionPassword take a value directly; the database and cache passwords come from your own DSN when the service is external.
Upgrades never rotate a live credential
Before generating anything, the chart looks up the Secret already in the namespace and reuses every key it finds. A fresh random value is only minted for a key that is not there yet.
That is what makes helm upgrade safe. Postgres was initialised with a password on day one and will not change its mind; if an upgrade re-generated POSTGRES_PASSWORD, the DSN in the Secret would stop matching the database and the platform would fail to connect. The lookup means an upgrade cannot pull a credential out from under a running deployment, no matter how many times you run it or which flags you pass.
The corollary: deleting a key from the Secret by hand makes the next upgrade generate a new one. That is the only way to rotate, and for the database password it is not enough on its own, because the database still holds the old one.
The at-rest encryption key
The platform encrypts the credentials it stores for you (supplier API keys, payment keys, per-tenant secrets) with an age key before they go into the database. Helm cannot generate an X25519 keypair, so the chart does not: you generate one and pass it in.
age-keygen -o key.txt
# public key: age1qyq... <- the recipient
# the file's AGE-SECRET-KEY-... line is the identity
helm upgrade --install my-mvno oci://ghcr.io/solvegio/charts/platform --version <version> \
--namespace my-mvno \
--set secrets.kek.recipient=age1qyq... \
--set secrets.kek.identity=AGE-SECRET-KEY-...
secrets.kek.recipient is the public half and lands in the ConfigMap as SECRETS_KEK_AGE_RECIPIENT. secrets.kek.identity is the private half and lands in the Secret as SECRETS_KEK_AGE_IDENTITY. You need the identity to read back anything the recipient encrypted, so keep a copy of key.txt somewhere you will still have it after a disaster. Losing it means losing every credential the platform stored.
Leave them unset and the platform still boots. It falls back to a pass-through codec and writes those credentials to the database in plain text, saying so loudly in the API logs on every start. Nothing warns you again after that. Set the keypair before you put a real supplier or payment credential into the platform, because switching the codec on later does not re-encrypt what is already there.
Bringing your own Secret
If your secrets come from SOPS, sealed-secrets, Vault or External Secrets Operator, set secrets.existingSecret and the chart renders no Secret of its own. Every reference in every template points at the name you gave.
--set secrets.existingSecret=my-mvno-secrets
Your Secret has to carry every key the chart would otherwise have generated, plus DB_DSN, AUTH_DATABASE_URL and ACTIVATION_CODE. The environment reference lists them all with their shapes. activation.code stops being a required Helm value in this mode, because the chart is no longer the thing writing it: the license agent reads ACTIVATION_CODE out of the Secret object directly.
With External Secrets Operator, point an ExternalSecret at that same name and let it materialise the Secret before you install:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: my-mvno-secrets
spec:
secretStoreRef: { name: my-vault, kind: ClusterSecretStore }
target:
name: my-mvno-secrets # == secrets.existingSecret
creationPolicy: Owner
deletionPolicy: Retain
dataFrom:
- extract: { key: solvegio/my-mvno }
The license agent writes into that Secret too
This is the part that bites people, so it gets its own heading.
The license agent holds get and patch on that one Secret, by name, and nothing else in the cluster. It owns six keys in it, and writes them on first enrollment and again on every renewal and key rotation for as long as the deployment runs:
SIGNING_SEED, POP_SIGNING_SEED, PARENT_LEASE_TOKEN, PARENT_BASE_URL, REQUIRE_PARENT_LEASE, ACTIVATION_CODE_CONSUMED.
A revoked deployment picks up a seventh, LEASE_STATUS, which the agent clears again when it recovers. That is the whole of what it writes. Everything else in the Secret is yours or the chart's.
The agent is the source of truth for those keys, not your secret store. The signing seed is generated inside your cluster and has no copy anywhere else, including with us. The lease token is a fresh signed credential the agent fetches and persists on a schedule. If your pipeline reconciles the Secret back to what your store thinks it should contain, it reverts the lease the agent just wrote, and the API stops serving on the next check.
So: whatever supplies the Secret must retain keys it did not create.
- With ESO,
creationPolicy: OwnerplusdeletionPolicy: Retain, as above. Do not turn on a sync policy that prunes keys ESO did not put there. ESO's default merge behaviour already leaves them alone; the danger is a policy that replaces the Secret wholesale. - With sealed-secrets, do not apply a
SealedSecretthat recreates the Secret from scratch after the agent has enrolled. - With SOPS and a plain
kubectl apply, useapplyand notreplace.
Do not put SIGNING_SEED or PARENT_LEASE_TOKEN into your secret store as literal values and expect them to work. A lease has an expiry and is rotated; a copied one goes stale. Let the agent own them.
Your key never leaves explains what the signing seed is and why it is generated where it is. If the API is stuck waiting on a lease, Licensing failures is the page you want.