Secrets
This repository is public. Nothing in it is a credential, and nothing in it is an identifying value either — no real addresses, endpoints, account or key identifiers. Files refer to where a value lives; they never inline it.
There are two stores, used at different times.
Proton Pass — bootstrap
Anything needed before the cluster can resolve secrets for itself comes from a Proton Pass
vault, addressed as pass://<vault>/<item>/<field>. The pointer is committed; the value is
resolved at runtime and never written to disk.
Two resolvers, one per plane:
- Ansible — the
protonpasslookup plugin (ansible/plugins/lookup/protonpass.py, registered bylookup_plugins = plugins/lookupinansible/ansible.cfg). It shells out topass-cliand rejects any reference that does not start withpass://. Used for the admin SSH identity, the tailnet MagicDNS suffix, each node’s IP, the Flux git deploy key, the OpenBao KMIP seal material, and the OpenBao root token. - OpenTofu —
pass-cli run --env-file secrets.env -- tofu <cmd>. Each module commits asecrets.envholding pointers only. See Rules for every module.
.taskfiles/bao/Taskfile.yaml’s bao:kv uses a third form, pass-cli item view piped into
kubectl exec over stdin — never argv, which would put the root token in the process
table.
OpenBao — runtime
Apps read their secrets from OpenBao through External Secrets Operator, never from a static credential committed to the repo. The store an app uses depends on its tier:
bao-infra(OpenBao namespaceinfra) — every node-agnostic component.bao-node-<hostname>(OpenBao namespacenode-<hostname>) — that node’s own apps.
Each OpenBao namespace has its own secret/ kv-v2 mount, its own kubernetes auth mount and
its own reader policy, so a compromised binding cannot read outside its namespace by
construction. An app needing both scopes uses two ExternalSecrets, one per secretStoreRef
— a store’s reach is never widened to cover both.
No long-lived credential lives in-cluster: every ClusterSecretStore authenticates via
OpenBao’s kubernetes auth method as the external-secrets ServiceAccount, and the k0s API
validates each request with a TokenReview.
Two secrets are deliberately outside GitOps reach, because they exist to seed values Flux
needs before it can resolve anything: the git deploy key and OpenBao’s KMIP seal material.
Both are (re)created by the relevant task ans:* run, not by Flux.
ESO RBAC
The external-secrets chart’s own cluster-wide RBAC is disabled — values.rbac.create: false
in infra/external-secrets/app/helmrelease.yaml. In its place:
infra/external-secrets/app/{clusterrole,role}.yamlgrant ESO only what is genuinely cluster-scoped (ClusterSecretStore) or scoped to its own namespace.- The
rbac-eso-writertemplate ininfra/configs/namespaces/_templates/grants ESO write access toSecrets in one namespace at a time, and is added to a namespace’s overlay only if that namespace actually has anExternalSecret.
ESO’s access to secrets is never cluster-wide.
Values that are identifying but not secret
A public IP is not a credential, but publishing it here would still tie this repo to a machine. The rule extends to every file type:
- In a tofu module, set it as
TF_VAR_<name>=pass://...insecrets.env. - In Ansible, use a
protonpasslookup — that is whyansible/nodes/<hostname>/host.ymlcarriesip: "{{ lookup('protonpass', ...) }}"rather than a literal. - In the cluster, use a placeholder.
infra/traefik-edgeandinfra/monitoringreference${PUBLIC_IP}and${MESH_IP}, substituted bypostBuild.substituteFromfrom anedge-ipsConfigMap thatansible/roles/flux_bootstrappushes straight intoflux-system— into the cluster, never into git.
A genuinely non-identifying constant that is shared across the repo is read from its
committed source instead of duplicated. The base domain is the only one, and it lives in
config/domain/domain.env — see Domains.
Adding a new secret
- Put the value in the Proton Pass vault if something needs it before the cluster is up;
otherwise put it in OpenBao under the right namespace (
task bao:kv -- put -namespace=...). - Reference it by pointer —
pass://in Ansible orsecrets.env, anExternalSecretin the cluster. - If it is an
ExternalSecretin a namespace that had none, add therbac-eso-writertemplate to that namespace’s overlay. - Confirm gitleaks is happy:
pre-commit run gitleaks --all-files.