Skip to content

feat(authentik): blueprints-as-code baseline + drafted MFA (phase 1, not applied)#2014

Merged
thaynes43 merged 1 commit into
mainfrom
feat/authentik-blueprints
Jul 10, 2026
Merged

feat(authentik): blueprints-as-code baseline + drafted MFA (phase 1, not applied)#2014
thaynes43 merged 1 commit into
mainfrom
feat/authentik-blueprints

Conversation

@thaynes43

Copy link
Copy Markdown
Owner

PLAN-011 Phase 1 — read-only. Everything here was authored from a GET-only
export of live Authentik; zero POST/PUT/PATCH/DELETE were issued. Nothing is
applied — Flux reconciles main only, and the MFA blueprint is excluded from
discovery. Do not merge until the owner-present Phase 2 session.

What this is

Turns the haynesnetwork Authentik customizations into config-as-code and drafts
native-account MFA on top, per PLAN-011.

  • Baseline blueprints (discovered, drift-zero) reproduce the live 2026.5.3 estate,
    so applying them changes nothing observable:
    File Concern
    app/blueprints/10-hnet-brand.yaml Default brand — title, logo/favicon/background media paths, full ~70 KB brand CSS
    app/blueprints/20-hnet-flows.yaml The 4 login-surface flows + stages + bindings + flow policies
    app/blueprints/30-hnet-sources.yaml The Plex OAuth source (non-secret fields; plex_token omitted)
  • Drafted MFA (NOT discovered): app/blueprints/pending/40-hnet-mfa.yaml — WebAuthn +
    TOTP validation/enrollment after the password stage, an mfa-exempt group
    (hnet-e2e, hnet-e2e-member) and a fail-closed skip policy.
  • exports/ — the sanitized read-only API snapshot (secrets → 1P://… refs) the
    blueprints were authored from.

Delivery (supported chart mechanism)

app/kustomization.yaml generates ConfigMap authentik-hnet-blueprints
(disableNameSuffixHash: true) from the three baseline files; app/helmrelease.yaml
adds values.blueprints.configMaps: [authentik-hnet-blueprints]. The goauthentik chart
mounts it onto the worker at /blueprints/mounted/cm-authentik-hnet-blueprints, where
only *.yaml keys are discovered. The pending/ MFA file is deliberately not in the
generator, so it is never mounted. Verified offline:

  • kubectl kustomize app/ → ConfigMap with keys 10/20/30-hnet-*.yaml (no MFA), stable name.
  • helm template (chart 2026.5.3, blueprints.configMaps set) → worker gets
    volume/volumeMount blueprints-cm-authentik-hnet-blueprints → /blueprints/mounted/cm-authentik-hnet-blueprints.

Exported inventory (names + pks)

Brand (1): default authentik-default uuid de1b7109-2d4d-466c-8890-326e969015d5, title haynesnetwork.

Flows (branded, 4 of 14):

slug pk designation title
default-authentication-flow c8e7b494-…bac40 authentication Sign in to haynesnetwork
default-source-authentication 67b51e33-…4dc5bc→flow 67b51e33-…a6d9 authentication Sign in to haynesnetwork
default-source-enrollment f80cd9b5-…1198d enrollment Welcome — choose your username
default-invalidation-flow 8ba7e7a7-…602c62 invalidation Default Invalidation Flow

Key stages: identification default-authentication-identification 08af63fb-…2772f;
password default-authentication-password abf918bc-…e40fde; authenticator-validate
default-authentication-mfa-validation 3cdaa8fa-…a1c88; login
default-authentication-login ae85d3ba-…fb69c; source login
default-source-authentication-login d81d77d2-…dc5bc.

Flow policies: default-authentication-flow-password-stage c2c42fe2-…f5f0 (skip
password if a backend is already attached), default-authentication-flow-authenticator-validate-stage
38f627a5-…cffe6 (skip MFA if webauthn-passwordless), default-source-authentication-if-sso
5d457f9a-…1ab5e (return ak_is_sso_flow).

Groups (3): authentik Admins 3a1337de-…e084 (superuser; akadmin+thaynes), authentik Read-only 0a15001d-…3180, grafana_admin ffd4d3b4-…1a63. No mfa-exempt yet (this PR drafts it).

Source (1): Plex HaynesTower slug haynestower 2c61a5af-…c22891promoted: true,
authentication_flow: 67b51e33 (default-source-authentication), enrollment_flow: f80cd9b5
(default-source-enrollment), user_matching_mode: identifier.

Provider/application (deferred — Q-11): Provider for haynesnetwork pk 109
(confidential, grant_types [authorization_code, refresh_token], include_claims_in_id_token),
application slug haynesnetwork b427e696-…b7355. Exported+sanitized under exports/, not blueprinted.

Q-02 — are Plex-source logins structurally outside the password + MFA path?

Yes. Verified against the exported flow graph:

  • default-authentication-flow (the native path) stage-binding order: 10 identification →
    20 password (gated by …password-stage policy) → 30 authenticator-validation
    (default-authentication-mfa-validation, gated by …authenticator-validate-stage policy) →
    100 login. This is the ONLY flow containing an authenticator-validation stage.
  • The Plex source routes existing users to authentication_flow = default-source-authentication
    and new users to enrollment_flow = default-source-enrollment.
  • default-source-authentication has exactly one stage-binding — order 0
    default-source-authentication-login (user-login). No password stage, no
    authenticator-validation stage.
    (default-source-enrollment = prompt → user-write
    (user_type: external) → login; also no password/MFA.)
  • Therefore a "Log in with Plex" user never enters default-authentication-flow and never
    reaches the order-30 MFA stage. The drafted MFA blueprint modifies only
    default-authentication-flow's order-30 stage + binding — nothing in the source flows.

Caveat for Phase 2: MFA gates the username+password path. A native account that signs
in via the Plex button also bypasses MFA (that is the source path) — consistent with owner
requirement #2 ("Log in with Plex accounts pass through untouched").

Drift vs the branding apply-seed

Essentially none. All brand media fields (hnet/logo-%(theme)s.svg, hnet/favicon.png,
hnet/bg-c-%(theme)s.svg), the title, and all three flow titles match
docs/ops/authentik-apply-seed exactly. The only difference is a single trailing
newline in the brand custom CSS (Authentik strips it on save); the committed blueprint uses
the live value, so a merge is truly drift-zero.

Phase 2 — apply / rollback (owner-present)

CTX=haynes-ops ; NS=network
# --- BASELINE (drift-zero; merging changes nothing) ---
# 1. merge main; Flux builds ConfigMap authentik-hnet-blueprints and the worker discovers it
kubectl --context $CTX -n $NS exec deploy/authentik-worker -c worker -- ls /blueprints/mounted/cm-authentik-hnet-blueprints
kubectl --context $CTX -n $NS logs deploy/authentik-worker -c worker --tail=200 | grep -i blueprint   # status=successful, no diff
# rollback baseline = revert the merge commit (drift-zero, so nothing live changes)

# --- MFA (only after the baseline is confirmed) ---
# 2. in this repo: remove the mfa-validation stage + order-30 flowstagebinding entries from
#    20-hnet-flows.yaml; `git mv pending/40-hnet-mfa.yaml 40-hnet-mfa.yaml`; add it to the
#    configMapGenerator.files in app/kustomization.yaml. Merge.
# 3. prove on a THROWAWAY native test account (enroll+login+recovery) BEFORE it covers real users
# 4. owner enrolls thaynes: 1Password passkey (WebAuthn) + backup TOTP; verify login
# 5. verify: a Plex-source login sees NO challenge; hnet-e2e Playwright stays green
# MFA rollback (fast, live): ak shell / API — set stage not_configured_action back to `skip`
#   or delete the exempt policy binding; then revert the promotion commit.

Full runbooks: app/blueprints/README.md and app/blueprints/pending/README.md.
Content rollback to stock Authentik remains the API payloads in
haynesnetwork:docs/ops/authentik-apply-seed/.

Carried open questions

  • Q-11: adopt the OIDC provider 109 + application into GitOps too, or stay
    brand/flows/source-only? (Provider secrets stay in 1P either way.)
  • Q-05 / Q-10: akadmin repair — stale AUTHENTIK_BOOTSTRAP_PASSWORD; rotate via ak
    shell in Phase 2 (keep as break-glass with MFA, or disable interactive login).

…not applied)

PLAN-011 Phase 1 (agent-safe, read-only against live). Reproduces the live
Authentik estate (2026.5.3) as declarative blueprints so brand/flows/source
become config-as-code, and drafts native-account MFA without applying anything.

Delivery: app/kustomization.yaml bundles the baseline blueprints into ConfigMap
authentik-hnet-blueprints (stable name); helmrelease.yaml lists it under
values.blueprints.configMaps, so the chart mounts it onto the worker at
/blueprints/mounted/cm-authentik-hnet-blueprints (verified via helm template).

Baseline (discovered, drift-zero — only delta vs the branding apply-seed was a
single trailing newline the server strips):
- 10-hnet-brand.yaml    default brand: title, media paths, full ~70KB brand CSS
- 20-hnet-flows.yaml     4 login flows + stages + bindings + flow policies
- 30-hnet-sources.yaml   Plex OAuth source (plex_token omitted; stays in Authentik/1P)

Drafted, NOT discovered (excluded from configMapGenerator):
- pending/40-hnet-mfa.yaml   WebAuthn+TOTP validation/enroll after the password
  stage; mfa-exempt group (hnet-e2e, hnet-e2e-member) + fail-closed skip policy.

Q-02 (verified structurally against the exported flow graph): Plex-source logins
are OUTSIDE the password+MFA path — existing users authenticate via
default-source-authentication (a login-only flow, no password/MFA stage); the
MFA stage lives only at order 30 of default-authentication-flow, which source
logins never enter.

exports/ holds the sanitized read-only API snapshot (secrets -> 1P refs) the
blueprints were authored from. Nothing here is applied — Flux only reconciles main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU9cU9mSv19WMB64bMGP71
@github-actions

Copy link
Copy Markdown
Contributor

Flux main kustomization diff

--- kubernetes/main/apps/network/authentik/app Kustomization: network/authentik HelmRelease: network/authentik

+++ kubernetes/main/apps/network/authentik/app Kustomization: network/authentik HelmRelease: network/authentik

@@ -34,12 +34,15 @@

       log_level: debug
       outposts:
         container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
       redis:
         host: '{{ .Release.Name }}-redis-master'
         password: ''
+    blueprints:
+      configMaps:
+      - authentik-hnet-blueprints
     geoip:
       enabled: false
       existingSecret:
         accountId: MAXMIND_ACCOUNT_ID
         licenseKey: MAXMIND_LICENSE_KEY
         secretName: authentik-secret
--- kubernetes/main/apps/network/authentik/app Kustomization: network/authentik ConfigMap: network/authentik-hnet-blueprints

+++ kubernetes/main/apps/network/authentik/app Kustomization: network/authentik ConfigMap: network/authentik-hnet-blueprints

@@ -0,0 +1,379 @@

+---
+apiVersion: v1
+data:
+  10-hnet-brand.yaml: "# PLAN-011 Phase 1 \u2014 haynesnetwork Authentik brand (config-as-code\
+    \ baseline).\n# DRIFT-ZERO: reproduces the live default brand (uuid de1b7109-\u2026\
+    ) exactly as applied\n# 2026-07-09 (docs/ops/authentik-apply-seed). Media paths\
+    \ resolve to the read-only\n# ConfigMap 'authentik-brand-assets' mounted at /media/public/hnet\
+    \ on the server pods.\nversion: 1\nmetadata:\n  name: hnet-brand\n  labels:\n\
+    \    blueprints.goauthentik.io/description: haynesnetwork login brand (PLAN-011)\
+    \ \u2014 title, logo/favicon/background media paths, and the full brand custom\
+    \ CSS. Drift-zero reproduction of the live default brand.\ncontext: {}\nentries:\n\
+    - model: authentik_brands.brand\n  state: present\n  identifiers:\n    brand_uuid:\
+    \ de1b7109-2d4d-466c-8890-326e969015d5\n  attrs:\n    branding_title: haynesnetwork\n\
+    \    branding_logo: hnet/logo-%(theme)s.svg\n    branding_favicon: hnet/favicon.png\n\
+    \    branding_default_flow_background: hnet/bg-c-%(theme)s.svg\n    branding_custom_css:\
+    \ |-\n      /* Outfit variable (SIL OFL 1.1, vendored from apps/web/fonts) as\
+    \ a data URI so the\n         IdP page needs no cross-origin font fetch (CSP font-src\
+    \ allows data:). */\n      @font-face {\n        font-family: 'Outfit';\n    \
+    \    font-style: normal;\n        font-weight: 100 900;\n        font-display:\
[Diff truncated by flux-local]

@github-actions

Copy link
Copy Markdown
Contributor

Flux main helmrelease diff

--- HelmRelease: network/authentik Deployment: network/authentik-worker

+++ HelmRelease: network/authentik Deployment: network/authentik-worker

@@ -42,12 +42,15 @@

         env: null
         envFrom:
         - secretRef:
             name: authentik
         - secretRef:
             name: authentik-secret
+        volumeMounts:
+        - name: blueprints-cm-authentik-hnet-blueprints
+          mountPath: /blueprints/mounted/cm-authentik-hnet-blueprints
         ports:
         - name: http
           containerPort: 9000
           protocol: TCP
         - name: metrics
           containerPort: 9300
@@ -91,8 +94,12 @@

               labelSelector:
                 matchLabels:
                   app.kubernetes.io/name: authentik
                   app.kubernetes.io/instance: authentik
                   app.kubernetes.io/component: worker
               topologyKey: kubernetes.io/hostname
+      volumes:
+      - name: blueprints-cm-authentik-hnet-blueprints
+        configMap:
+          name: authentik-hnet-blueprints
       enableServiceLinks: true
 

@thaynes43 thaynes43 marked this pull request as ready for review July 10, 2026 13:04
@thaynes43 thaynes43 merged commit 3e0d8d2 into main Jul 10, 2026
9 checks passed
@thaynes43 thaynes43 deleted the feat/authentik-blueprints branch July 10, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant