fix(authentik): long-lived social-login sessions + shield auth from CrowdSec lockout
All checks were successful
ci/woodpecker/push/default Pipeline was successful

Viktor's passkeys all vanished and he was suddenly being asked to log in
multiple times a day instead of ~monthly. Root cause: on 2026-06-18 an ad-hoc
tripit passkey E2E test (run from the devvm as akadmin via python-httpx) cleaned
up "the demo user's" passkeys with GET /core/users/?search={demo} then DELETE
each device of users[0] — but the fuzzy search returned the REAL account, so it
wiped all 6 real passkeys. Losing passkeys forced fallback to Google login, and
the social-login stage (default-source-authentication-login) had the provider
default session_duration=seconds=0, which falls back to UNAUTHENTICATED_AGE=2h —
hence the constant re-logins. (Password + passkey logins were already weeks=4.)

Changes:
- authentik: adopt default-source-authentication-login into Terraform (import)
  and pin session_duration=weeks=4, so Google/GitHub/Facebook logins last as long
  as password/passkey. Immediate relief without re-enrolling.
- authentik: document the provider-schema gotcha — authentik_stage_identification
  exposes no webauthn_stage / enable_remember_me attribute, so they must NOT be in
  ignore_changes (commit 4e882989 removed them for this reason; re-adding breaks
  every apply). The passkey break was purely the missing device records, not drift.
- edge (rybbit): shield auth so a CrowdSec hit can never wall a user out of login —
  carve authentik.viktorbarzin.me + public-auth out of the zone WAF block rule,
  make the LAPI->edge sync ban-only (stop downgrading captcha to a hard block),
  and set exclude_crowdsec on the Authentik UI ingress (auth keeps rate-limiting).
- docs: record the session-duration change, the edge enforcement + auth carve-out
  (previously undocumented), and the pre-existing broken crowdsec-cf-sync CronJob
  (CF cursor pagination 400 + ~31k IPs vs list capacity -> edge list inert).

Passkey re-enrollment is a manual user action (devices are gone from the DB);
nothing auto-re-deletes them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-20 23:40:22 +00:00
parent 600f1f933c
commit 46166c63b2
6 changed files with 119 additions and 20 deletions

View file

@ -109,16 +109,25 @@ resource "cloudflare_ruleset" "crowdsec" {
# must exist before this ruleset is created/updated.
depends_on = [cloudflare_list.crowdsec_ban]
# CrowdSec ban block every IP in the single edge list. The sync writes BOTH
# ban and captcha decisions into crowdsec_ban (captcha downgraded to block at
# the edge) because the CF account allows only ONE Rules List.
# CrowdSec ban block every IP in the single edge list, EXCEPT on the
# Authentik auth hosts. The sync (lapi_kv_sync.py) now writes ONLY "ban"
# decisions into crowdsec_ban captcha is no longer downgraded to a hard
# block. The auth-host carve-out guarantees a CrowdSec hit can never wall a
# user out of the login / WebAuthn flow they authenticate through: without it,
# a false-positive ban would 403 the passkey ceremony + session-refresh XHRs
# on every proxied host, auth included. 2026-06-20.
rules {
action = "block"
expression = "(ip.src in $crowdsec_ban)"
description = "CrowdSec: block banned IPs"
expression = "(ip.src in $crowdsec_ban) and not (http.host in {\"authentik.viktorbarzin.me\" \"public-auth.viktorbarzin.me\"})"
description = "CrowdSec: block banned IPs (auth hosts carved out)"
enabled = true
}
# Pre-existing rule, imported and preserved verbatim (currently disabled).
# NOTE: Cloudflare auto-attaches logging{enabled=true} to skip rules. It must
# be declared here to match live, otherwise editing the OTHER rule re-sends
# this one too and the v4 provider errors "Provider produced inconsistent
# result after apply: .rules[1].logging block count changed from 0 to 1"
# (hit 2026-06-20 when adding the auth-host carve-out above).
rules {
action = "skip"
expression = "(http.host contains \"viktorbarzin.me\")"
@ -129,6 +138,9 @@ resource "cloudflare_ruleset" "crowdsec" {
products = ["uaBlock", "bic", "hot", "securityLevel", "rateLimit", "waf", "zoneLockdown"]
ruleset = "current"
}
logging {
enabled = true
}
}
}