diff --git a/.claude/reference/authentik-state.md b/.claude/reference/authentik-state.md index 2005bb09..f76dd325 100644 --- a/.claude/reference/authentik-state.md +++ b/.claude/reference/authentik-state.md @@ -119,3 +119,18 @@ Removed bindings from: - `default-source-authentication` (PK: via policybindingmodel `1a779f24`) — Google/GitHub/Facebook OAuth Policy still exists with 0 bindings. If brute-force protection is needed, bind to the **password stage** (not the flow level). + +## Session Duration (2026-05-01) + +Pinned via Terraform in `stacks/authentik/`: + +| Knob | Value | Surface | Effect | +|------|-------|---------|--------| +| `UserLoginStage.session_duration` on `default-authentication-login` | `weeks=4` | `authentik_stage_user_login.default_login` in `authentik_provider.tf` | Authenticated users stay logged in 4 weeks across browser restarts. No sliding refresh — resets on each login. | +| `AUTHENTIK_SESSIONS__UNAUTHENTICATED_AGE` (server + worker) | `hours=2` | `server.env` + `worker.env` in `modules/authentik/values.yaml` | Anonymous Django sessions (bots, healthcheckers, partial flows) are reaped within 2h instead of the 1d default. | + +Notes: +- There is **no** `Brand.session_duration`; `UserLoginStage` is the only correct lever for authenticated session lifetime. +- Embedded outpost session storage moved from `/dev/shm` → Postgres table `authentik_providers_proxy_proxysession` in authentik 2025.10. The 2026-04-18 `/dev/shm`-fill outage class is no longer load-bearing in 2026.2.2; the `unauthenticated_age` cap is still the right lever for anonymous-session bloat from external monitors. +- `ProxyProvider.access_token_validity` and `remember_me_offset` stay UI-managed via `ignore_changes`. +- The `unauthenticated_age` env var is injected via `server.env` / `worker.env` (not `authentik.sessions.unauthenticated_age`) because we set `authentik.existingSecret.secretName: goauthentik`, which makes the chart skip rendering its own `AUTHENTIK_*` Secret. The `authentik.*` value block is therefore inert in this stack — anything new under `authentik.*` must use the `*.env` arrays instead. The same applies to the existing `authentik.cache.*`, `authentik.web.*`, `authentik.worker.*` blocks (currently inert; live values come from the orphaned, helm-keep-policy `goauthentik` Secret created by chart 2025.10.3 before `existingSecret` was introduced). diff --git a/stacks/authentik/authentik_provider.tf b/stacks/authentik/authentik_provider.tf index e9db3985..f33a225a 100644 --- a/stacks/authentik/authentik_provider.tf +++ b/stacks/authentik/authentik_provider.tf @@ -57,3 +57,34 @@ resource "authentik_provider_proxy" "catchall" { ignore_changes = [property_mappings, jwt_federation_sources, skip_path_regex, internal_host, basic_auth_enabled, basic_auth_password_attribute, basic_auth_username_attribute, intercept_header_auth, access_token_validity] } } + +# ----------------------------------------------------------------------------- +# Default User Login stage — bound to default-authentication-flow. +# Adopted into Terraform 2026-05-01 to set session_duration=weeks=4 so users +# stay logged in across browser restarts. There is no Brand.session_duration +# in authentik 2026.2.x — UserLoginStage is the correct knob. +# ----------------------------------------------------------------------------- + +data "authentik_stage" "default_authentication_login" { + name = "default-authentication-login" +} + +import { + to = authentik_stage_user_login.default_login + id = data.authentik_stage.default_authentication_login.id +} + +resource "authentik_stage_user_login" "default_login" { + name = "default-authentication-login" + session_duration = "weeks=4" + lifecycle { + # Pin only session_duration; everything else stays UI-managed so the + # plan doesn't churn unrelated knobs (e.g. remember_me_offset toggles). + ignore_changes = [ + remember_me_offset, + terminate_other_sessions, + geoip_binding, + network_binding, + ] + } +} diff --git a/stacks/authentik/modules/authentik/values.yaml b/stacks/authentik/modules/authentik/values.yaml index e8c7d5ea..9822516c 100644 --- a/stacks/authentik/modules/authentik/values.yaml +++ b/stacks/authentik/modules/authentik/values.yaml @@ -37,6 +37,15 @@ authentik: server: replicas: 3 + # Anonymous Django sessions (no completed login: bots, healthcheckers, + # partial flows) expire in 2h. Default is days=1. Once login completes, + # UserLoginStage.session_duration takes over via request.session.set_expiry. + # Injected via server.env (not authentik.sessions.*) because we use + # authentik.existingSecret.secretName, which makes the chart skip + # rendering the AUTHENTIK_* secret — so the values block doesn't reach env. + env: + - name: AUTHENTIK_SESSIONS__UNAUTHENTICATED_AGE + value: "hours=2" strategy: type: RollingUpdate rollingUpdate: @@ -70,6 +79,11 @@ global: worker: replicas: 3 + # Same unauthenticated_age cap as server — both the server (Django session + # middleware) and worker (cleanup tasks) need to see the value. + env: + - name: AUTHENTIK_SESSIONS__UNAUTHENTICATED_AGE + value: "hours=2" strategy: type: RollingUpdate rollingUpdate: