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:
- A vault for plaintext custody (master password, private keys, S3 tokens).
- 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:
- Generate one strong master password → store it in your vault.
- Run the generate script with it → the script derives + seals the full set against your controller.
- 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:
- 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.
- 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.yamldocuments the exact name, namespace and keys. - Commit your sealed value into your fork. For hand-maintained charts that means
pasting it into the chart's
values.yamlsealed.<key>(see the note above), replacing our committed ciphertext; openDesk's derived set is written by the generate script. - Sync and confirm the consuming pod comes up (a sealed value that decrypts to the
wrong thing shows up as a
CrashLoopBackOffor 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 →