Skip to content

Secrets

Every secret in alpstack is committed to Git only in sealed form — encrypted with Sealed Secrets against a controller key that never leaves the cluster. Our committed sealed values are encrypted against our controllers and are useless to you. This page is the inventory of what the stack expects and the workflow to produce your own set.

The golden rule

Plaintext secrets live in your vault (we use Psono) and in the cluster's controller — never in Git, never in a chat, never echoed to a terminal that's being screen-shared or logged. kubeseal is run privately by a human; only the sealed output is committed.

The model

graph LR
  V[Your vault<br/>plaintext] -->|kubeseal --raw| S[SealedSecret YAML<br/>in your fork]
  S -->|ArgoCD sync| C[Sealed Secrets controller<br/>in-cluster key]
  C -->|decrypts to| K[Kubernetes Secret<br/>consumed by pods]

Two things are yours to establish before anything else:

  1. A vault for plaintext custody (master password, private keys, S3 tokens).
  2. One or more Sealed Secrets controllers in your cluster. We run one per layer, split by which namespaces each is trusted for:
Controller (namespace) Seals for namespaces
sealed-secrets-platform (platform-shared) longhorn-system (platform layer)
sealed-secrets-addons (addons-shared) the addons namespaces (NetBird, Vaultwarden)
sealed-secrets-opendesk (opendesk-addons) opendesk, opendesk-addons, opendesk-prod

You may run one or many — the point is each SealedSecret is sealed against the controller that owns its target namespace, with strict scope (name + namespace bound):

# Run privately; commit only the emitted sealed YAML.
printf '%s' "$PLAINTEXT" | kubeseal --raw \
    --namespace <ns> --name <secret-name> --scope strict \
    --controller-name <your-controller> \
    --controller-namespace <your-controller-ns>

Where your sealed string goes

Hand-maintained charts keep their sealed ciphertext in the chart's values.yaml under a sealed: block, referenced by a generic templates/…sealedsecret….yaml — so the template folder stays pure structure and every site-specific value (sealed ones included) lives in values.yaml. Paste your kubeseal --raw output into the matching sealed.<key>; each key carries a comment with the exact command to reseal it. A few charts instead reference a named key directly (e.g. s3secret, rootPassword) — same idea, different key name. openDesk's master-password-derived secrets are sealed and written into place by its generate script rather than pasted by hand.

One master password derives most of openDesk

openDesk's many secrets (DB passwords, service tokens, internal API keys) are derived deterministically from a single MASTER_PASSWORD by the generate-* script in opendesk-argocd, then sealed. So for the whole openDesk layer you:

  1. Generate one strong master password → store it in your vault.
  2. Run the generate script with it → the script derives + seals the full set against your controller.
  3. Commit the sealed output.

Regenerating with the same master password reproduces the same secrets — which is how the render stays stable across syncs. Losing the master password is unrecoverable from Git alone; treat it as your most important vault entry.

Secrets inventory

What the stack expects, by layer, and where each plaintext comes from. Sealed forms live in the repo as SealedSecrets (and, for openDesk, are produced by the generate script).

Secret What it is Source of plaintext
Talos PKI (secrets.yaml) Cluster PKI + bootstrap tokens talosctl gen secrets → your vault (never committed)
Cluster CA cert-manager self-signed root (<cluster>-ca) Generated by cert-manager on first sync
ArgoCD repo credentials Git deploy token for private forks Your Git host → git-ignored values-local.yaml
Longhorn S3 Access key/secret + endpoint for backups Your S3 provider
NetBird management PAT API token the operator uses to program NetBird NetBird dashboard (≤365-day cap)
Vaultwarden SSO OIDC client secret Your Keycloak client
Vaultwarden backup age keypair — public half committed, private in vault age-keygen → your vault
openDesk MASTER_PASSWORD Root secret most others derive from Generate once → your vault
Garage S3 access keys, admin token, backup encryption key Derived from master password
Postgres / MariaDB DB + backup (pgBackRest) credentials, encryption keys Derived from master password
DKIM Mail-signing private key Generated (rotation runbook exists)
TURN coturn shared secret Generated (rotation runbook exists)

A few upstream components take a plaintext secret via env/ConfigMap

openDesk is migrating its components onto proper secret references; a small number consume a value the render supplies directly. Those are an upstream limitation, not an alpstack choice — track openDesk's releases for when each moves to a secretKeyRef.

The seal-your-own workflow

For each area:

  1. Source the plaintext — generate it (master password, age key, DKIM/TURN) or fetch it from the provider (S3 token, OIDC client secret). Store it in your vault.
  2. Seal it against your controller with kubeseal --raw --scope strict (command above), matching the target namespace + secret name the chart expects — the chart's <secret>.example.yaml documents the exact name, namespace and keys.
  3. Commit your sealed value into your fork. For hand-maintained charts that means pasting it into the chart's values.yaml sealed.<key> (see the note above), replacing our committed ciphertext; openDesk's derived set is written by the generate script.
  4. Sync and confirm the consuming pod comes up (a sealed value that decrypts to the wrong thing shows up as a CrashLoopBackOff or an auth error, not a seal error).

Per-secret example files + controller-agnostic scripts

Each SealedSecret has a <secret>.example.yaml in its chart directory documenting the unsealed structure — the Secret name and namespace, every key, and where to source each value (placeholders only, no real data). Use it as the per-secret checklist. The generate-*-secrets.sh and bootstrap/seal-*.sh helpers seal against any controller: set CONTROLLER_NAME and CONTROLLER_NAMESPACE to yours (they default to this deployment's). openDesk's generate-argocd-objects.sh derives most of its set from MASTER_PASSWORD and seals it in one pass, prompting for the external inputs (S3 keys, TURN, Keycloak client secrets). DKIM and TURN also have standalone rotation runbooks.


Next: License & contributing →