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:
1 parent
71d5804eaf
commit
23ad0ca317
9 files changed
+23
-22
No files matched your search
+7
-6
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
Reference in new issue
Block a user