Skip to main content

Secrets

The chart reads every sensitive value from a single Kubernetes secret, named by app.existingSecret.

The keys

KeyRequiredPurpose
APP_KEKWhen KMS_BACKEND=k8sEnvelope-encryption key
SESSION_SECRETFor more than one replicaSigns the session cookie
INDEX_PEPPERYesHMAC for blind indexes on searchable columns
AUTH_TOKEN_PEPPERYesHMAC for hashing login tokens at rest
LICENSE_KEYYesSigned license token
DATABASE_URLUsuallyFull DSN. Not needed with the Cloud SQL Auth Proxy
MODEL_CONTENT_KEYSometimesDecrypts model assets, when the license does not carry it
OPENAI_API_KEYNoEnables narrative rendering and chat
ACCESS_TOKENSNoBreak-glass login tokens
GOOGLE_CLIENT_IDNoGoogle Sign-In

Generating the crypto material

openssl rand -base64 32 # APP_KEK
openssl rand -base64 32 # SESSION_SECRET
openssl rand -base64 32 # INDEX_PEPPER
openssl rand -base64 32 # AUTH_TOKEN_PEPPER
warning

APP_KEK is not recoverable. Lose it and every encrypted row is permanently unreadable. A database restore does not help, because the data in the backup is encrypted with the key you lost.

Back it up somewhere that outlives the cluster, the namespace, and the person who created it. A cloud secret manager in a different project, or your organisation's password manager. Not only in the Kubernetes Secret.

INDEX_PEPPER and AUTH_TOKEN_PEPPER are nearly as unforgiving. Changing INDEX_PEPPER invalidates every blind index, so equality lookups on sensitive columns stop matching until the data is reindexed.

Creating it

For evaluation:

kubectl create secret generic presponsieve-secrets -n presponsieve \
--from-literal=APP_KEK="..." \
--from-literal=SESSION_SECRET="..." \
--from-literal=INDEX_PEPPER="..." \
--from-literal=AUTH_TOKEN_PEPPER="..." \
--from-literal=LICENSE_KEY="..."

For production, use External Secrets, Vault, or Sealed Secrets. The chart only needs the secret to exist with the right keys.

External Secrets

Store the values as a JSON object in your cloud secret manager, then project the keys:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: presponsieve-secrets
namespace: presponsieve
spec:
refreshInterval: 1h
secretStoreRef:
name: your-store
kind: SecretStore
target:
name: presponsieve-secrets
creationPolicy: Owner
data:
- secretKey: APP_KEK
remoteRef: {key: presponsieve/app, property: APP_KEK}
- secretKey: SESSION_SECRET
remoteRef: {key: presponsieve/app, property: SESSION_SECRET}
- secretKey: INDEX_PEPPER
remoteRef: {key: presponsieve/app, property: INDEX_PEPPER}
- secretKey: AUTH_TOKEN_PEPPER
remoteRef: {key: presponsieve/app, property: AUTH_TOKEN_PEPPER}
- secretKey: LICENSE_KEY
remoteRef: {key: presponsieve/app, property: LICENSE_KEY}

The Terraform blueprints set this up for you on all three clouds.

Rotation and restarts

Changing a Kubernetes secret does not restart the pods. Install Reloader and a rotated license or pepper triggers a rolling restart automatically:

kubectl annotate externalsecret presponsieve-secrets -n presponsieve \
force-sync="$(date +%s)" --overwrite

Without Reloader:

kubectl rollout restart deploy/presponsieve -n presponsieve

Avoiding APP_KEK entirely

Set KMS_BACKEND to aws, gcp, or vault and the key-encryption key lives in a real KMS instead. That removes the one secret whose loss is unrecoverable from your backup burden, and lets you revoke access centrally.

The k8s backend is documented as the weaker fallback for a reason. Use a real KMS in production. See Encryption.