Skip to content

fix(identity): stop an idle hour from breaking gated pages - #418

Merged
PBernaerts merged 1 commit into
mainfrom
fix/authentik-idle-session
Aug 21, 2026
Merged

fix(identity): stop an idle hour from breaking gated pages#418
PBernaerts merged 1 commit into
mainfrom
fix/authentik-idle-session

Conversation

@PBernaerts

Copy link
Copy Markdown
Owner

What broke

Leave a gated tab alone for an hour, come back, and the page loads with its
icons missing until you reload a second time. Seen on the MTG lobby today; the
landing page and the music library are set up the same way.

Why

The three gates run on authentik's default access token of one hour, which
50-proxy-gates.yaml never pinned. That is a regression against the Authelia
sessions they replaced, which lasted a week idle.

Once the token is gone the outpost does not refresh in the background. The next
request gets a full OAuth round trip, and the browser asks for the page and
every icon at the same moment
, so each unauthenticated one starts its own
round trip and the callbacks overwrite each other's single state cookie. Only
the last one can match. The rest fail with oauth state does not match the session and render as broken assets. The reload works because by then a
session exists.

Evidence: ten of those warnings at 13:49:16, one per asset, naming exactly the
icons that went missing (/icons/sections/play.png, decks.png, draft.png,
online.png, settings.png, apple-touch-icon.png, /favicon-16x16.png,
/logo.webp, /logo_only.webp, /wordmark.webp).

Nobody is prompted for a password anywhere in this, which is why it reads as
breakage rather than as a login, and it is invisible to curl, which asks for
one thing at a time.

What this changes

  • All three gates pin access_token_validity: hours=24. Nobody meets a day
    over lunch or a meeting, and a revoked account still loses these gates within
    a day, because the outpost answers from its own session store instead of
    asking authentik per request. Raising it further weakens that.
  • The MTG lobby's icons and brand marks are served ahead of the gate, so
    they cannot join the race on the loads that do still expire. Branding, not
    secrets: the same reasoning that already exempts the apex brand assets for
    mail clients. Written as a handle block rather than a not path matcher on
    the gate, because an exempt path that does not exist has to 404 there, where
    falling through to the SPA fallback would serve index.html to anyone asking
    for /icons/anything.png with no session.
  • Both are pinned by tests, including the ordering that makes them work (a
    handle after the gate exempts nothing).
  • docs/authentik-migration.md records this as 6f evidence item 4, answered by
    observation. The compressed-clock experiment it describes is no longer needed
    to unblock 6f, and its "the blueprint does not pin this field" note is
    reversed, because now it does.

Checks

  • tests/run.sh: 0 failing tests.
  • Both site renders (Authelia default and all-gates-authentik) validated with
    the pinned caddy:2.11.4-alpine image: Valid configuration.
  • pre-commit run --all-files: all hooks pass.

Not deployed. Note for whoever deploys: existing sessions keep their old
one-hour expiry (upstream #12751), so the first day after this applies still
behaves like the old value.

Conflicts

Touches web/caddy/Caddyfile.j2 and docs/authentik-migration.md, which
PR #417 (mon-1, Caddy access logging) also touches. Whichever merges second
needs a rebase; the changes are in different parts of both files.

Leaving a gated tab alone for an hour and coming back showed a page with
its icons missing until a second reload. It happened on the MTG lobby, and
it would happen on the landing page and the music library the same way.

The gates run on authentik's default access token of one hour, which
50-proxy-gates.yaml never pinned. That is a regression against the Authelia
sessions they replaced, which lasted a week idle. Once the token is gone the
outpost does not refresh in the background: the next request gets a full
OAuth round trip, and the browser asks for the page and every icon at the
same moment, so each unauthenticated one starts its own round trip and the
callbacks overwrite each other's state cookie. Only the last can match. The
rest fail with "oauth state does not match the session" and render broken.
The reload works because by then a session exists. Nobody is prompted for a
password in any of it, which is why it reads as breakage and not as a login.

All three gates now pin a one-day session. Nobody meets a day over lunch or
a meeting, and a revoked account still loses these gates within a day,
because the outpost answers from its own session store instead of asking
authentik per request.

The MTG lobby's icons and brand marks are also served ahead of the gate, so
they cannot join the race on the loads that do still expire. They are
branding, not secrets, which is the same reasoning that already exempts the
apex brand assets for mail clients. It is a handle block rather than a
matcher on the gate itself: an exempt path that does not exist has to 404
there, where falling through to the SPA fallback would serve index.html to
anyone asking for /icons/anything.png without a session.

Both changes are pinned by tests, including the ordering that makes them
work, and the migration doc records this as evidence item 4 - answered by
observation, so the compressed-clock experiment it describes is no longer
needed to unblock 6f.
@PBernaerts
PBernaerts force-pushed the fix/authentik-idle-session branch from d98db14 to 21d05d4 Compare August 21, 2026 12:20
@PBernaerts
PBernaerts merged commit 1ea0039 into main Aug 21, 2026
9 checks passed
@PBernaerts
PBernaerts deleted the fix/authentik-idle-session branch August 21, 2026 12:30
@PBernaerts

Copy link
Copy Markdown
Owner Author

Deployed and verified 2026-08-21. /opt/homelab at 1ea0039, deploy.yml ok=66 changed=4 failed=0, Caddy reloaded rather than restarted.

Proven from an anonymous client:

Check Result
mtg/icons/sections/play.png, no session 200, image/png, 605827 bytes
mtg/icons/nope.png 404, not the SPA shell
mtg/ and mtg/decks 302 into the login flow
apex, music web UI 302
music /rest/ping.view 200
Postgres, all three providers access_token_validity hours=24, refresh days=30

That 404 is the check worth keeping: it is the failure a not path matcher on the gate would have produced, and it would have looked correct in every other assertion here.

Read the timing before reading any graph. Sessions that already existed at deploy time keep their original one-hour expiry (upstream #12751), so the behaviour this PR fixes can still occur for about a day. A quiet evening is not the fix working. With #417's Caddy logging now live, the honest before/after is {container="caddy"} |= "handled request" | json | request_host="mtg.<domain>", status="302" on the MTG host, read no earlier than 2026-08-22.

One interaction with #417 worth recording: the exempted assets terminate before the gate, so they are 200s carrying no identity headers and the Alloy stage drops them. Their absence from Loki is the exemption working, not a gap in the logging.

PBernaerts added a commit that referenced this pull request Aug 21, 2026
…00 (#421)

mon-1 made the three gated hosts log every request and had Alloy drop the
2xx and 304 on the way to Loki, so what reaches Loki is only the refusals.
That is right everywhere except one path. When a gate session expires
mid-page-load the browser re-requests the page and every asset at once,
each starts its own OAuth round trip, and the parallel callbacks overwrite
each other's state cookie. Caddy answers 200 to the failures too, so the
drop discarded exactly the requests that broke, and the only record left
was the outpost's own "oauth state does not match the session". Diagnosing
#418 had to be done from that log because the access log could not help.

/outpost.goauthentik.io/ is now exempt from the status drop, whatever it
returns. It is the auth handshake itself, it is only touched on a login
round trip, so it is not the firehose the drop exists to avoid, and it is
the one place where a 200 is not evidence that anything worked. The drop
is not widened anywhere else.

The exemption is a line filter on the selector, because Alloy matches with
Go RE2 and RE2 has no negative lookahead: "drop a 2xx unless the URI is an
outpost path" cannot be written as one expression. It is the regex form
anchored on the "uri" key rather than a plain substring test, because Caddy
logs the request headers too, so a bare substring filter would also exempt
any ordinary success whose Referer pointed at a handshake page.

The client-cancelled-stream drop moves to a block of its own. It carries no
URI, so the exemption is meaningless to it and sharing a filtered selector
would only make it look conditional on something it cannot depend on.

tests/test-alloy-log-drops.sh pins both directions: a failed callback that
must now be KEPT, and an asset whose Referer merely quotes the path, which
must still go. Its parser had to learn about line filters first, or it
would have reported a drop for a line the running pipeline keeps.
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