diff --git a/BACKLOG.md b/BACKLOG.md index 7a02cc0..09ebfb8 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1583,10 +1583,64 @@ with rules, and it is right; its step 3 does separate "the standalone Pis" as th class, so rpi5 remains the correct place to start *enforcing* — but discover-then-enforce applies to one host as much as to twelve. -### 2.7 Committed argon2id hashes for five OIDC client secrets +### 2.7 ~~Committed argon2id hashes for five OIDC client secrets~~ — **DOWNGRADED 2026-09-06 on measurement; one real defect found instead** `gitops/workloads/authelia/configmap.yaml:72,86,98,111,125` -Offline-crackable, and the only credential for those clients. +Original text, preserved: *"Offline-crackable, and the only credential for those +clients."* **Both halves are wrong**, and the entry was ranked Tier 1 on that basis +earlier the same day. + +**"Offline-crackable" — no.** The committed values are argon2id hashes at +`m=65536,t=3,p=4`, and the plaintexts behind them are 256-bit random strings. Measured +without reading any value, by length alone: + + immich secret/lab/immich oidc-client-secret len=64 hex + minio secret/lab/minio oidc-client-secret len=64 hex + semaphore secret/lab/semaphore oidc-client-secret len=64 hex + argocd secret/lab/argocd oidc-client-secret len=64 hex + grafana secret/lab/grafana oidc-client-secret len=44 base64 + +64 hex chars is `openssl rand -hex 32`; 44 base64 chars is `openssl rand -base64 32`. +Both are 256 bits — `minio/externalsecret.yaml` even documents the command. Cracking a +64 MiB-cost argon2id hash of a 256-bit random string is not hard, it is arithmetically +out of reach. + +**"The only credential" — no.** The plaintexts are in Vault under access control; the +committed value is the digest. **Storing the hash in config is Authelia's documented +design** — that is what hashing it is for. What the hashes actually leak is which +clients exist and the KDF parameters. + +**Not migrating them, deliberately.** Moving the hashes into Vault would need Authelia's +config-templating filter, adding a dependency and a failure mode where a templating +mistake breaks SSO — including access to Grafana and ArgoCD, the tools you would use to +diagnose it. Real cost, no meaningful gain. + +--- + +**THE ONE REAL DEFECT, found while checking the above.** `authelia/external-secret.yaml` +said: + +> *"The plaintext lives only in Vault at `secret/lab/authelia/immich_client_secret`"* + +**It has never been there.** `secret/lab/authelia` holds seven keys and that is not one +of them. The immich plaintext is at `secret/lab/immich` → `oidc-client-secret`, following +the same convention as the other four. + +That comment is a **recovery instruction**, which is what makes it worth fixing: anyone +needing to re-enter that secret would have checked the documented path, found nothing, +and reasonably concluded the credential was lost. Corrected, with all five paths and +their measured lengths recorded in one place. + +Immich is the only client with no ExternalSecret for its OIDC secret, and that is correct +rather than an omission — Immich keeps OIDC settings in its own Postgres, entered through +its admin UI, so there is no k8s Secret for ESO to populate. The plaintext exists in Vault +*and* in the Immich database, which is backed up nightly. + +**Pattern worth noting.** This is the third entry in two days to overstate its own +severity — §2.2 turned out to be a deletion rather than a redesign, §2.14's dnsmasq had +served zero queries in five days, and this one is a hash doing exactly what hashes are +for. All three were written from reading the repo. All three corrections came from +measuring the running system. ### 2.8 Optional: tighten `admin` so it cannot self-escalate `ansible/files/vault-policies/admin.hcl`, `TODO-2026-08-03.md:310` diff --git a/gitops/workloads/authelia/external-secret.yaml b/gitops/workloads/authelia/external-secret.yaml index 80390c6..07d0528 100644 --- a/gitops/workloads/authelia/external-secret.yaml +++ b/gitops/workloads/authelia/external-secret.yaml @@ -21,10 +21,35 @@ # If rotating this key, existing OIDC sessions (Immich) will be invalidated — # users will need to re-authenticate after the restart. # -# NOTE: immich_client_secret is no longer stored here — the Immich OIDC client -# secret is now committed as an argon2id hash directly in the Authelia configmap. -# The plaintext lives only in Vault at secret/lab/authelia/immich_client_secret -# (informational; Authelia reads the hash from the ConfigMap, not this secret). +# NOTE: immich_client_secret is not stored here. Authelia reads the argon2id HASH +# from the ConfigMap; the plaintext is the client's problem, not Authelia's. +# +# CORRECTED 2026-09-06 — this used to say "the plaintext lives only in Vault at +# secret/lab/authelia/immich_client_secret". IT HAS NEVER BEEN THERE. That path does +# not exist; `vault kv get secret/lab/authelia` returns jwt_secret, ldap_password, +# oidc_hmac_secret, oidc_private_key, session_secret, storage_encryption_key and +# storage_pg_password, and nothing else. +# +# This mattered because it is a RECOVERY INSTRUCTION. Anyone needing to re-enter that +# secret would have looked at the documented path, found nothing, and reasonably +# concluded the credential was unrecoverable. +# +# ALL FIVE OIDC CLIENT PLAINTEXTS FOLLOW ONE CONVENTION — secret/lab/: +# +# immich secret/lab/immich -> oidc-client-secret (len 64, hex) +# minio secret/lab/minio -> oidc-client-secret (len 64, hex) +# semaphore secret/lab/semaphore -> oidc-client-secret (len 64, hex) +# argocd secret/lab/argocd -> oidc-client-secret (len 64, hex) +# grafana secret/lab/grafana -> oidc-client-secret (len 44, base64) +# +# Lengths measured 2026-09-06 without reading the values: 64 hex chars is +# `openssl rand -hex 32` and 44 base64 chars is `openssl rand -base64 32` — both 256 +# bits. See BACKLOG §2.7 for why that makes the committed hashes a non-issue. +# +# Immich is the one client with no ExternalSecret for it, and that is correct rather +# than an omission: Immich stores OIDC settings in its own Postgres, entered through +# its admin UI, so there is no k8s Secret for ESO to populate. The plaintext therefore +# exists in Vault AND in the Immich database, which is backed up nightly. --- apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret