From 49e3e311a83bf4c145ee336ca485f4ccbf72dedd Mon Sep 17 00:00:00 2001 From: swares Date: Sun, 6 Sep 2026 11:04:26 -0600 Subject: [PATCH] docs(2.7): downgrade on measurement; fix a recovery path that pointed at nothing The entry read 'offline-crackable, and the only credential for those clients'. Both halves are wrong, and it was ranked Tier 1 on that basis earlier the same day. Not offline-crackable: the committed values are argon2id hashes at m=65536,t=3,p=4 over 256-bit random plaintexts. Measured by length without reading any value -- four at len=64 (openssl rand -hex 32) and grafana at len=44 (openssl rand -base64 32). minio/externalsecret.yaml documents the command. Not the only credential either: the plaintexts are in Vault under access control and the committed value is the digest. Storing the hash in config is Authelia's documented design. Not migrating them: moving the hashes into Vault needs Authelia's config-templating filter, which adds 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: authelia/external-secret.yaml said the immich plaintext lives at secret/lab/authelia/immich_client_secret. It has never been there -- that path holds seven keys and this is not one. The secret is at secret/lab/immich -> oidc-client-secret, following the same convention as the other four. That comment is a recovery instruction, so anyone needing to re-enter the secret would have checked the documented path, found nothing, and reasonably concluded the credential was lost. Corrected, with all five paths and measured lengths recorded in one place. Immich having no ExternalSecret for its OIDC secret is correct rather than an omission: it keeps OIDC settings in its own Postgres via the 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. --- BACKLOG.md | 58 ++++++++++++++++++- .../workloads/authelia/external-secret.yaml | 33 +++++++++-- 2 files changed, 85 insertions(+), 6 deletions(-) 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