chore(abs): name the Audiobookshelf door audiobooks.franksdank.cloud

`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.
This commit is contained in:
Frank committed 2026-11-03 01:23:54 -08:00
1 parent 71d5804eaf
commit 23ad0ca317
9 files changed
+23 -22

No files matched your search

+7 -6
View File
@@ -268,10 +268,11 @@ files.franksdank.cloud {
# Anubis → Authentik → app, like romm/files. The container carries # Anubis → Authentik → app, like romm/files. The container carries
# ROUTER_BASE_PATH=/audiobookshelf, so this vhost serves the app under that # ROUTER_BASE_PATH=/audiobookshelf, so this vhost serves the app under that
# path; the bare root is redirected onto it in the post-auth listener, never # path; the bare root is redirected onto it in the post-auth listener, never
# pre-auth. Guard state in the IdP: proxy provider `abs-proxy` # pre-auth. Guard state in the IdP: proxy provider `audiobooks-proxy`
# (external_host https://abs.franksdank.cloud/, internal_host http://caddy:8017, # (external_host https://audiobooks.franksdank.cloud/, internal_host
# application "Audiobookshelf Guard"), claimed by the embedded outpost. # http://caddy:8017, application "Audiobookshelf Guard"), claimed by the
abs.franksdank.cloud { # embedded outpost.
audiobooks.franksdank.cloud {
reverse_proxy anubis:8923 { reverse_proxy anubis:8923 {
header_up X-Real-Ip {remote_host} header_up X-Real-Ip {remote_host}
} }
@@ -423,8 +424,8 @@ http://:8017 {
reverse_proxy 192.168.0.235:3923 reverse_proxy 192.168.0.235:3923
} }
@abs host abs.franksdank.cloud @audiobooks host audiobooks.franksdank.cloud
handle @abs { handle @audiobooks {
# The app answers only under its ROUTER_BASE_PATH; the bare root is a # The app answers only under its ROUTER_BASE_PATH; the bare root is a
# convenience hop, taken post-auth so nothing reaches the app # convenience hop, taken post-auth so nothing reaches the app
# pre-authentication. # pre-authentication.
+1 -1
View File
@@ -26,7 +26,7 @@
192.168.0.143 romm.franksdank.cloud 192.168.0.143 romm.franksdank.cloud
192.168.0.143 files.franksdank.cloud 192.168.0.143 files.franksdank.cloud
192.168.0.143 jelly.franksdank.cloud 192.168.0.143 jelly.franksdank.cloud
192.168.0.143 abs.franksdank.cloud 192.168.0.143 audiobooks.franksdank.cloud
192.168.0.143 radarr.franksdank.cloud 192.168.0.143 radarr.franksdank.cloud
192.168.0.143 lidarr.franksdank.cloud 192.168.0.143 lidarr.franksdank.cloud
192.168.0.143 readarr.franksdank.cloud 192.168.0.143 readarr.franksdank.cloud
+3 -3
View File
@@ -79,8 +79,8 @@
# origin, reached directly, which is also what the phone # origin, reached directly, which is also what the phone
# app is configured with) # app is configured with)
# ABS_EXTRA_BASES space-separated additional origins, base path included # ABS_EXTRA_BASES space-separated additional origins, base path included
# (default: https://abs.franksdank.cloud/audiobookshelf — # (default: https://audiobooks.franksdank.cloud/audiobookshelf
# the guarded vhost, dankclaw/Caddyfile). # — the guarded vhost, dankclaw/Caddyfile).
# ABS builds its redirect_uri from the ORIGIN of the # ABS builds its redirect_uri from the ORIGIN of the
# request, so each origin the app is served at needs its # request, so each origin the app is served at needs its
# own registered URI — an unregistered origin is a # own registered URI — an unregistered origin is a
@@ -100,7 +100,7 @@ SSO_USER="${ABS_OIDC_USERNAME:-akadmin}"
MEMBERS="${ABS_OIDC_USERS:-}" MEMBERS="${ABS_OIDC_USERS:-}"
E2E_ADMIN="${ABS_E2E_ADMIN:-qa-e2e-admin}" E2E_ADMIN="${ABS_E2E_ADMIN:-qa-e2e-admin}"
BASE="${ABS_BASE:-http://192.168.0.235:8000/audiobookshelf}" BASE="${ABS_BASE:-http://192.168.0.235:8000/audiobookshelf}"
EXTRA_BASES="${ABS_EXTRA_BASES:-https://abs.franksdank.cloud/audiobookshelf}" EXTRA_BASES="${ABS_EXTRA_BASES:-https://audiobooks.franksdank.cloud/audiobookshelf}"
HOST="${AUTHENTIK_HOST:-dankclaw}" HOST="${AUTHENTIK_HOST:-dankclaw}"
# The claim expression: translate group membership into ABS role names. It is stored # The claim expression: translate group membership into ABS role names. It is stored
+2 -2
View File
@@ -10,8 +10,8 @@
# #
# * redirect_uris empty -> authentik answers its own page: # * redirect_uris empty -> authentik answers its own page:
# "Redirect URI Error — The request fails due to a missing, invalid, or # "Redirect URI Error — The request fails due to a missing, invalid, or
# mismatching redirection URI (redirect_uri)". Measured on abs-proxy # mismatching redirection URI (redirect_uri)". Measured on the Audiobookshelf
# (abs.franksdank.cloud) 2026-11-03. # guard provider, 2026-11-03.
# * grant_types empty -> the authorize endpoint refuses the outpost's # * grant_types empty -> the authorize endpoint refuses the outpost's
# authorization-code request with a bodiless HTTP 400 (Chromium renders # authorization-code request with a bodiless HTTP 400 (Chromium renders
# "This page isn't working / HTTP ERROR 400"). Same provider, one step # "This page isn't working / HTTP ERROR 400"). Same provider, one step
@@ -17,7 +17,7 @@ server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on
array. array.
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) — **Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) —
**Doors:** LAN `http://192.168.0.235:8000/audiobookshelf` (direct, what the phone app uses) and guarded `https://abs.franksdank.cloud/audiobookshelf/`. **Doors:** LAN `http://192.168.0.235:8000/audiobookshelf` (direct, what the phone app uses) and guarded `https://audiobooks.franksdank.cloud/audiobookshelf/`.
## Shape ## Shape
@@ -98,7 +98,7 @@ the local form, and the phone app offers SSO on its server-address screen. The l
| The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file | | The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file |
| Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) | | Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) |
| Admin group members | `akadmin` (the operator's own authentik admin account — sign in as it and the claim grants site admin) and `qa-e2e-admin` (the identity the deploy gate signs in as, so the script re-adds it every run; drop it and the gate fails, not a phone) | | Admin group members | `akadmin` (the operator's own authentik admin account — sign in as it and the claim grants site admin) and `qa-e2e-admin` (the identity the deploy gate signs in as, so the script re-adds it every run; drop it and the gate fails, not a phone) |
| The guard in front of the vhost | proxy provider `abs-proxy` (external_host `https://abs.franksdank.cloud/`, internal_host `http://caddy:8017`) + application "Audiobookshelf Guard", claimed by the embedded outpost — the ak-shell recipe in [[Caddy]] *Guard a new vhost* creates it; without that row the chain answers authentik's error page, not the login | | The guard in front of the vhost | proxy provider `audiobooks-proxy` (external_host `https://audiobooks.franksdank.cloud/`, internal_host `http://caddy:8017`) + application "Audiobookshelf Guard" (its own name tracks the app, not the vhost), claimed by the embedded outpost — the ak-shell recipe in [[Caddy]] *Guard a new vhost* creates it; without that row the chain answers authentik's error page, not the login |
The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on, The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on,
a first sign-in creates the account, and the claim alone decides the type. A user in a first sign-in creates the account, and the claim alone decides the type. A user in
@@ -144,7 +144,7 @@ Both halves — authentik's provider row and ABS's settings — are asserted end
`callback` against the origin it believes it is served from — `getRequestOrigin()` is `callback` against the origin it believes it is served from — `getRequestOrigin()` is
`x-forwarded-proto://Host` (`server/utils/requestUtils.js`, read in the pinned digest `x-forwarded-proto://Host` (`server/utils/requestUtils.js`, read in the pinned digest
2026-11-03) — so behind a TLS edge that reaches the container over plain http it computes 2026-11-03) — so behind a TLS edge that reaches the container over plain http it computes
`http://abs.franksdank.cloud` and answers its own https callback with a 400 "Invalid callback `http://audiobooks.franksdank.cloud` and answers its own https callback with a 400 "Invalid callback
URL - must be same-origin" before the IdP is ever contacted. The Caddy `:8017` route pins URL - must be same-origin" before the IdP is ever contacted. The Caddy `:8017` route pins
`header_up X-Forwarded-Proto https` for exactly that reason ([[Caddy]] *Guard a new vhost*); `header_up X-Forwarded-Proto https` for exactly that reason ([[Caddy]] *Guard a new vhost*);
the LAN door needs nothing, because there browser and server agree on `http`. the LAN door needs nothing, because there browser and server agree on `http`.
@@ -197,7 +197,7 @@ Both halves — authentik's provider row and ABS's settings — are asserted end
service across a reboot ([[Quadlet Lifecycle]]). service across a reboot ([[Quadlet Lifecycle]]).
- **Two doors, one app.** `http://192.168.0.235:8000/audiobookshelf` is the raw - **Two doors, one app.** `http://192.168.0.235:8000/audiobookshelf` is the raw
LAN address — plain HTTP, nothing in front, what the phone app is configured LAN address — plain HTTP, nothing in front, what the phone app is configured
with; `https://abs.franksdank.cloud/audiobookshelf/` is the guarded vhost with; `https://audiobooks.franksdank.cloud/audiobookshelf/` is the guarded vhost
(Caddy → Anubis → the Authentik outpost → Caddy `:8017` → this container), (Caddy → Anubis → the Authentik outpost → Caddy `:8017` → this container),
the house chain for browser traffic — [[Caddy]] *Guard a new vhost*. That the house chain for browser traffic — [[Caddy]] *Guard a new vhost*. That
chain serves browsers only: the phone app is a non-browser client and solves chain serves browsers only: the phone app is a non-browser client and solves
@@ -87,7 +87,7 @@ Three provisioning rules, each verified live by an outage or a dead login:
`_get_callback_url`) and `grant_types` is `[]`. Both kill the guard before any credential, `_get_callback_url`) and `grant_types` is `[]`. Both kill the guard before any credential,
in this order: authentik renders its own **"Redirect URI Error"** page, and once the URIs in this order: authentik renders its own **"Redirect URI Error"** page, and once the URIs
are filled by hand the next wall is a bodiless **HTTP 400** out of `/application/o/authorize/` are filled by hand the next wall is a bodiless **HTTP 400** out of `/application/o/authorize/`
(both measured on `abs-proxy`, 2026-11-03). Census and repair in one idempotent pass, all (both measured on the Audiobookshelf guard provider, 2026-11-03). Census and repair in one idempotent pass, all
providers at once: `scripts/authentik-proxy-provider-defaults.sh`; `--check` prints the providers at once: `scripts/authentik-proxy-provider-defaults.sh`; `--check` prints the
census and exits 1 on drift, and a provider that already matches is left alone. census and exits 1 on drift, and a provider that already matches is left alone.
@@ -107,7 +107,7 @@ authentik's error page instead of the app.
from authentik.providers.proxy.models import ProxyProvider from authentik.providers.proxy.models import ProxyProvider
from authentik.core.models import Application from authentik.core.models import Application
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
NAME, EXT = "abs", "https://abs.franksdank.cloud/" NAME, EXT = "audiobooks", "https://audiobooks.franksdank.cloud/"
pp = ProxyProvider.objects.filter(name=f"{NAME}-proxy").first() pp = ProxyProvider.objects.filter(name=f"{NAME}-proxy").first()
if pp is None: if pp is None:
pp = ProxyProvider.objects.create(name=f"{NAME}-proxy", external_host=EXT, pp = ProxyProvider.objects.create(name=f"{NAME}-proxy", external_host=EXT,
+1 -1
View File
@@ -516,7 +516,7 @@
"category": "admin", "category": "admin",
"section": "Media Library Apps", "section": "Media Library Apps",
"description": "Audiobook server for the entropy library: its own SQLite library, its own login, per-listener positions.", "description": "Audiobook server for the entropy library: its own SQLite library, its own login, per-listener positions.",
"url": "http://abs.franksdank.cloud/audiobookshelf/", "url": "http://audiobooks.franksdank.cloud/audiobookshelf/",
"docs": "", "docs": "",
"probe": "192.168.0.235:8000", "probe": "192.168.0.235:8000",
"method": "http", "method": "http",
+3 -3
View File
@@ -15,11 +15,11 @@ const BASE = "https://www.franksdank.cloud";
// Every vhost that Caddy routes through anubis:8923 (see dankclaw/Caddyfile). // Every vhost that Caddy routes through anubis:8923 (see dankclaw/Caddyfile).
// Keep this list in lockstep with the Caddyfile: a new guarded vhost must be // Keep this list in lockstep with the Caddyfile: a new guarded vhost must be
// added here or the guard-chain audit is incomplete. // added here or the guard-chain audit is incomplete.
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook", "abs"]; const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook", "audiobooks"];
// Markers that prove app content leaked past the guard to an anonymous user. Each // Markers that prove app content leaked past the guard to an anonymous user. Each
// marker must be a string the APP renders that the gate itself could never print — // marker must be a string the APP renders that the gate itself could never print —
// for abs that is the Audiobookshelf login's own SSO button label (the app's OIDC // for audiobooks that is the Audiobookshelf login's own SSO button label (the app's
// door), which authentik's gate never renders because it IS the IdP. // door), which authentik's gate never renders because it IS the IdP.
const LEAK_MARKERS: Record<string, string[]> = { const LEAK_MARKERS: Record<string, string[]> = {
"search.franksdank.cloud": ["SearXNG preferences"], "search.franksdank.cloud": ["SearXNG preferences"],
@@ -29,7 +29,7 @@ const LEAK_MARKERS: Record<string, string[]> = {
"grafana.franksdank.cloud": ["Dashboards"], "grafana.franksdank.cloud": ["Dashboards"],
"prometheus.franksdank.cloud": ["Expression Browser", "Graph"], "prometheus.franksdank.cloud": ["Expression Browser", "Graph"],
"notebook.franksdank.cloud": ["Open Notebook"], "notebook.franksdank.cloud": ["Open Notebook"],
"abs.franksdank.cloud": ["Sign in with Authentik"], "audiobooks.franksdank.cloud": ["Sign in with Authentik"],
}; };
test("homepage root (/) is guarded for unauthenticated users", async ({ page }) => { test("homepage root (/) is guarded for unauthenticated users", async ({ page }) => {