Skip to content

Secrets Management

Shoehorn references every credential through a typed *SecretRef shape: {name, key}, the same shape as Kubernetes’ native valueFrom.secretKeyRef. The chart never creates Secrets; it only reads them. Bring whatever secret tooling you already use (kubectl, Sealed Secrets, External Secrets Operator, CSI Secret Store) and the chart consumes the Secrets it produces.

There are two operating modes:

  • One Secret for everything. Set secret.defaultName once. Every *SecretRef may then omit name: and just supply key:. Best for kubectl + Sealed Secrets workflows.
  • Per-credential Secrets. Set name: on each *SecretRef pointing at a different K8s Secret synced from a different upstream source. Best for ESO + Vault / AWS / GCP / Azure where each credential domain rotates independently.

Use this for getting started, local dev, or any workflow where one Kubernetes Secret holds every credential.

Terminal window
kubectl create secret generic shoehorn-credentials -n shoehorn \
--from-literal=postgres_password='<postgres-password>' \
--from-literal=db_password='<app-user-password>' \
--from-literal=valkey_password='<valkey-password>' \
--from-literal=meilisearch_master_key="$(openssl rand -base64 32)" \
--from-literal=jwt_secret="$(openssl rand -hex 32)" \
--from-literal=auth_encryption_key="$(openssl rand -base64 32)" \
--from-literal=secrets_encryption_key="$(openssl rand -base64 32)"
values.yaml
secret:
defaultName: shoehorn-credentials
postgresql:
superuserPasswordSecretRef:
key: postgres_password # name defaults to shoehorn-credentials
passwordSecretRef:
key: db_password
valkey:
passwordSecretRef:
key: valkey_password
meilisearch:
masterKeySecretRef:
key: meilisearch_master_key
auth:
session:
jwtSecretRef:
key: jwt_secret
encryptionKeyRef:
key: auth_encryption_key
secretsEncryptionKeyRef:
key: secrets_encryption_key

The chart also ships an examples/values-minimal.yaml showing this pattern end-to-end.

Use this when you want the encrypted Secret committed in git. The shape is identical to the kubectl path — secret.defaultName plus per-ref key: — only the way you create the Secret changes.

  1. Generate the literal Secret manifest as in the kubectl path, but pipe through kubeseal:
    Terminal window
    kubectl create secret generic shoehorn-credentials -n shoehorn \
    --dry-run=client -o yaml --from-literal=... \
    | kubeseal --controller-name=sealed-secrets --format=yaml > shoehorn-credentials.sealed.yaml
  2. Commit shoehorn-credentials.sealed.yaml to git.
  3. Apply it (manually or via Argo CD / Flux). The Sealed Secrets controller decrypts and creates the matching Secret.
  4. The chart reads it through secret.defaultName: shoehorn-credentials — no other config changes.

See bitnami/sealed-secrets for installation and operator details.

Each credential domain (database, auth, session, search, cache, GitHub, SMTP) gets its own Vault path, its own ExternalSecret, and its own Kubernetes Secret. Rotating one credential doesn’t touch the others’ resourceVersion, so unrelated pods don’t get restarted, and ESO refreshInterval can differ per source (DB: 1h, JWT: 24h).

The chart ships a worked example at examples/values-eso-vault.yaml. The shape:

secret:
defaultName: "" # no shared default — each ref names its own Secret
postgresql:
superuserPasswordSecretRef:
name: shoehorn-db-creds # synced from secret/data/shoehorn/database
key: postgres_password
passwordSecretRef:
name: shoehorn-db-creds
key: db_password
auth:
okta:
clientSecretRef:
name: shoehorn-okta-creds # synced from secret/data/shoehorn/okta
key: client_secret
apiTokenSecretRef:
name: shoehorn-okta-creds
key: api_token
session:
jwtSecretRef:
name: shoehorn-session-keys # synced from secret/data/shoehorn/session
key: jwt_secret
# ...

Apply the matching ExternalSecret resources alongside (one per domain). Example for the database credentials:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: shoehorn-db-creds
namespace: shoehorn
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: shoehorn-db-creds # matches postgresql.*SecretRef.name
data:
- secretKey: postgres_password
remoteRef:
key: secret/data/shoehorn/database
property: postgres_password
- secretKey: db_password
remoteRef:
key: secret/data/shoehorn/database
property: db_password

And for the Okta credentials:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: shoehorn-okta-creds
namespace: shoehorn
spec:
refreshInterval: 6h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: shoehorn-okta-creds
data:
- secretKey: client_secret
remoteRef:
key: secret/data/shoehorn/okta
property: client_secret
- secretKey: api_token
remoteRef:
key: secret/data/shoehorn/okta
property: api_token

Pre-requisites: External Secrets Operator installed, a ClusterSecretStore (e.g. vault-backend) configured against your Vault, and the listed Vault paths populated.

External Secrets Operator + AWS Secrets Manager

Section titled “External Secrets Operator + AWS Secrets Manager”

Same shape, different SecretStore. Each *SecretRef.name points at a Secret synced from a distinct AWS Secrets Manager ARN.

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: aws-secretsmanager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secrets
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: shoehorn-db-creds
namespace: shoehorn
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: shoehorn-db-creds
data:
- secretKey: postgres_password
remoteRef:
key: arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/shoehorn/db-postgres
- secretKey: db_password
remoteRef:
key: arn:aws:secretsmanager:us-east-1:111122223333:secret:prod/shoehorn/db-app

The chart values block is identical to the Vault example. Only the SecretStore provider changes.

The CSI Secret Store driver mounts secrets as files but can also sync them to Kubernetes Secrets via secretObjects. Reference those synced Secrets the same way:

apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: shoehorn-db-creds
spec:
provider: gcp # or `azure`, `aws`, `vault`
parameters:
secrets: |
- resourceName: "projects/your-project/secrets/shoehorn-postgres-password/versions/latest"
path: "postgres_password"
- resourceName: "projects/your-project/secrets/shoehorn-db-password/versions/latest"
path: "db_password"
secretObjects:
- secretName: shoehorn-db-creds
type: Opaque
data:
- objectName: postgres_password
key: postgres_password
- objectName: db_password
key: db_password

Then mount the SPC on a workload (so the driver creates the K8s Secret) and reference shoehorn-db-creds from postgresql.passwordSecretRef.name.

Every credential the chart consumes, mapped to its *SecretRef values path and the env var the chart emits.

Credential / env varValues path
POSTGRES_PASSWORDpostgresql.superuserPasswordSecretRef
DB_PASSWORD (and APP_USER_PASSWORD)postgresql.passwordSecretRef
VALKEY_PASSWORDvalkey.passwordSecretRef
MEILISEARCH_API_KEY (clients), MEILI_MASTER_KEY (server)meilisearch.masterKeySecretRef
JWT_SECRETauth.session.jwtSecretRef
AUTH_ENCRYPTION_KEYauth.session.encryptionKeyRef
SECRETS_ENCRYPTION_KEYauth.session.secretsEncryptionKeyRef
ZITADEL_SERVICE_USER_PAT (optional, orgdata)auth.zitadel.serviceUserPatSecretRef
OKTA_CLIENT_SECRET (required when provider: okta)auth.okta.clientSecretRef
OKTA_API_TOKEN (optional, orgdata)auth.okta.apiTokenSecretRef
ENTRA_CLIENT_SECRET (required when provider: entra-id)auth.entraId.clientSecretRef
ARGOCD_TOKEN (optional)auth.argocd.tokenSecretRef
UPCLOUD_TOKEN (optional)cloudProviders.upcloud.tokenSecretRef
SMTP_PASSWORD (required when smtp.enabled)smtp.passwordSecretRef

Public identifiers are plain values, not secret refs:

Env varValues path
ZITADEL_PROJECT_IDauth.zitadel.projectId
ZITADEL_CLIENT_IDauth.zitadel.clientId
GITHUB_APP_IDauth.github.appId
GITHUB_APP_INSTALLATION_ID (and GITHUB_INSTALLATION_ID)auth.github.installationId
GITHUB_FORGE_APP_IDauth.github.forge.appId
GITHUB_FORGE_INSTALLATION_IDauth.github.forge.installationId
GITHUB_FORGE_ORGANIZATIONauth.github.forge.organization
  • Identity providers — overview of supported IdPs
  • Okta integration, Zitadel integration — per-provider setup walkthroughs
  • examples/values-minimal.yaml and examples/values-eso-vault.yaml in the Helm chart — copy-pasteable starting points for the two operating modes