The guarded Audiobookshelf door, renamed and working
The vhost is audiobooks.franksdank.cloud (renamed here at Frank's request on
2026-11-03, before the abs. name was ever deployed — so no alias is left behind: repo,
LAN record, public record, Caddy matcher and the IdP rows all move together, and the old
name appears nowhere).
The abs-era vhost, its DNS, TLS and the Anubis/Authentik guard went live in #130. Signing
in on the guarded door did not: ABS's login page rendered, Sign in with Authentik
produced an authentik Redirect URI Error page, and after the redirect URIs were filled
by hand the same button produced a bodiless HTTP 400. The LAN door
(http://192.168.0.235:8000/audiobookshelf, what the phone app uses) kept working. Two
independent walls, both fixed here.
Wall 1 — the hand-built proxy provider had no OAuth defaults
audiobooks-proxy (then abs-proxy) was created with ProxyProvider.objects.create(...),
which is not the API: the REST serializer calls ProxyProvider.set_oauth_defaults(), the raw
ORM path does not. The row looked fully configured — external_host, internal_host,
application bound, embedded-outpost claim present — while redirect_uris was empty (those
are derived from external_host by _get_callback_url()), grant_types was [], and authorization_flow was NULL. Each breaks the login in a different, unrelated-looking way,
all before any credential is requested.
scripts/authentik-proxy-provider-defaults.sh fills them from the vendor's own set_oauth_defaults() plus default-provider-authorization-implicit-consent, idempotently,
for every proxy provider. --check is a census (exit 1 on drift, writes nothing).
Applied to the live IdP on 2026-11-03; the census afterwards:
$ scripts/authentik-proxy-provider-defaults.sh --check
ok embedded-outpost (ext https://www.franksdank.cloud)
ok search-proxy (ext https://search.franksdank.cloud)
ok prometheus-proxy (ext https://prometheus.franksdank.cloud)
ok romm-proxy (ext https://romm.franksdank.cloud)
ok files-proxy (ext https://files.franksdank.cloud)
ok loki-proxy (ext https://loki.franksdank.cloud/)
ok grafana-proxy (ext https://grafana.franksdank.cloud/)
ok seerr-proxy (ext https://seerr.franksdank.cloud/)
ok marreta-proxy (ext https://marreta.franksdank.cloud)
ok notebook-proxy (ext https://notebook.franksdank.cloud/)
ok abs-proxy (ext https://abs.franksdank.cloud/)
providers checked, drift: 0
Nothing else drifted, which is the point of the census: grafana-sso and the ten providers
created through the REST API already matched and were left alone. This is authentik DB
state — no deployable file carries it, so a blueprint re-import or an ak shell re-creation
reverts it and the symptom returns verbatim.
Wall 2 — Caddy lied to ABS about its own scheme
Audiobookshelf validates every OIDC callback against the origin it believes it is served
from: getRequestOrigin() is x-forwarded-proto://Host (dist-server/server/utils/requestUtils.js,
read inside the pinned image digest). The post-auth http://:8017 listener is fed over plain
http, so Caddy's own X-Forwarded-Proto is http, ABS computes http://<name> and rejects
its own https callback. Probed at the app, holding the callback fixed:
Request to 192.168.0.235:8000/audiobookshelf/auth/openid/callback?…
Answer
Host: <vhost>, no forwarded scheme
400 {"message":"Invalid callback URL - must be same-origin"}
So the fix is in the @audiobooks route on :8017: header_up X-Forwarded-Proto https. The
edge for a guarded name is TLS-only, so https is the truthful value, not a spoof. Same class
as the grafana-sso incident — the guard's view of the request is part of the app's contract.
The rename: one commit, because a test enforces the lockstep
audiobooks.franksdank.cloud replaces abs.franksdank.cloud in every place that names it,
in a single commit — the pihole seam fails the suite if a Caddyfile vhost has no addn-hosts record, so splitting it would leave a red commit:
dankclaw/Caddyfile — site block, matcher @abs → @audiobooks, guard-state comment
dankclaw/pihole/addn-hosts — the LAN record
scripts/authentik-audiobookshelf-sso.sh — ABS_EXTRA_BASES default, so the RP's
registered callback list is regenerated for the new origin (the script replaces the
list, so the old URI leaves the provider)
shared/homepage/services.json — the admin tile link
tests/e2e/specs/14-guard.spec.ts — GUARDED + the leak marker
the three vault pages that name the door
Verification
Check
Result
./tests/run.sh
green (tail in the comment thread)
docs seams after the last docs commit
6/6
specs/11-admin.spec.ts + specs/14-guard.spec.ts
18 passed (pre-rename: abs in the admin SSO walk and the guest census)
https://<vhost>/audiobookshelf/login → Anubis challenge, zero app bytes
23-audiobookshelf-sso.spec.ts (LAN origin)
passes
Porkbun rename, provider rename + defaults re-run, RP URI re-registration
run in the deploy window — [[LAN-DNS]]Porkbun zone ops order: zone edit first, then deploy, because the deploy's own e2e gate resolves the new name
In-app Sign in with Authentik on the guarded door
pending that window — the 302 above is the wall removed
Two earlier e2e failures on those spec files were ERR_NAME_NOT_RESOLVED — the workstation's systemd-resolved negative cache still held the pre-Porkbun NXDOMAIN. It expired on its own;
the same specs pass unmodified, so no test was changed to make the suite green.
Docs
One owner per fact: the provider-defaults law on Authentik Outpost Reference
(provisioning rule 3), the origin-check law on Audiobookshelf (Laws), and both as steps
in Guard a new vhost in Caddy, so the next guarded app doesn't re-hit either wall. The
rename procedure itself is already owned by LAN-DNSPorkbun zone ops — this PR follows
it, it does not restate it.
Built in the worktree /tmp/wt-absfix (branch fix/abs-guard-oauth-defaults, off origin/main).
# The guarded Audiobookshelf door, renamed and working
The vhost is **`audiobooks.franksdank.cloud`** (renamed here at Frank's request on
2026-11-03, before the `abs.` name was ever deployed — so no alias is left behind: repo,
LAN record, public record, Caddy matcher and the IdP rows all move together, and the old
name appears nowhere).
The `abs`-era vhost, its DNS, TLS and the Anubis/Authentik guard went live in #130. Signing
in **on the guarded door** did not: ABS's login page rendered, `Sign in with Authentik`
produced an authentik **`Redirect URI Error`** page, and after the redirect URIs were filled
by hand the same button produced a bodiless **HTTP 400**. The LAN door
(`http://192.168.0.235:8000/audiobookshelf`, what the phone app uses) kept working. Two
independent walls, both fixed here.
## Wall 1 — the hand-built proxy provider had no OAuth defaults
`audiobooks-proxy` (then `abs-proxy`) was created with `ProxyProvider.objects.create(...)`,
which is not the API: the REST serializer calls `ProxyProvider.set_oauth_defaults()`, the raw
ORM path does not. The row looked fully configured — `external_host`, `internal_host`,
application bound, embedded-outpost claim present — while `redirect_uris` was empty (those
are *derived* from `external_host` by `_get_callback_url()`), `grant_types` was `[]`, and
`authorization_flow` was NULL. Each breaks the login in a different, unrelated-looking way,
all before any credential is requested.
`scripts/authentik-proxy-provider-defaults.sh` fills them from the vendor's own
`set_oauth_defaults()` plus `default-provider-authorization-implicit-consent`, idempotently,
for **every** proxy provider. `--check` is a census (exit 1 on drift, writes nothing).
Applied to the live IdP on 2026-11-03; the census afterwards:
$ scripts/authentik-proxy-provider-defaults.sh --check
ok embedded-outpost (ext https://www.franksdank.cloud)
ok search-proxy (ext https://search.franksdank.cloud)
ok prometheus-proxy (ext https://prometheus.franksdank.cloud)
ok romm-proxy (ext https://romm.franksdank.cloud)
ok files-proxy (ext https://files.franksdank.cloud)
ok loki-proxy (ext https://loki.franksdank.cloud/)
ok grafana-proxy (ext https://grafana.franksdank.cloud/)
ok seerr-proxy (ext https://seerr.franksdank.cloud/)
ok marreta-proxy (ext https://marreta.franksdank.cloud)
ok notebook-proxy (ext https://notebook.franksdank.cloud/)
ok abs-proxy (ext https://abs.franksdank.cloud/)
providers checked, drift: 0
Nothing else drifted, which is the point of the census: `grafana-sso` and the ten providers
created through the REST API already matched and were left alone. This is authentik **DB**
state — no deployable file carries it, so a blueprint re-import or an `ak shell` re-creation
reverts it and the symptom returns verbatim.
## Wall 2 — Caddy lied to ABS about its own scheme
Audiobookshelf validates every OIDC `callback` against the origin it believes it is served
from: `getRequestOrigin()` is `x-forwarded-proto://Host` (`dist-server/server/utils/requestUtils.js`,
read inside the pinned image digest). The post-auth `http://:8017` listener is fed over plain
http, so Caddy's own `X-Forwarded-Proto` is `http`, ABS computes `http://<name>` and rejects
its own `https` callback. Probed at the app, holding the callback fixed:
|Request to `192.168.0.235:8000/audiobookshelf/auth/openid/callback?…`|Answer|
|---|---|
|`Host: <vhost>`, no forwarded scheme|`400 {"message":"Invalid callback URL - must be same-origin"}`|
|same + `X-Forwarded-Proto: https`|`302` → `…/application/o/authorize/?client_id=audiobookshelf&redirect_uri=https%3A%2F%2F<vhost>%2Faudiobookshelf%2Fauth%2Fopenid%2Fcallback…`|
So the fix is in the `@audiobooks` route on `:8017`: `header_up X-Forwarded-Proto https`. The
edge for a guarded name is TLS-only, so `https` is the truthful value, not a spoof. Same class
as the grafana-sso incident — the guard's view of the request is part of the app's contract.
## The rename: one commit, because a test enforces the lockstep
`audiobooks.franksdank.cloud` replaces `abs.franksdank.cloud` in every place that names it,
in a single commit — the `pihole` seam fails the suite if a Caddyfile vhost has no
`addn-hosts` record, so splitting it would leave a red commit:
- `dankclaw/Caddyfile` — site block, matcher `@abs` → `@audiobooks`, guard-state comment
- `dankclaw/pihole/addn-hosts` — the LAN record
- `scripts/authentik-audiobookshelf-sso.sh` — `ABS_EXTRA_BASES` default, so the RP's
registered callback list is regenerated for the new origin (the script **replaces** the
list, so the old URI leaves the provider)
- `shared/homepage/services.json` — the admin tile link
- `tests/e2e/specs/14-guard.spec.ts` — `GUARDED` + the leak marker
- the three vault pages that name the door
## Verification
|Check|Result|
|---|---|
|`./tests/run.sh`|green (tail in the comment thread)|
|docs seams after the last docs commit|6/6|
|`specs/11-admin.spec.ts` + `specs/14-guard.spec.ts`|18 passed (pre-rename: `abs` in the admin SSO walk and the guest census)|
|LAN door untouched|`/audiobookshelf/healthcheck` → `OK`, `/ping` → `{"success":true}`, login page serves|
|Guard answers anonymously|`https://<vhost>/audiobookshelf/login` → Anubis challenge, zero app bytes|
|`23-audiobookshelf-sso.spec.ts` (LAN origin)|passes|
|Porkbun rename, provider rename + defaults re-run, RP URI re-registration|**run in the deploy window** — `[[LAN-DNS]]` *Porkbun zone ops* order: zone edit first, then deploy, because the deploy's own e2e gate resolves the new name|
|In-app `Sign in with Authentik` on the guarded door|**pending that window** — the `302` above is the wall removed|
Two earlier e2e failures on those spec files were `ERR_NAME_NOT_RESOLVED` — the workstation's
`systemd-resolved` negative cache still held the pre-Porkbun NXDOMAIN. It expired on its own;
the same specs pass unmodified, so no test was changed to make the suite green.
## Docs
One owner per fact: the provider-defaults law on [[Authentik Outpost Reference]]
(provisioning rule 3), the origin-check law on [[Audiobookshelf]] (Laws), and both as steps
in *Guard a new vhost* in [[Caddy]], so the next guarded app doesn't re-hit either wall. The
rename procedure itself is already owned by [[LAN-DNS]] *Porkbun zone ops* — this PR follows
it, it does not restate it.
---
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
Built in the worktree `/tmp/wt-absfix` (branch `fix/abs-guard-oauth-defaults`, off `origin/main`).
A proxy provider built by hand in `ak shell` never runs
ProxyProvider.set_oauth_defaults() — the REST serializer does, the ORM does not —
so the row looks fully configured while redirect_uris is empty (they are derived
from external_host by _get_callback_url()), grant_types is [], and
authorization_flow is NULL. Each one kills a guarded login in a different,
unrelated-looking way, all before any credential is requested: authentik's own
"Redirect URI Error" page, then a bodiless HTTP 400 from /application/o/authorize/
once the URIs are filled by hand, and a 500 NoneType.slug in the flow planner.
abs-proxy (abs.franksdank.cloud) hit the first two on 2026-11-03.
The script calls the vendor's own set_oauth_defaults() and points the
authorization flow at default-provider-authorization-implicit-consent for every
proxy provider, idempotently; a provider that already matches is untouched, so a
deliberate deviation is never flattened. `--check` is the census (exit 1 on
drift, writes nothing) for gate use. Applied live 2026-11-03: abs-proxy fixed,
the other ten (REST-created) already compliant, census now reports drift: 0.
This is authentik DB state and no deployable file carries it — a blueprint
re-import or an ak-shell re-creation reverts it and the symptom returns, so the
law and the failure signatures are recorded on the outpost page as provisioning
rule 3.
ABS checks every OIDC callback against the origin it believes it is served from:
getRequestOrigin() is x-forwarded-proto://Host (dist-server/server/utils/requestUtils.js,
read in the pinned image digest 2026-11-03). The post-auth http://:8017 listener is fed
over plain http, so Caddy's own X-Forwarded-Proto is http, ABS derives
http://abs.franksdank.cloud, and answers its own https callback with
400 {"message":"Invalid callback URL - must be same-origin"}
Measured at the app with the callback held fixed: Host abs.franksdank.cloud alone
reproduces the 400 (as does every other Host, including the bare LAN IP), and adding
`X-Forwarded-Proto: https` turns it into a 302 to /application/o/authorize/ carrying
the registered redirect_uri. The edge for a guarded name is TLS-only, so https is the
truthful value, not a spoof; the LAN door needs nothing because there browser and
server already agree on http, which is why only the guarded path was broken.
Step 1 gains the closing sentence the abs-proxy incident demands: the ak-shell
snippet that creates a ProxyProvider with objects.create() must be followed by
scripts/authentik-proxy-provider-defaults.sh, or the guard fails before any
credential. Step 2 gains the forwarded-scheme clause for any app that derives its
own origin from the request. Each fact keeps one owner — the laws stay on
[[Authentik Outpost Reference]] (provisioning rule 3) and [[Audiobookshelf]]; this
page only says where in the procedure they bite. Outpost page timestamp bumped to
match its substantive edit.
./tests/run.sh on fix/abs-guard-oauth-defaults (824 tests):
ok 824 every crop in the tracked pack list clears the invariant
==> ALL TESTS PASSED
Docs seams re-run after the last docs commit (bats tests/docs_completeness.bats):
1..6
ok 3 no unresolved links in the docs vault
ok 5 the vault conforms to OKF v0.1
ok 6 vault indices are generated, not hand-carved
Playwright, the two spec files that carry abs (11-admin, 14-guard), against the live fleet:
✓ 13 [guest] › specs/14-guard.spec.ts:58:3 › unauthenticated direct navigation to abs.franksdank.cloud is gated by authentik (1.5s)
✓ 17 [admin] › specs/11-admin.spec.ts:28:1 › admin open links reach their apps through SSO (8.5s)
18 passed (1.0m)
The earlier failures in those two files were net::ERR_NAME_NOT_RESOLVED — the workstation's systemd-resolved negative cache still held the NXDOMAIN that predates the Porkbun record.
It expired by itself; both specs pass unmodified, so no test was changed to make the suite green.
Census after the provider fix
scripts/authentik-proxy-provider-defaults.sh --check → 11 providers ok, providers checked, drift: 0, exit 0 (full output in the description).
## Test evidence
`./tests/run.sh` on `fix/abs-guard-oauth-defaults` (824 tests):
```
ok 824 every crop in the tracked pack list clears the invariant
==> ALL TESTS PASSED
```
Docs seams re-run after the last docs commit (`bats tests/docs_completeness.bats`):
```
1..6
ok 3 no unresolved links in the docs vault
ok 5 the vault conforms to OKF v0.1
ok 6 vault indices are generated, not hand-carved
```
Playwright, the two spec files that carry `abs` (`11-admin`, `14-guard`), against the live fleet:
```
✓ 13 [guest] › specs/14-guard.spec.ts:58:3 › unauthenticated direct navigation to abs.franksdank.cloud is gated by authentik (1.5s)
✓ 17 [admin] › specs/11-admin.spec.ts:28:1 › admin open links reach their apps through SSO (8.5s)
18 passed (1.0m)
```
The earlier failures in those two files were `net::ERR_NAME_NOT_RESOLVED` — the workstation's
`systemd-resolved` negative cache still held the NXDOMAIN that predates the Porkbun record.
It expired by itself; both specs pass unmodified, so no test was changed to make the suite green.
## Census after the provider fix
`scripts/authentik-proxy-provider-defaults.sh --check` → 11 providers `ok`,
`providers checked, drift: 0`, exit 0 (full output in the description).
dank
changed title from fix(abs-guard): the guarded door completes OIDC (forwarded scheme + provider OAuth defaults) to fix(abs-guard): audiobooks.franksdank.cloud completes OIDC (forwarded scheme + provider OAuth defaults)2026-11-03 01:24:00 -08:00
`abs.` was a three-letter abbreviation of the container; the library it serves is
/mnt/entropy/Audiobooks, and `audiobooks.` says what the door is. Renamed in one
commit on purpose: the pihole seam fails the suite if a Caddyfile vhost has no
addn-hosts record, so any split would leave a red commit, and the `abs` name was
never deployed at the IdP with this fix, so nothing has to keep answering it —
no alias, no redirect, no stale URI.
Moves together: the Caddyfile site block, its matcher (@abs -> @audiobooks), the
LAN record, ABS_EXTRA_BASES in authentik-audiobookshelf-sso.sh (that script
REPLACES the provider's redirect-URI list, so the old origin's callback leaves the
RP when it is next run), the homepage admin tile, the guard spec's GUARDED list and
leak marker, and the three vault pages that name the door. authentik's provider row
follows in the deploy window (external_host + name), which re-derives its
redirect_uris through scripts/authentik-proxy-provider-defaults.sh; the Application
row keeps its own name, "Audiobookshelf Guard" — it names the app, not the vhost.
dank
merged commit df597bb1b1 into main2026-11-03 01:26:32 -08:00
Live verification after merge and deploy (merge df597bb1, 2026-11-03)
Gate run: ./scripts/deploy.sh dankclaw --service pihole --service caddy — 38 passed, 1 failed (the failure is explained below and is not this change).
Check
Result
LAN DNS (Pi-hole addn-hosts)
audiobooks.franksdank.cloud → 192.168.0.143; the abs. record is gone
Public DNS (Porkbun A)
audiobooks.franksdank.cloud → 71.227.227.172
TLS
Let's Encrypt cert, SAN audiobooks.franksdank.cloud — DNS-01 re-issued for the new name
Guard chain
Anubis challenge pre-auth; outpost passes the app's login page through (green in 14-guard)
OIDC round trip
Headless admin walk on the new door: "Sign in with Authentik" → app session issued → GET /api/me200, type=admin
SPA boot
30 _nuxt/*.js fetched over the vhost, every 200 answered application/javascript; library cards render real content
Realtime transport
GET /socket.io/?EIO=4&transport=polling and the /audiobookshelf/ mount both answer 0{"sid":…}; the client logs [SOCKET] Connected
LAN door untouched
192.168.0.235:8000/audiobookshelf still serves assets as application/javascript and the socket handshake still answers
The one red spec is a test-side assumption, not a regression
23-audiobookshelf-sso.spec.ts asserts that every.js response on the LAN host carries a JavaScript MIME. The trace shows the entry script fetched twice:
request with If-None-Match / If-Modified-Since → 304 Not Modified, which carries no Content-Type by HTTP spec
max-age=0 makes that revalidation deterministic once the OIDC bounce re-navigates the page, so the assertion fires on a perfectly healthy bundle: the defect class it was written for is an HTML stub served as a script (200 text/html), and a 304 is not that. The precise assertion is to judge only responses that carried a body (status === 200), which keeps the guard exactly as strong and drops the revalidation noise. Holding the change pending Frank's OK, since it edits a pre-existing test.
## Live verification after merge and deploy (merge `df597bb1`, 2026-11-03)
Gate run: `./scripts/deploy.sh dankclaw --service pihole --service caddy` — **38 passed, 1 failed** (the failure is explained below and is not this change).
|Check|Result|
|---|---|
|LAN DNS (Pi-hole `addn-hosts`)|`audiobooks.franksdank.cloud → 192.168.0.143`; the `abs.` record is gone|
|Public DNS (Porkbun A)|`audiobooks.franksdank.cloud → 71.227.227.172`|
|TLS|Let's Encrypt cert, SAN `audiobooks.franksdank.cloud` — DNS-01 re-issued for the new name|
|Guard chain|Anubis challenge pre-auth; outpost passes the app's login page through (green in `14-guard`)|
|OIDC round trip|Headless admin walk on the new door: "Sign in with Authentik" → app session issued → `GET /api/me` `200`, `type=admin`|
|SPA boot|30 `_nuxt/*.js` fetched over the vhost, every `200` answered `application/javascript`; library cards render real content|
|Realtime transport|`GET /socket.io/?EIO=4&transport=polling` and the `/audiobookshelf/` mount both answer `0{"sid":…}`; the client logs `[SOCKET] Connected`|
|LAN door untouched|`192.168.0.235:8000/audiobookshelf` still serves assets as `application/javascript` and the socket handshake still answers|
### The one red spec is a test-side assumption, not a regression
`23-audiobookshelf-sso.spec.ts` asserts that **every** `.js` response on the LAN host carries a JavaScript MIME. The trace shows the entry script fetched twice:
1. `200` — `Content-Type: application/javascript; charset=UTF-8`, `ETag: W/"1417-…"`, `Cache-Control: public, max-age=0`
2. request with `If-None-Match` / `If-Modified-Since` → `304 Not Modified`, **which carries no `Content-Type` by HTTP spec**
`max-age=0` makes that revalidation deterministic once the OIDC bounce re-navigates the page, so the assertion fires on a perfectly healthy bundle: the defect class it was written for is *an HTML stub served as a script* (`200 text/html`), and a `304` is not that. The precise assertion is to judge only responses that carried a body (`status === 200`), which keeps the guard exactly as strong and drops the revalidation noise. Holding the change pending Frank's OK, since it edits a pre-existing test.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The guarded Audiobookshelf door, renamed and working
The vhost is
audiobooks.franksdank.cloud(renamed here at Frank's request on2026-11-03, before the
abs.name was ever deployed — so no alias is left behind: repo,LAN record, public record, Caddy matcher and the IdP rows all move together, and the old
name appears nowhere).
The
abs-era vhost, its DNS, TLS and the Anubis/Authentik guard went live in #130. Signingin on the guarded door did not: ABS's login page rendered,
Sign in with Authentikproduced an authentik
Redirect URI Errorpage, and after the redirect URIs were filledby hand the same button produced a bodiless HTTP 400. The LAN door
(
http://192.168.0.235:8000/audiobookshelf, what the phone app uses) kept working. Twoindependent walls, both fixed here.
Wall 1 — the hand-built proxy provider had no OAuth defaults
audiobooks-proxy(thenabs-proxy) was created withProxyProvider.objects.create(...),which is not the API: the REST serializer calls
ProxyProvider.set_oauth_defaults(), the rawORM path does not. The row looked fully configured —
external_host,internal_host,application bound, embedded-outpost claim present — while
redirect_uriswas empty (thoseare derived from
external_hostby_get_callback_url()),grant_typeswas[], andauthorization_flowwas NULL. Each breaks the login in a different, unrelated-looking way,all before any credential is requested.
scripts/authentik-proxy-provider-defaults.shfills them from the vendor's ownset_oauth_defaults()plusdefault-provider-authorization-implicit-consent, idempotently,for every proxy provider.
--checkis a census (exit 1 on drift, writes nothing).Applied to the live IdP on 2026-11-03; the census afterwards:
Nothing else drifted, which is the point of the census:
grafana-ssoand the ten providerscreated through the REST API already matched and were left alone. This is authentik DB
state — no deployable file carries it, so a blueprint re-import or an
ak shellre-creationreverts it and the symptom returns verbatim.
Wall 2 — Caddy lied to ABS about its own scheme
Audiobookshelf validates every OIDC
callbackagainst the origin it believes it is servedfrom:
getRequestOrigin()isx-forwarded-proto://Host(dist-server/server/utils/requestUtils.js,read inside the pinned image digest). The post-auth
http://:8017listener is fed over plainhttp, so Caddy's own
X-Forwarded-Protoishttp, ABS computeshttp://<name>and rejectsits own
httpscallback. Probed at the app, holding the callback fixed:192.168.0.235:8000/audiobookshelf/auth/openid/callback?…Host: <vhost>, no forwarded scheme400 {"message":"Invalid callback URL - must be same-origin"}X-Forwarded-Proto: https302→…/application/o/authorize/?client_id=audiobookshelf&redirect_uri=https%3A%2F%2F<vhost>%2Faudiobookshelf%2Fauth%2Fopenid%2Fcallback…So the fix is in the
@audiobooksroute on:8017:header_up X-Forwarded-Proto https. Theedge for a guarded name is TLS-only, so
httpsis the truthful value, not a spoof. Same classas the grafana-sso incident — the guard's view of the request is part of the app's contract.
The rename: one commit, because a test enforces the lockstep
audiobooks.franksdank.cloudreplacesabs.franksdank.cloudin every place that names it,in a single commit — the
piholeseam fails the suite if a Caddyfile vhost has noaddn-hostsrecord, so splitting it would leave a red commit:dankclaw/Caddyfile— site block, matcher@abs→@audiobooks, guard-state commentdankclaw/pihole/addn-hosts— the LAN recordscripts/authentik-audiobookshelf-sso.sh—ABS_EXTRA_BASESdefault, so the RP'sregistered callback list is regenerated for the new origin (the script replaces the
list, so the old URI leaves the provider)
shared/homepage/services.json— the admin tile linktests/e2e/specs/14-guard.spec.ts—GUARDED+ the leak markerVerification
./tests/run.shspecs/11-admin.spec.ts+specs/14-guard.spec.tsabsin the admin SSO walk and the guest census)/audiobookshelf/healthcheck→OK,/ping→{"success":true}, login page serveshttps://<vhost>/audiobookshelf/login→ Anubis challenge, zero app bytes23-audiobookshelf-sso.spec.ts(LAN origin)[[LAN-DNS]]Porkbun zone ops order: zone edit first, then deploy, because the deploy's own e2e gate resolves the new nameSign in with Authentikon the guarded door302above is the wall removedTwo earlier e2e failures on those spec files were
ERR_NAME_NOT_RESOLVED— the workstation'ssystemd-resolvednegative cache still held the pre-Porkbun NXDOMAIN. It expired on its own;the same specs pass unmodified, so no test was changed to make the suite green.
Docs
One owner per fact: the provider-defaults law on Authentik Outpost Reference
(provisioning rule 3), the origin-check law on Audiobookshelf (Laws), and both as steps
in Guard a new vhost in Caddy, so the next guarded app doesn't re-hit either wall. The
rename procedure itself is already owned by LAN-DNS Porkbun zone ops — this PR follows
it, it does not restate it.
cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
Built in the worktree
/tmp/wt-absfix(branchfix/abs-guard-oauth-defaults, offorigin/main).Test evidence
./tests/run.shonfix/abs-guard-oauth-defaults(824 tests):Docs seams re-run after the last docs commit (
bats tests/docs_completeness.bats):Playwright, the two spec files that carry
abs(11-admin,14-guard), against the live fleet:The earlier failures in those two files were
net::ERR_NAME_NOT_RESOLVED— the workstation'ssystemd-resolvednegative cache still held the NXDOMAIN that predates the Porkbun record.It expired by itself; both specs pass unmodified, so no test was changed to make the suite green.
Census after the provider fix
scripts/authentik-proxy-provider-defaults.sh --check→ 11 providersok,providers checked, drift: 0, exit 0 (full output in the description).fix(abs-guard): the guarded door completes OIDC (forwarded scheme + provider OAuth defaults)to fix(abs-guard): audiobooks.franksdank.cloud completes OIDC (forwarded scheme + provider OAuth defaults)Live verification after merge and deploy (merge
df597bb1, 2026-11-03)Gate run:
./scripts/deploy.sh dankclaw --service pihole --service caddy— 38 passed, 1 failed (the failure is explained below and is not this change).addn-hosts)audiobooks.franksdank.cloud → 192.168.0.143; theabs.record is goneaudiobooks.franksdank.cloud → 71.227.227.172audiobooks.franksdank.cloud— DNS-01 re-issued for the new name14-guard)GET /api/me200,type=admin_nuxt/*.jsfetched over the vhost, every200answeredapplication/javascript; library cards render real contentGET /socket.io/?EIO=4&transport=pollingand the/audiobookshelf/mount both answer0{"sid":…}; the client logs[SOCKET] Connected192.168.0.235:8000/audiobookshelfstill serves assets asapplication/javascriptand the socket handshake still answersThe one red spec is a test-side assumption, not a regression
23-audiobookshelf-sso.spec.tsasserts that every.jsresponse on the LAN host carries a JavaScript MIME. The trace shows the entry script fetched twice:200—Content-Type: application/javascript; charset=UTF-8,ETag: W/"1417-…",Cache-Control: public, max-age=0If-None-Match/If-Modified-Since→304 Not Modified, which carries noContent-Typeby HTTP specmax-age=0makes that revalidation deterministic once the OIDC bounce re-navigates the page, so the assertion fires on a perfectly healthy bundle: the defect class it was written for is an HTML stub served as a script (200 text/html), and a304is not that. The precise assertion is to judge only responses that carried a body (status === 200), which keeps the guard exactly as strong and drops the revalidation noise. Holding the change pending Frank's OK, since it edits a pre-existing test.