Secrets¶
Keep the GitOps repo private (Use this template → Private). You will still commit SealedSecrets and LAN YAML; the public internet does not need that map. Argo pulls with a PAT — that is the first row in the catalog below.
Never commit a Kubernetes Secret with real data. Never commit talosconfig, secrets.yaml from talosctl gen secrets, or kubeconfig.
kubeseal workflow¶
Sealed Secrets — kubeseal encrypts to this cluster’s sealing cert only.
Validation
Do not seal anything until wave 1 passed (kubeseal --fetch-cert writes a real PEM). Ciphertext sealed to the wrong cert, or to no cert, is paper.
After wave 1 is Healthy:
kubeseal --fetch-cert --controller-name=sealed-secrets-controller \
--controller-namespace=kube-system > pub-cert.pem
Keep pub-cert.pem for sealing on the workstation. It is the public half. The private half lives in the cluster (next section).
The PAT Secret must already exist on the cluster before Argo can clone a private repo. Create it with kubectl during bootstrap. After wave 1, seal that same Secret so Git owns it:
# example: GitHub PAT for Argo (same name as the bootstrap Secret)
kubectl -n argocd create secret generic repo-creds-github \
--from-literal=type=git \
--from-literal=url=https://github.com/YOUR_GITHUB \
--from-literal=username=git \
--from-literal=password=ghp_REDACTED \
--dry-run=client -o yaml \
| kubeseal --format=yaml --cert=pub-cert.pem \
> values/argocd-repo-creds/sealed-secret-repo-creds.yaml
Label repo-creds so Argo treats them as repository credentials:
Add the file to values/argocd-repo-creds/kustomization.yaml resources:.
Back up the sealing key¶
If you rebuild the cluster or lose kube-system’s sealing Secret, every SealedSecret in Git is paper. The ciphertext is bound to that key.
kubectl -n kube-system get secret \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > ~/sealed-secrets-key-BACKUP.yaml
chmod 600 ~/sealed-secrets-key-BACKUP.yaml
# copy off the workstation: encrypted disk, password manager attachment, not Git
Restore before SealedSecrets try to decrypt (or the controller mints a new key):
kubectl apply -f ~/sealed-secrets-key-BACKUP.yaml
kubectl -n kube-system delete pod -l app.kubernetes.io/name=sealed-secrets
Take a new backup after a key rotation.
Re-seal on a new cluster¶
The controller certificate is unique. Ciphertext from another cluster will not decrypt. New cert fetch, seal again — unless you restored the old sealing key first (same cluster identity).
Catalog¶
Seal each of these after wave 1. Namespace must match. Key names must match what the Deployment / CR references.
| Secret | Namespace | Keys | Used by |
|---|---|---|---|
repo-creds-github (name yours) |
argocd |
type, url, username, password + label argocd.argoproj.io/secret-type=repo-creds |
Argo pull of a private repo (recommended). |
argocd-secret (patch) |
argocd |
accounts.labadmin.password, accounts.labadmin.passwordMtime, webhook.github.secret |
Named Argo login + GitHub webhook. Annotation sealedsecrets.bitnami.com/patch: "true" — do not replace this Secret. Wave 7. |
grafana-admin |
monitoring |
admin-user, admin-password |
Grafana admin.existingSecret. Observability. |
step-issuer-provisioner-password |
step-issuer |
password |
StepClusterIssuer provisioner.passwordRef |
tsig |
external-dns |
secret (the TSIG secret bytes, not the key name) |
external-dns TSIG_SECRET |
longhorn-backup-s3 |
longhorn |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINTS (http://10.0.0.2:9000), optionally AWS_REGION |
Longhorn defaultBackupStore |
cnpg-barman-s3 |
app namespace | ACCESS_KEY_ID, ACCESS_SECRET_KEY |
CNPG ObjectStore per database |
csi-s3-secret |
kube-system |
accessKeyID, secretAccessKey, endpoint |
csi-s3 chart (secret.create: false) |
| CNPG owner / app user | app namespace | whatever the Cluster bootstrap.initdb.secret names |
Day-2 database |
TSIG example (key material from tsig-keygen / ddns-confgen on BIND — DNS):
kubectl -n external-dns create secret generic tsig \
--from-literal=secret='BASE64-OR-HMAC-SECRET-FROM-BIND' \
--dry-run=client -o yaml \
| kubeseal --format=yaml --cert=pub-cert.pem \
> values/external-dns/sealed-secret-tsig.yaml
Add that file to values/external-dns/kustomization.yaml. The Deployment CrashLoops until it exists.
Step-CA provisioner:
kubectl -n step-issuer create secret generic step-issuer-provisioner-password \
--from-literal=password='THE-PROVISIONER-PASSWORD' \
--dry-run=client -o yaml \
| kubeseal --format=yaml --cert=pub-cert.pem \
> values/step-issuer/manifests/sealed-secret-provisioner.yaml
Add it to values/step-issuer/manifests/kustomization.yaml (that directory is already an Argo source).
argocd-secret is different. The Helm chart already owns that object (Redis password, server keys). A SealedSecret with the same name without sealedsecrets.bitnami.com/patch: "true" wipes those keys and Argo dies. Patch only the extra keys (wave 7). Grafana grafana-admin is a new Secret — no patch annotation.
Longhorn / MinIO: backups. Same seal dance, namespace longhorn.
What belongs in Git¶
| OK | Not OK |
|---|---|
| SealedSecret YAML | Secret with data: |
Placeholder CHANGEME |
Live TSIG / MinIO keys / PAT |
| Chart values without passwords | .pem private keys, secrets.yaml, kubeconfig |
pub-cert.pem if you want (public) |
sealed-secrets-key-BACKUP.yaml |
See SECURITY.md.