infra_wiki

Secrets

Where secrets are stored, what decrypts them, and how they reach hosts and workloads.

Hosts and workloads get secrets from SOPS files in git, the homelab 1Password vault and GCP Secret Manager.

Parts#

PartJobWhere it runs
SOPSEncrypts the values in *.sops.yaml filesYour machine
FluxDecrypts clusters/**/*.sops.yaml when it applies themEach cluster
sops-nixDecrypts nix/secrets/<host>.sops.yaml at activationNixOS hosts
1PasswordHolds long-lived secrets and recovery copiesThe homelab vault
External Secrets Operator (ESO)Copies values from a store into Kubernetes SecretsEach cluster
OpenBaoA secrets server with no consumersfolly

Keys and recipients#

SOPS encrypts with age, a file-encryption tool. An age recipient is a public key that SOPS encrypts a file to, and the matching private key decrypts the file.

  • Every SOPS file lists the operator key, the owner's age key. Its private half is in ~/.config/age/keys.txt and the 1Password item sops homelab age key.
  • Flux reads the operator key from the Secret sops-age in flux-system. Git does not declare that Secret.
  • A host recipient is the age key that ssh-to-age derives from the SSH host key. nix/system/sops.nix makes sops-nix decrypt with that key, so a host reads only the files encrypted to it.
  • nix/secrets/bosun.sops.yaml has oldschool, a Talos node, as its host recipient. No host configuration reads it.
  • In clusters/, SOPS encrypts only data and stringData.

Stores#

Both clusters have three ESO ClusterSecretStores.

StoreReads fromAuthenticates with
onepassword-connectThe homelab vault, through 1Password ConnectA Connect token from a SOPS file
gcp-secret-managerkthx App config in Secret Manager in bluenoseWorkload identity
kthx-datastoresThe credentials of kthx DatastoresA ServiceAccount that reads Secrets in kthx-datastores

OpenBao#

Git declares no auth method, mount, policy or client for OpenBao. Flux deploys the HelmRelease vault from clusters/folly/apps/vault/. The release runs one pod, vault-openbao-0, with Raft storage and a UI at https://vault.lolwtf.ca/ui/.

A sealed OpenBao cannot read its storage. The pod unseals itself with the GCP KMS key openbao through workload identity. If oidc.lolwtf.ca or the folly token signer fails, a restarted pod stays sealed.

Velero backs up the volume data-vault-openbao-0 each night, because the pod has the annotation backup.velero.io/backup-volumes. The copy is best effort: it reads the Raft files while the server writes them, so a backup can be inconsistent.

Rules#

  • Keep a recovery copy of each host secret in a homelab item titled <host> <thing>. If the operator key and the host key are lost, SOPS cannot decrypt the file.
  • A workload that reads the onepassword-connect store uses the vault item as its live value. An edit reaches the Secret within an hour.
  • Declare no secret from a new host file before the file lists the host recipient, or the deploy fails.
  • Replace the operator key in .sops.yaml, every SOPS file, both sops-age Secrets and 1Password, or Flux cannot decrypt.

Where it lives#

  • .sops.yaml: the recipients for each path
  • nix/secrets/: the host SOPS files
  • clusters/base/platform/: ESO, 1Password Connect and the stores
  • terraform/gcp/projects/homelab-ng/kms.tf: the OpenBao unseal key

Edit on GitHub

↑↓ move ↵ open esc close