Secrets

Shepherd needs two values it cannot invent for itself: an encryption key, and a password for the first administrator. You can supply them, or you can let the cluster generate them so they never pass through your shell, your values file, or your terminal history.

What goes in the secret

The chart reads one Kubernetes Secret, named by existingSecret. Its keys are environment variable names — the full list is in Configuration. Only two are ever required: SHEPHERD_DATABASE_URL (unless CloudNativePG provides it) and SHEPHERD_SECURITY_ENCRYPTION_KEY.

The encryption key cannot be rotated. It encrypts git credentials and the OIDC client secret at rest. A new key cannot decrypt what the old one wrote, and nothing reports the loss at the moment it happens — Shepherd simply stops being able to use those credentials. Generate it once, and back it up with your other secrets.

Supplying them yourself

shell
kubectl -n shepherd create secret generic shepherd-secrets \
  --from-literal=SHEPHERD_DATABASE_URL='postgres://user:pass@host:5432/shepherd' \
  --from-literal=SHEPHERD_SECURITY_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
  --from-literal=SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD='choose-a-password'

helm install shepherd oci://ghcr.io/procoduck/charts/shepherd --version 0.22.0 \
  --namespace shepherd --set existingSecret=shepherd-secrets

This works with any secret tooling you already have — Sealed Secrets, SOPS, Vault Agent, an External Secrets SecretStore of your own. The chart only needs the Secret to exist by the time it installs; it does not care what created it.

Letting the cluster generate them

With the External Secrets Operator installed, the chart can generate both values in-cluster:

shell
helm install shepherd oci://ghcr.io/procoduck/charts/shepherd --version 0.22.0 \
  --namespace shepherd --create-namespace \
  --set externalSecrets.enabled=true \
  --set cnpg.enabled=true          # required: the generated Secret holds no database URL

The generated Secret holds only the encryption key and the admin password, so the database URL must come from cnpg.enabled or from extraEnv; the chart refuses to render otherwise, and refuses existingSecret or an inline secrets: block alongside it.

There is nothing else to do. That install has no secret material in it at all: the chart renders Password generators and ExternalSecrets (two and one for Shepherd's own secret; a third generator and a second ExternalSecret for the sandbox token while simulator.enabled is on), and the operator produces the Secrets the workloads already read. No SecretStore is needed — a generator is the source.

Retrieve the generated administrator password once, to sign in the first time:

shell
kubectl -n shepherd get secret shepherd-secrets \
  -o jsonpath='{.data.SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d

Change it after signing in, or create your own account and disable that one — see Users and teams.

What the chart does to keep the key alive

A generated encryption key is only safe if it is generated once and never taken away, so two settings are not adjustable in any meaningful sense:

  • refreshInterval: "0" — generate once, never regenerate. A rotation here would silently orphan every credential already encrypted with the old key.
  • deletionPolicy: Retain — deleting the ExternalSecret, or uninstalling the release, leaves the Secret in place. The key outlives the thing that made it.

Both are asserted by the chart’s own tests, because neither failure announces itself: losing the key looks exactly like Shepherd forgetting your git credentials.

Neither setting, though, is what actually protects the key. They hold only while the ExternalSecret itself survives, and they do nothing if Helm deletes and recreates it — which is exactly what the default hook policy does on every upgrade: the fresh object syncs once against fresh Password generators and overwrites the key. The real protection is that the chart does not render an ExternalSecret that already exists. A hook absent from the manifest is never processed, so the live one is left alone.

Deploying the chart with Argo CD, Flux, or helm template | kubectl apply? Set externalSecrets.render: never once the secret exists. That existence check uses Helm’s lookup, which needs a live cluster connection. Tools that render offline have none, so the guard fails open and the ExternalSecret is recreated on every sync — regenerating an encryption key that cannot be rotated, with no error at the moment it happens. You discover it later, as every stored git credential and OIDC client secret failing to decrypt at once. The value takes the same three settings as cnpg.render.

This path is tested against a real operator. Shepherd’s Kubernetes suite installs the External Secrets Operator into a throwaway cluster, installs Shepherd supplying nothing, and checks the generated key decodes to exactly the 32 bytes Shepherd requires and that the generated password actually signs in — with a wrong password as the control.

It is one or the other

externalSecrets.enabled refuses to render alongside existingSecret or an inline secrets: block, and says so at install time rather than producing two competing definitions of the same Secret. Pick the source of truth you want; the chart will not try to merge them.

The third option, and why it is last

You can also put the values straight in your values file under secrets:, and the chart renders the Secret for you:

values.yaml
secrets:
  SHEPHERD_DATABASE_URL: "postgres://…"
  SHEPHERD_SECURITY_ENCRYPTION_KEY: "…32 bytes, base64…"
  SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD: "choose-something"

It is the least private of the three. Whatever you put there is stored verbatim in the Helm release and stays readable through helm get values shepherd for the life of the release history — not just in your values file, where you can at least encrypt it. Use it for a trial; prefer existingSecret or the generated path for anything real.

Two behaviours are worth knowing if you do use it. Changing a value here restarts the pods, as it should — environment variables are read once at container start, so the chart hashes the Secret into the pod template to force a rollout. And since chart 0.9.0 helm uninstall does remove this Secret; earlier charts left it behind. Copy the encryption key out before uninstalling if you intend to reinstall against the same database.

Never put any of these in the config: block. That renders into a ConfigMap, which is not a place for a password.