Skip to content

Secrets and GitOps

With secrets.generate: true (the default), the chart creates a Secret named langfuse-secrets:

KeyPurpose
saltHashes project API keys
encryptionKey64 hex characters — encrypts stored integration credentials
nextauth-secretNextAuth session signing key
postgres-passwordPostgreSQL
redis-passwordRedis
clickhouse-passwordClickHouse
root-userMinIO root user (langfuse)
root-passwordMinIO root password

The template reads any existing Secret with Helm’s lookup and reuses each key it finds, generating only what is missing. Combined with helm.sh/resource-policy: keep, values survive helm upgrade and helm uninstall without rotating.

Both the Langfuse app and the four bundled datastore subcharts read from this one Secret, so the credentials on both sides of every connection always agree.

1. Namespace and label:

Terminal window
kubectl create namespace langfuse
kubectl label namespace langfuse nebari.dev/managed=true

2. Create the Secret once, before the first sync:

Terminal window
kubectl -n langfuse create secret generic langfuse-secrets \
--from-literal=salt=$(openssl rand -hex 16) \
--from-literal=encryptionKey=$(openssl rand -hex 32) \
--from-literal=nextauth-secret=$(openssl rand -hex 32) \
--from-literal=postgres-password=$(openssl rand -hex 16) \
--from-literal=redis-password=$(openssl rand -hex 16) \
--from-literal=clickhouse-password=$(openssl rand -hex 16) \
--from-literal=root-user=langfuse \
--from-literal=root-password=$(openssl rand -hex 16)

3. Turn generation off:

secrets:
generate: false

4. Optionally, stop Argo CD flagging the pre-created Secret as out of sync with an ignoreDifferences entry for the Secret resource.

A production Helm install should do the same. Generation is a convenience for development, not a secret-management strategy — for anything real, use sealed-secrets, external-secrets, or your cloud’s secret manager.

What rotating each key costs:

KeyCost of rotating
nextauth-secretEvery active session is invalidated; users log in again.
postgres-password, redis-password, clickhouse-passwordMust be changed in the datastore and the Secret, then both sides restarted.
root-passwordSame, for MinIO.
saltInvalidates every project API key. All SDK ingestion stops until keys are reissued.
encryptionKeyOrphans all stored integration credentials.

Rotating a bundled datastore password means restarting the datastore and both Langfuse workloads:

Terminal window
kubectl -n langfuse rollout restart statefulset/langfuse-postgresql
kubectl -n langfuse rollout restart deployment/langfuse-web deployment/langfuse-worker
Terminal window
kubectl -n langfuse get secret langfuse-secrets -o jsonpath='{.data}' | jq 'keys'

All eight keys should be present. The one with a hard length requirement:

Terminal window
kubectl -n langfuse get secret langfuse-secrets \
-o jsonpath='{.data.encryptionKey}' | base64 -d | tr -d '\n' | wc -c
# 64

Anything other than 64 crash-loops langfuse-web with an ENCRYPTION_KEY length error.

Under Argo CD, confirm the Secret is stable across a sync:

Terminal window
kubectl -n langfuse get secret langfuse-secrets -o jsonpath='{.data.salt}' | sha256sum
# sync, then run again — the digest must not change

The Secret is not derivable from anything else, and without it a restored database is unreadable: encryptionKey decrypts the integration credentials, and the datastore passwords are the only copies the bundled instances have.

Terminal window
kubectl -n langfuse get secret langfuse-secrets -o yaml > langfuse-secrets-backup.yaml

Store it wherever your other credentials live, not next to the database dump.