Secrets and PKI
This wiki is public (wiki.lolwtf.ca). Nothing decrypted β no secret values, keys, tokens, or recovery material β ever lands in
docs/.SOPS (Kubernetes cluster secrets)
.sops.yamlmatchesclusters/.*\.sops\.ya?mland encrypts only thedataandstringDatafields (encrypted_regex: "^(data|stringData)$"); the rest of the manifest βapiVersion,kind,metadataβ stays plaintext and diffable.These files decrypt against a single shared age recipient (the fleet's cluster-secrets key). Flux decrypts them in-cluster at reconcile time.
sops clusters/folly/config/cluster-secrets.sops.yaml # edit sops -e -i clusters/<cluster>/<path>.sops.yaml # encrypt a new file
sops-nix (bare NixOS host secrets)
nix/secrets/<host>.sops.yamlholds secrets for a single bare host β currentlyoptiplex,retrofit,oldschool, andforge. Each file has its own.sops.yamlrule keyed to that host's own age recipient (an ssh-to-age conversion of its ed25519 SSH host key), in addition to the shared key.nix/system/sops.nixsetssops.age.sshKeyPathsto the host's own/etc/ssh/ssh_host_ed25519_keyβ sops-nix decrypts on the host using its own key rather than a fleet-wide shared key, so a compromised host only exposes secrets scoped to itself.flake.nixwires each host'ssops.defaultSopsFileand declares its secrets:optiplexandretrofit(bothrole = "control-plane") carryk8s-sa-signing-key(ownerkubernetes, restartskube-apiserver/kube-controller-manager);oldschoolcarriesharmonia-cache-key(cache public half atnix/secrets/oldschool-harmonia-cache.pub);forgecarriesharmonia-cache-key(public half atnix/secrets/forge-harmonia-cache.pub).k8s-sa-signing-keyis the per-cluster ServiceAccount token signer private key issued byterraform/pkiβ see PKI below.nix/secrets/bosun.sops.yamlbreaks the one-file-per-host pattern above on purpose: it holds bosun's own key on the shared Spindrift+bosun GitHub App (see Architecture/Bosun), encrypted to every bosun host's recipient β riptide and oldschool β rather than a copy per host. A new bosun host needs its recipient added to this file through the same two-stage flow before itsbosununit can start, and what gets pasted in is this App key, never a personal access token.The two-stage recipient setup (operator age key only on first commit, host's own ssh-to-age recipient added after first boot) and the operator-key location (
~/.config/age/keys.txt, 1Password "sops homelab age key" β NOT the sops binary's default path) are spelled out in Runbooks/SOPS Secrets and Age Keys.
OpenBao
Deployed by Flux from
clusters/folly/apps/vault/(HelmReleasevault, chartopenbao, namespacevault). No other cluster runs it.Storage is integrated Raft (
storage "raft", single nodevault-openbao-0); no external database.Auto-unseal is
seal "gcpckms"against a GCP KMS key isolated to OpenBao (key ringopenbao, crypto keyopenbao, projecthomelab-ng, regionnorthamerica-northeast1, provisioned interraform/gcp/projects/homelab-ng/kms.tf).OpenBao authenticates to GCP with no static key file: a projected ServiceAccount token (audience
fml-pool/providerfolly) is exchanged via GCP Workload Identity Federation for short-lived access as thevault-idGSA, which holds the KMS grants. The federation provider is the folly cluster's own OIDC issuer β the same issuer PKI below sets up β so unsealing depends on that cluster's ServiceAccount token signing working.Reachable at
vault.${SECRET_DOMAIN}(folly's Gateway/cert-manager) once bootstrapped.Bootstrapping (init, unseal verification, first policies) is a runbook, not architecture: Runbooks/OpenBao Bootstrap.
PKI (
terraform/pki)Trust chain, root to leaf:
FML Root CA β offline; only its certificate (no key) is read from 1Password. Never touched by Terraform.
FML Intermediate CA β certificate and key both read from 1Password at plan/apply time (so the key transits Terraform state, a deliberate tradeoff given state lives in the IAM-gated homelab-ng bucket).
FML K8s
<cluster>CA β issued here per cluster (folly,offsite), signed by the intermediate.CA:TRUE,pathLen:0(may only sign leaves, never another CA). ~2 year validity.<cluster>ServiceAccount token signer β a leaf issued here per cluster, signed by that cluster's K8s CA. RSA-4096, 1 year validity.
Requires OpenTofu specifically (
opentofu/tlsprovider fork, formax_path_lengthβ not published for plain Terraform).Each cluster's kube-apiserver signs ServiceAccount tokens with its signer key (delivered to the control-plane hosts via sops-nix as
k8s-sa-signing-key, above) and advertises issuerhttps://oidc.lolwtf.ca/<cluster>.The discovery documents (
oidc/<cluster>/{openid-configuration.json,jwks.json}, JWKSkid=base64url(SHA256(SPKI))of the signer) are committed interraform/pki/and served atoidc.lolwtf.cavia Cloudflare Pages (domain/DNS interraform/network/cloudflare/oidc.tf, deployed by.github/workflows/oidc.yml).That issuer is also a GCP Workload Identity Federation provider (
fml-pool, one provider per cluster, interraform/gcp/projects/homelab-ng/workload-identity.tf) β the mechanism OpenBao uses to reach its KMS key above, and available to any other in-cluster workload that needs federated GCP access.Rotation is a runbook step (
scripts/pki/post-rotate.shre-encrypts signer keys and regenerates the discovery documents) β seeterraform/pki/README.md.
ArgoCD / Atlantis authentication
Atlantis reaches each cluster's API through a kubeconfig its
kubeconfig-hook.shpre-workflow hook writes from the checked-out repo: API-server host and port from that site'scluster-topology.json, credentials from its projected ServiceAccount token, and the trust anchor fromterraform/pki/certs/<cluster>-ca-bundle.pem. Reading the same bundlescripts/pki/post-rotate.shmaintains means a CA rotation reaches Atlantis with no second copy to update. A cluster with no bundle falls back to its ownclusters/<site>/config/kubernetes-ca.pem; both clusters carry one, and bothkubernetes-ca.pemfiles are symlinks onto it, so the hook resolves the same anchor whether it runs from the checked-out repo or from an older mounted ConfigMap, and the two can never disagree.terraform/argo/is the root module for theargocdTerraform provider (use_local_config = true: it authenticates through a local ArgoCD session rather than an explicit token in the module). Atlantis applies it like any other root. The token/session wiring is scoped and rotated, not a shared static credential β see Architecture/GitOps for the apply flow and the runbook it points to for rotation.
Linked references 6
Cloud & identity β terraform/gcp/organization/ (org-level IAM, folders, projects, billing), terraform/gcp/projects/<name>/ (one directory per GCP project), terraform/argo/ (the argocd provider wiring; it declares no resources today), terraform/google-workspace/ (users, groups, domains), and terraform/pki/ (the FML per-cluster K8s CAs and ServiceAccount token-signer certs, issued off a 1Password-held intermediate; each cluster's OIDC discovery document lives under terraform/pki/oidc/<cluster>/ and is served at oidc.lolwtf.ca β see Architecture/Secrets and PKI).
clusters/base/operator-rbac.yaml is the identity, applied to both clusters through Architecture/GitOps. See Architecture/Secrets and PKI for the certificate chain the break-glass user rides on.
SOPS-encrypted in-repo (clusters/**/*.sops.yaml), decrypted per-Kustomization via decryption.provider: sops β see Architecture/Secrets and PKI.
Architecture/Secrets and PKI β SOPS/age, OpenBao, and the cluster CAs
bosun and Spindrift hold the same private key on that App today β the operator's choice, not a code constraint: GitHub Apps support a distinct key per consumer, but this fleet rotates one PEM for both. That key lives in nix/secrets/bosun.sops.yaml, shared by every bosun host rather than duplicated per host; see Architecture/Secrets and PKI.
Secrets are SOPS-encrypted in the repository. This site is public β nothing decrypted ever lands here. See Architecture/Secrets and PKI.