diff --git a/infrastructure/production/fleet/authentik/README.md b/infrastructure/production/fleet/authentik/README.md index 85406f8..8e762ee 100644 --- a/infrastructure/production/fleet/authentik/README.md +++ b/infrastructure/production/fleet/authentik/README.md @@ -36,14 +36,40 @@ kubectl -n dezky-auth get secret authentik-secret -o jsonpath='{.data.AUTHENTIK_ ``` Log in at https://auth.dezky.eu as **akadmin** / that password. -## TODO (mirror the rest of dev — not yet applied) -- **OIDC app blueprints.** Dev provisions the operator app + a - `dezky-platform-admins` access policy via a blueprint - (`infrastructure/docker-compose/configs/authentik/blueprints/`). For prod, - mount the equivalent at `/blueprints/custom` with **prod** redirect URLs - (`operator.dezky.eu`, `app.dezky.eu`) + the matching client IDs/secrets. - The portal OIDC client (hand-made in dev) also needs creating. -- **Rebrand** ("Powered by authentik" → "Powered by Dezky"): dev runs - `rebrand-web.sh` as a root lifecycle script. Replicating in k8s needs a - root-capable initContainer/command override — cosmetic, deferred. +## Blueprints + branding (APPLIED) + +`blueprints/` holds prod blueprints (applied & `successful` on node1): +- `brand.yaml` — dezky branding on the default brand (title + signal-green + custom CSS). **This is what puts the login page in dezky colors.** +- `portal-application.yaml` — `dezky-portal` OIDC app/provider + (`https://app.dezky.eu/api/auth/callback`). +- `operator-application.yaml` — `dezky-operator` OIDC app/provider + (`https://operator.dezky.eu/auth/oidc/callback`) + `dezky-platform-admins` + group + an access policy restricting operator login to that group. + +Client secrets live in `authentik-secret` (`PORTAL_OIDC_CLIENT_SECRET`, +`OPERATOR_OIDC_CLIENT_SECRET`) — the apps must reuse the SAME values. + +### Applying them (two gotchas, both handled) +1. **`invalidation_flow` is REQUIRED** on OAuth2 providers in Authentik 2026.5 + (dev's 2025.10 didn't need it) — both providers set it via `!Find`. +2. **ConfigMap mounts present files as symlinks**, which Authentik's discovery + won't read. So the worker uses an **initContainer that copies the ConfigMap + into an emptyDir as real files** at `/blueprints/custom`: + ```bash + kubectl -n dezky-auth create configmap authentik-blueprints \ + --from-file=blueprints/ --dry-run=client -o yaml | kubectl apply -f - + # patch worker: add bp-src(configMap) + bp-cust(emptyDir) + initContainer + # `cp -L /bp-src/*.yaml /bp-cust/`, mount bp-cust at /blueprints/custom + kubectl -n dezky-auth rollout restart deploy/authentik-worker + # apply each (or let discovery): ak apply_blueprint custom/.yaml + ``` + > The chart's `worker.volumes` value did NOT take effect on this chart + > version, hence the direct Deployment patch. **Caveat:** a helm upgrade of + > Authentik reverts the patch — re-apply it (move it into a custom image or a + > post-render kustomize patch to make it durable). TODO. + +## Still deferred +- **Rebrand** of the "Powered by authentik" string (web-bundle `sed`, needs a + root lifecycle override) — cosmetic; the *colors* are done via the brand CSS. - Pin the **chart version** (currently latest → app `2026.5.2`). diff --git a/infrastructure/production/fleet/authentik/blueprints/brand.yaml b/infrastructure/production/fleet/authentik/blueprints/brand.yaml new file mode 100644 index 0000000..ef51dbd --- /dev/null +++ b/infrastructure/production/fleet/authentik/blueprints/brand.yaml @@ -0,0 +1,39 @@ +# dezky branding for the default Authentik brand — title + signal-green accent. +# (The "Powered by authentik" string lives in the web bundle and needs the +# separate rebrand-web patch; this covers the colors/title that custom CSS can +# reach.) +version: 1 +metadata: + name: dezky-brand + labels: + blueprints.goauthentik.io/instantiate: "true" +entries: + - model: authentik_brands.brand + state: present + identifiers: + domain: authentik-default + attrs: + branding_title: dezky + branding_custom_css: | + :root, :host { + --ak-accent: #D4FF3A; + --ak-accent-secondary: #b8e020; + --pf-t--global--color--brand--default: #D4FF3A; + --pf-v5-global--primary-color--100: #D4FF3A; + --pf-v5-global--primary-color--200: #b8e020; + --pf-global--primary-color--100: #D4FF3A; + --pf-global--primary-color--200: #b8e020; + --pf-global--link--Color: #D4FF3A; + --pf-global--active-color--100: #D4FF3A; + } + .pf-c-button.pf-m-primary, + .pf-v5-c-button.pf-m-primary, + .pf-v6-c-button.pf-m-primary { + background-color: #D4FF3A !important; + color: #0A0A0A !important; + border-color: #D4FF3A !important; + } + a, .pf-c-button.pf-m-link, .pf-v5-c-button.pf-m-link { + color: #D4FF3A !important; + } + .pf-c-login__main, .ak-login-container { border-top: 3px solid #D4FF3A; } diff --git a/infrastructure/production/fleet/authentik/blueprints/operator-application.yaml b/infrastructure/production/fleet/authentik/blueprints/operator-application.yaml new file mode 100644 index 0000000..ecbb368 --- /dev/null +++ b/infrastructure/production/fleet/authentik/blueprints/operator-application.yaml @@ -0,0 +1,77 @@ +# Prod operator OIDC application + dezky-platform-admins access policy. +# Mirrors infrastructure/docker-compose/configs/authentik/blueprints/ +# operator-application.yaml, with .local → .eu URLs. Applied by the +# authentik-worker (mounts /blueprints/custom; reads OPERATOR_OIDC_* from env). +# +# Provider/app are state:created (never clobber a hand-made live provider); +# group/policy/binding are state:present (reconcile + enforce on every env). +version: 1 +metadata: + name: dezky-operator-application + labels: + blueprints.goauthentik.io/instantiate: "true" + +entries: + - model: authentik_core.group + state: present + identifiers: + name: dezky-platform-admins + attrs: + name: dezky-platform-admins + + - id: operator-oauth2-provider + model: authentik_providers_oauth2.oauth2provider + state: created + identifiers: + client_id: !Env [OPERATOR_OIDC_CLIENT_ID, dezky-operator] + attrs: + name: dezky-operator + client_type: confidential + client_id: !Env [OPERATOR_OIDC_CLIENT_ID, dezky-operator] + client_secret: !Env OPERATOR_OIDC_CLIENT_SECRET + authorization_flow: + !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: + !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + signing_key: + !Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]] + redirect_uris: + - matching_mode: strict + url: https://operator.dezky.eu/auth/oidc/callback + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]] + sub_mode: hashed_user_id + issuer_mode: per_provider + + - id: operator-application + model: authentik_core.application + state: created + identifiers: + slug: dezky-operator + attrs: + name: Dezky Operator + slug: dezky-operator + provider: !KeyOf operator-oauth2-provider + meta_launch_url: https://operator.dezky.eu + meta_description: Internal Dezky operator control plane. Platform admins only. + + - id: operator-require-platform-admin + model: authentik_policies_expression.expressionpolicy + state: present + identifiers: + name: operator-require-platform-admin + attrs: + name: operator-require-platform-admin + expression: | + return ak_is_group_member(request.user, name="dezky-platform-admins") + + - model: authentik_policies.policybinding + state: present + identifiers: + target: !KeyOf operator-application + policy: !KeyOf operator-require-platform-admin + attrs: + enabled: true + order: 0 diff --git a/infrastructure/production/fleet/authentik/blueprints/portal-application.yaml b/infrastructure/production/fleet/authentik/blueprints/portal-application.yaml new file mode 100644 index 0000000..4e63348 --- /dev/null +++ b/infrastructure/production/fleet/authentik/blueprints/portal-application.yaml @@ -0,0 +1,53 @@ +# Prod customer-portal OIDC application. In dev this provider was made by hand +# (docs/AUTHENTIK-SETUP.md §3.3); captured here as code for prod. Same shape as +# the operator provider (implicit-consent flow, self-signed signing key, +# openid/email/profile, hashed sub, per-provider issuer) but open to ALL users +# (no platform-admin policy) and with the portal's redirect URI. +# +# state:created so a hand-made live provider is never clobbered. The +# authentik-worker reads PORTAL_OIDC_CLIENT_SECRET from env; the SAME secret +# must be given to the portal app (portal-secrets.NUXT_OIDC_CLIENT_SECRET). +version: 1 +metadata: + name: dezky-portal-application + labels: + blueprints.goauthentik.io/instantiate: "true" + +entries: + - id: portal-oauth2-provider + model: authentik_providers_oauth2.oauth2provider + state: created + identifiers: + client_id: !Env [PORTAL_OIDC_CLIENT_ID, dezky-portal] + attrs: + name: dezky-portal + client_type: confidential + client_id: !Env [PORTAL_OIDC_CLIENT_ID, dezky-portal] + client_secret: !Env PORTAL_OIDC_CLIENT_SECRET + authorization_flow: + !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: + !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + signing_key: + !Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]] + redirect_uris: + - matching_mode: strict + url: https://app.dezky.eu/api/auth/callback + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]] + - !Find [authentik_providers_oauth2.scopemapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]] + sub_mode: hashed_user_id + issuer_mode: per_provider + + - id: portal-application + model: authentik_core.application + state: created + identifiers: + slug: dezky-portal + attrs: + name: Dezky Portal + slug: dezky-portal + provider: !KeyOf portal-oauth2-provider + meta_launch_url: https://app.dezky.eu + meta_description: Your dezky workspace — mail, files, calendar and more. diff --git a/infrastructure/production/fleet/authentik/helmchart.yaml b/infrastructure/production/fleet/authentik/helmchart.yaml index 0d2716e..3eade5a 100644 --- a/infrastructure/production/fleet/authentik/helmchart.yaml +++ b/infrastructure/production/fleet/authentik/helmchart.yaml @@ -53,3 +53,8 @@ spec: - hosts: - auth.dezky.eu secretName: authentik-tls + # NOTE: blueprints are mounted via a post-install initContainer patch on the + # worker Deployment (this chart version ignored worker.volumes here) — it + # copies the 'authentik-blueprints' ConfigMap into an emptyDir as real files + # at /blueprints/custom. See README "Blueprints + branding". Client secrets + # come from authentik-secret (PORTAL_OIDC_CLIENT_SECRET / OPERATOR_OIDC_CLIENT_SECRET). diff --git a/infrastructure/production/fleet/authentik/values.yaml b/infrastructure/production/fleet/authentik/values.yaml index 7740b74..2fee60d 100644 --- a/infrastructure/production/fleet/authentik/values.yaml +++ b/infrastructure/production/fleet/authentik/values.yaml @@ -53,3 +53,8 @@ server: - hosts: - auth.dezky.eu secretName: authentik-tls + +# Blueprints (portal + operator OIDC apps + brand) are mounted via a post-install +# initContainer patch on the worker (this chart version ignored worker.volumes), +# copying the authentik-blueprints ConfigMap to an emptyDir as real files at +# /blueprints/custom. See README "Blueprints + branding".