Secret management¶
Secrets never live in Git. They flow:
The pieces¶
- A
ClusterSecretStorenamedonepassword-connectreads theTalos1Password vault. - Per-app
externalsecret.yamlfiles reference specific 1Password items by title. Apps that use one shoulddependsOnonepassword-connectinexternal-secrets. cluster-secrets: a single 1Password item extracted into a Secret and injected into every app'spostBuild.substituteFrom. Holds cluster-wide sensitive values, including${SECRET_DOMAIN},${SECRET_INTERNAL_DOMAIN}, and internal device DNS names.cluster-settings: a git-tracked ConfigMap (components/global-vars/) holding cluster-wide non-sensitive${...}values (non-secret feature flags and the like; currently empty).
Rules for a public repository¶
- Use the
${SECRET_DOMAIN}/${SECRET_INTERNAL_DOMAIN}placeholders in Git; Flux substitutes the real values at apply time. - Any literal
${VAR}you want to survive substitution (Grafana dashboards, envsubst templates, shell snippets) must be escaped as$${VAR}. - Device address / lookup tables (e.g. router backup inventories, SNMP/NUT targets) must be
templated inside an ExternalSecret's
target.template.datablock and mounted from the rendered Secret, never rendered into a ConfigMap in Git. Internal device DNS names live in thecluster-secrets1Password item, not incluster-settings. - A CI guard (
.github/scripts/check_internal_identifiers.py, run by the security-scans workflow) fails any pull request that introduces a LAN IP, node name, site-prefixed device hostname (cr-*/sw-*), MAC address, or internal hostname (.lan/.internal) that its allowlist of accepted functional configs doesn't cover. Each allowlist entry is anAllow(path glob, kinds, reason, contains=… | regex=…): it accepts only the named kinds, only on lines containing that literal or matching that regex, which describes the line's shape or YAML key and never the value, and only one identifier per line unless it setsper_line. Any other identifier in the same file still fails. An entry that matches nothing fails the scan with a message to delete it, and a pull request that edits the script is scanned against the whole tree. Device models are intentionally not enumerated in the script to avoid self-disclosure in this public repository.
Talos machine secrets (talsecret)¶
The talhelper secrets bundle (cluster CA/PKI, etcd certs, bootstrap tokens) follows the same
"1Password owns it" rule as everything else. There is no SOPS anywhere in this repository:
the historical talos/talsecret.sops.yaml was removed in PR #3463 (2026-07) after the age key for
it was lost; the encrypted blob left in Git history is dead ciphertext.
- The bundle is stored as the
talsecretdocument in theTalos1Password vault. just talos gen-configfetches it (op document get talsecret --vault Talos) to a temp file, runstalhelper genconfig, and cleans up. Node configs land in the gitignoredtalos/clusterconfig/.just talos gen-secretcreates the 1Password document (fromtalhelper gensecret) only if it does not already exist.- Recovery without 1Password: any previously generated node config contains the full secret material, so the bundle can be reconstructed offline from the gitignored output of a prior run:
talosctl gen secrets \
--from-controlplane-config talos/clusterconfig/kubernetes-<node>.yaml \
-o /tmp/talsecret.yaml --force
This was how the bundle was recovered when the age key disappeared: regenerated configs were verified byte-identical before the SOPS file was deleted.