Merge pull request 'feat(caddy): give Audiobookshelf a guarded abs.franksdank.cloud address' (#130) from feat/abs-vhost into main
This commit was merged in pull request #130.
This commit is contained in:
commit
17ea2f4110
8 files changed
+117
-30
No files matched your search
@@ -264,6 +264,19 @@ files.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Audiobookshelf (guarded) ---
|
||||||
|
# Anubis → Authentik → app, like romm/files. The container carries
|
||||||
|
# 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
|
||||||
|
# pre-auth. Guard state in the IdP: proxy provider `abs-proxy`
|
||||||
|
# (external_host https://abs.franksdank.cloud/, internal_host http://caddy:8017,
|
||||||
|
# application "Audiobookshelf Guard"), claimed by the embedded outpost.
|
||||||
|
abs.franksdank.cloud {
|
||||||
|
reverse_proxy anubis:8923 {
|
||||||
|
header_up X-Real-Ip {remote_host}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Jellyfin ---
|
# --- Jellyfin ---
|
||||||
jelly.franksdank.cloud {
|
jelly.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8096
|
reverse_proxy 192.168.0.235:8096
|
||||||
@@ -410,6 +423,15 @@ http://:8017 {
|
|||||||
reverse_proxy 192.168.0.235:3923
|
reverse_proxy 192.168.0.235:3923
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@abs host abs.franksdank.cloud
|
||||||
|
handle @abs {
|
||||||
|
# The app answers only under its ROUTER_BASE_PATH; the bare root is a
|
||||||
|
# convenience hop, taken post-auth so nothing reaches the app
|
||||||
|
# pre-authentication.
|
||||||
|
redir / /audiobookshelf/ permanent
|
||||||
|
reverse_proxy 192.168.0.235:8000
|
||||||
|
}
|
||||||
|
|
||||||
@prowlarr host prowlarr.franksdank.cloud
|
@prowlarr host prowlarr.franksdank.cloud
|
||||||
handle @prowlarr {
|
handle @prowlarr {
|
||||||
reverse_proxy 192.168.0.112:9696
|
reverse_proxy 192.168.0.112:9696
|
||||||
|
|||||||
@@ -26,6 +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 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
|
||||||
|
|||||||
@@ -11,12 +11,15 @@
|
|||||||
# dist-server/server/auth/OidcAuthStrategy.js + objects/settings/ServerSettings.js)
|
# dist-server/server/auth/OidcAuthStrategy.js + objects/settings/ServerSettings.js)
|
||||||
# rather than a forum, because each of these is a dead-login trap:
|
# rather than a forum, because each of these is a dead-login trap:
|
||||||
#
|
#
|
||||||
# * TWO redirect URIs on the provider: <base>/auth/openid/callback (browser) and
|
# * The browser callback URI <base>/auth/openid/callback for EVERY origin ABS is
|
||||||
# <base>/auth/openid/mobile-redirect (mobile). ABS SWAPS the app's own
|
# reached at, plus <base>/auth/openid/mobile-redirect on ABS_BASE only. ABS SWAPS
|
||||||
# audiobookshelf://oauth for the mobile-redirect URL before the IdP ever sees a
|
# the app's own audiobookshelf://oauth for the mobile-redirect URL before the IdP
|
||||||
# redirect_uri, so registering the app scheme with authentik — the obvious move —
|
# ever sees a redirect_uri, so registering the app scheme with authentik — the
|
||||||
# is what makes phone logins fail. The app scheme belongs in ABS's
|
# obvious move — is what makes phone logins fail. The app scheme belongs in ABS's
|
||||||
# authOpenIDMobileRedirectURIs list (its built-in default), not here.
|
# authOpenIDMobileRedirectURIs list (its built-in default), not here. The mobile
|
||||||
|
# URI is registered on the LAN origin only because the phone is configured with
|
||||||
|
# the LAN address: it is a non-browser client and cannot solve the guard
|
||||||
|
# challenge that sits in front of the vhost.
|
||||||
# * <base> CARRIES THE BASE PATH. ABS builds redirect_uri as
|
# * <base> CARRIES THE BASE PATH. ABS builds redirect_uri as
|
||||||
# <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that subfolder is
|
# <origin><authOpenIDSubfolderForRedirectURLs>/auth/openid/callback, and that subfolder is
|
||||||
# the ROUTER_BASE_PATH the container is mounted at — /audiobookshelf, which is also the only
|
# the ROUTER_BASE_PATH the container is mounted at — /audiobookshelf, which is also the only
|
||||||
@@ -72,8 +75,17 @@
|
|||||||
# (qa-e2e-admin — tests/e2e/specs/23-audiobookshelf-sso.spec.ts
|
# (qa-e2e-admin — tests/e2e/specs/23-audiobookshelf-sso.spec.ts
|
||||||
# signs in as this identity, so a re-run restores the fixture)
|
# signs in as this identity, so a re-run restores the fixture)
|
||||||
# ABS_BASE ABS URL as the browser sees it, base path included
|
# ABS_BASE ABS URL as the browser sees it, base path included
|
||||||
# (http://192.168.0.235:8000/audiobookshelf — LAN-only by policy:
|
# (http://192.168.0.235:8000/audiobookshelf — the LAN
|
||||||
# no vhost, no reverse proxy in front)
|
# origin, reached directly, which is also what the phone
|
||||||
|
# app is configured with)
|
||||||
|
# ABS_EXTRA_BASES space-separated additional origins, base path included
|
||||||
|
# (default: https://abs.franksdank.cloud/audiobookshelf —
|
||||||
|
# the guarded vhost, dankclaw/Caddyfile).
|
||||||
|
# ABS builds its redirect_uri from the ORIGIN of the
|
||||||
|
# request, so each origin the app is served at needs its
|
||||||
|
# own registered URI — an unregistered origin is a
|
||||||
|
# redirect_uri_mismatch at the IdP, before any credential.
|
||||||
|
# Set it empty to register the LAN origin alone.
|
||||||
# AUTHENTIK_HOST ssh target (dankclaw)
|
# AUTHENTIK_HOST ssh target (dankclaw)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -88,6 +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}"
|
||||||
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
|
||||||
@@ -110,6 +123,7 @@ from authentik.providers.oauth2.models import (
|
|||||||
|
|
||||||
ADMIN_GROUP, USER_GROUP = "$ADMIN_GROUP", "$USER_GROUP"
|
ADMIN_GROUP, USER_GROUP = "$ADMIN_GROUP", "$USER_GROUP"
|
||||||
CLAIM, CLIENT_ID, SECRET, BASE = "$CLAIM", "$CLIENT_ID", "$SECRET", "$BASE"
|
CLAIM, CLIENT_ID, SECRET, BASE = "$CLAIM", "$CLIENT_ID", "$SECRET", "$BASE"
|
||||||
|
BASES = [BASE] + """$EXTRA_BASES""".split()
|
||||||
SSO_USER, E2E_ADMIN = "$SSO_USER", "$E2E_ADMIN"
|
SSO_USER, E2E_ADMIN = "$SSO_USER", "$E2E_ADMIN"
|
||||||
MEMBERS = """$MEMBERS""".split()
|
MEMBERS = """$MEMBERS""".split()
|
||||||
EXPR = '''$EXPR'''
|
EXPR = '''$EXPR'''
|
||||||
@@ -143,10 +157,12 @@ _ns = {}
|
|||||||
exec(_src, _ns)
|
exec(_src, _ns)
|
||||||
|
|
||||||
# --- provider --------------------------------------------------------------------
|
# --- provider --------------------------------------------------------------------
|
||||||
|
# One browser callback per origin (LAN address and the guarded vhost), because ABS
|
||||||
|
# derives redirect_uri from the request origin; the mobile URI stays on the LAN origin.
|
||||||
uris = [RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT,
|
uris = [RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT,
|
||||||
url="%s/auth/openid/callback" % BASE),
|
url="%s/auth/openid/callback" % b) for b in BASES]
|
||||||
RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT,
|
uris.append(RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT,
|
||||||
url="%s/auth/openid/mobile-redirect" % BASE)]
|
url="%s/auth/openid/mobile-redirect" % BASE))
|
||||||
authz_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
|
authz_flow = Flow.objects.get(slug="default-provider-authorization-implicit-consent")
|
||||||
|
|
||||||
# Three things beyond the name, each one a silent dead login: the grant-type list (2026.8
|
# Three things beyond the name, each one a silent dead login: the grant-type list (2026.8
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
type: reference
|
type: reference
|
||||||
description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, SSD state, API bootstrap, authentik SSO, laws."
|
description: "Audiobookshelf on Dankserver: quadlet shape, array bind, SSD state, API bootstrap, authentik SSO, guarded vhost, laws."
|
||||||
tags:
|
tags:
|
||||||
- dankserver
|
- dankserver
|
||||||
- audiobooks
|
- audiobooks
|
||||||
- quadlet
|
- quadlet
|
||||||
timestamp: 2026-11-02
|
timestamp: 2026-11-03
|
||||||
---
|
---
|
||||||
|
|
||||||
# Audiobookshelf
|
# Audiobookshelf
|
||||||
@@ -16,8 +16,8 @@ per-listener playback positions, and streams the audio. It is **not** a media
|
|||||||
server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on the
|
server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on the
|
||||||
array.
|
array.
|
||||||
|
|
||||||
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user
|
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) —
|
||||||
manager) — **Internal-only:** LAN address `http://192.168.0.235:8000/audiobookshelf`, no vhost, no DNS name.
|
**Doors:** LAN `http://192.168.0.235:8000/audiobookshelf` (direct, what the phone app uses) and guarded `https://abs.franksdank.cloud/audiobookshelf/`.
|
||||||
|
|
||||||
## Shape
|
## Shape
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ second host 2026-11-02 and re-measured on both base paths: `/ping` (`{"success":
|
|||||||
`/healthcheck` (`OK`) answer at the ROOT whatever `ROUTER_BASE_PATH` says, so the app's
|
`/healthcheck` (`OK`) answer at the ROOT whatever `ROUTER_BASE_PATH` says, so the app's
|
||||||
connectivity probe works before it resolves the base path at all, and `iptables -S INPUT`
|
connectivity probe works before it resolves the base path at all, and `iptables -S INPUT`
|
||||||
carries no rule for 8000 — a phone on `192.168.0.0/24` connects with no further setup, and
|
carries no rule for 8000 — a phone on `192.168.0.0/24` connects with no further setup, and
|
||||||
`download: true` on the root account is what makes offline listening work. Away from the LAN
|
`download: true` on the root account is what makes offline listening work. Away
|
||||||
— see the LAN-only law, and note that the guard chain is not the remedy there.
|
from the LAN, see the two-doors law in *Laws* — the guarded vhost is not the remedy.
|
||||||
|
|
||||||
**The login token is at `user.token`, not top level.** `POST /login` answers
|
**The login token is at `user.token`, not top level.** `POST /login` answers
|
||||||
`{ user, serverSettings, ereaderDevices, Source, userDefaultLibraryId }` and the bearer sits
|
`{ user, serverSettings, ereaderDevices, Source, userDefaultLibraryId }` and the bearer sits
|
||||||
@@ -98,6 +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 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
|
||||||
@@ -186,13 +187,13 @@ Both halves — authentik's provider row and ABS's settings — are asserted end
|
|||||||
- **No `systemctl --user enable`.** Generated Quadlet units cannot be enabled on
|
- **No `systemctl --user enable`.** Generated Quadlet units cannot be enabled on
|
||||||
this host; `WantedBy=default.target` in the unit file is what carries the
|
this host; `WantedBy=default.target` in the unit file is what carries the
|
||||||
service across a reboot ([[Quadlet Lifecycle]]).
|
service across a reboot ([[Quadlet Lifecycle]]).
|
||||||
- **LAN-only until Frank says otherwise.** `0.0.0.0:8000` with no reverse proxy
|
- **Two doors, one app.** `http://192.168.0.235:8000/audiobookshelf` is the raw
|
||||||
in front. Any public vhost requires the Anubis + Authentik chain first, and the
|
LAN address — plain HTTP, nothing in front, what the phone app is configured
|
||||||
outpost proxy provider has to exist before the vhost flips (external exposure
|
with; `https://abs.franksdank.cloud/audiobookshelf/` is the guarded vhost
|
||||||
policy in `AGENTS.md`). That chain serves browsers: the phone app is a non-browser
|
(Caddy → Anubis → the Authentik outpost → Caddy `:8017` → this container),
|
||||||
client and cannot solve a challenge, so guarding a vhost does not make remote
|
the house chain for browser traffic — [[Caddy]] *Guard a new vhost*. That
|
||||||
listening work — remote listening wants a VPN into the LAN, after which the LAN
|
chain serves browsers only: the phone app is a non-browser client and solves
|
||||||
address in *Phone app* is correct unchanged.
|
no challenge, so remote listening wants a VPN into the LAN, not the vhost.
|
||||||
- **There is no `/setup` route; the creation door is `POST /init`.** `/status`
|
- **There is no `/setup` route; the creation door is `POST /init`.** `/status`
|
||||||
(no `/api` prefix) reports `isInit` = "a root user exists", `POST /init` with
|
(no `/api` prefix) reports `isInit` = "a root user exists", `POST /init` with
|
||||||
`{"newRoot":{"username","password"}}` creates it, `POST /login` returns the
|
`{"newRoot":{"username","password"}}` creates it, `POST /login` returns the
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ description: "Caddy is the single reverse proxy for both hosts."
|
|||||||
tags:
|
tags:
|
||||||
- infrastructure
|
- infrastructure
|
||||||
- caddy
|
- caddy
|
||||||
timestamp: 2026-08-29
|
timestamp: 2026-11-03
|
||||||
---
|
---
|
||||||
|
|
||||||
# Caddy
|
# Caddy
|
||||||
@@ -90,7 +90,50 @@ pihole.franksdank.cloud {
|
|||||||
|
|
||||||
After rendering, the line reads `dankuser <hash>` (one complete hash) — `tests/deployment_flow.bats:96-99` pins the `dankuser` name and the single-hash form, and `tests/caddyfile.bats:75` pins the bare `dankuser REPLACE_WITH_HASH` template. Do not put a `$2a$14$` prefix in the template: it would render a double-prefixed, invalid hash (2026-08-22 incident — every basicauth vhost returned 401 for every password; `tests/caddyfile.bats:67-72` forbids the prefix). `tests/deployment_flow.bats` pins the render contract.
|
After rendering, the line reads `dankuser <hash>` (one complete hash) — `tests/deployment_flow.bats:96-99` pins the `dankuser` name and the single-hash form, and `tests/caddyfile.bats:75` pins the bare `dankuser REPLACE_WITH_HASH` template. Do not put a `$2a$14$` prefix in the template: it would render a double-prefixed, invalid hash (2026-08-22 incident — every basicauth vhost returned 401 for every password; `tests/caddyfile.bats:67-72` forbids the prefix). `tests/deployment_flow.bats` pins the render contract.
|
||||||
|
|
||||||
Everything else authenticates, not basicauth. The guarded set — `www.` (homepage), `search.`, `grafana.`, `prometheus.`, `loki.`, `seerr.`, `marreta.`, `romm.`, `files.` — proxies **all** traffic to `anubis:8923`, which verifies the client and forwards it back to Caddy's internal listener `http://:8016`; Authentik's proxy-provider mode serves the login inline (this build exposes **no** `forward_auth` endpoint) and forwards authenticated traffic to `http://:8017`, where the request is routed straight to its app (`dankclaw/Caddyfile:14-24,283-423`). The exempt-by-construction set never enters that chain: `auth.` (the IdP itself), `ai.` and `gitea.` (own logins), `hermes.` and `hermesadmin.` (app-enforced passwords), `pihole.` (the basicauth edge above), and the media/*arr vhosts. Grafana left basicauth on 2026-09-01 and is an OAuth2 relying party against Authentik (`dankclaw/Caddyfile:122-132`). See [[Homepage]] and [[Authentik]].
|
Everything else authenticates, not basicauth. The guarded set — `www.` (homepage), `search.`, `grafana.`, `prometheus.`, `loki.`, `seerr.`, `marreta.`, `romm.`, `files.`, `notebook.`, `abs.` — proxies **all** traffic to `anubis:8923`, which verifies the client and forwards it back to Caddy's internal listener `http://:8016`; Authentik's proxy-provider mode serves the login inline (this build exposes **no** `forward_auth` endpoint) and forwards authenticated traffic to `http://:8017`, where the request is routed straight to its app (`dankclaw/Caddyfile`: the header comment and the two internal listeners). The exempt-by-construction set never enters that chain: `auth.` (the IdP itself), `ai.` and `gitea.` (own logins), `hermes.` and `hermesadmin.` (app-enforced passwords), `pihole.` (the basicauth edge above), and the media/*arr vhosts. Grafana left basicauth on 2026-09-01 and is an OAuth2 relying party against Authentik (`dankclaw/Caddyfile:122-132`). See [[Homepage]] and [[Authentik]].
|
||||||
|
|
||||||
|
## Guard a new vhost
|
||||||
|
|
||||||
|
Order is the whole procedure: the IdP row has to exist **before** the route flips, or
|
||||||
|
authenticated traffic reaches the outpost with no matching provider and the user gets
|
||||||
|
authentik's error page instead of the app.
|
||||||
|
|
||||||
|
1. **Provider first.** A proxy provider plus an application, claimed by the embedded
|
||||||
|
outpost. No blueprint owns this state — it lives only in the IdP's database, which is
|
||||||
|
why it is easy to forget:
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh dankclaw 'podman exec -i authentik-server ak shell' <<'PY'
|
||||||
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
|
from authentik.core.models import Application
|
||||||
|
from authentik.outposts.models import Outpost
|
||||||
|
NAME, EXT = "abs", "https://abs.franksdank.cloud/"
|
||||||
|
pp = ProxyProvider.objects.filter(name=f"{NAME}-proxy").first()
|
||||||
|
if pp is None:
|
||||||
|
pp = ProxyProvider.objects.create(name=f"{NAME}-proxy", external_host=EXT,
|
||||||
|
internal_host="http://caddy:8017")
|
||||||
|
if not Application.objects.filter(provider=pp).exists():
|
||||||
|
Application.objects.create(name=f"{NAME.title()} Guard", slug=f"{NAME}-guard",
|
||||||
|
provider=pp)
|
||||||
|
o = Outpost.objects.get(managed="goauthentik.io/outposts/embedded")
|
||||||
|
o.providers.add(pp) # an unclaimed provider is invisible to the outpost
|
||||||
|
print(pp.name, pp.external_host, o.providers.count())
|
||||||
|
PY
|
||||||
|
```
|
||||||
|
|
||||||
|
`external_host` is the exact vhost URL **with the trailing slash**: the embedded
|
||||||
|
outpost routes on the ORIGINAL `Host` header alone, so anything else matches zero
|
||||||
|
providers. Never print a provider's fields wholesale — a proxy provider carries a
|
||||||
|
client secret, and it lands in whatever transcript the command runs in.
|
||||||
|
2. **Then the route.** The public site block proxies the whole request to `anubis:8923`
|
||||||
|
with `header_up X-Real-Ip {remote_host}`, and the post-auth `http://:8017` listener
|
||||||
|
gains a `@<name> host <vhost>` handler pointing at the app — copy `romm`/`files`,
|
||||||
|
which are the same shape including a Dankserver LAN-port upstream.
|
||||||
|
3. **Then the name and the audit.** A `dankclaw/pihole/addn-hosts` line (the `pihole`
|
||||||
|
seam fails the build if a Caddyfile vhost has no record), a Porkbun A record if the
|
||||||
|
name must resolve off-LAN ([[LAN-DNS]]), and the subdomain added to `GUARDED` in
|
||||||
|
`tests/e2e/specs/14-guard.spec.ts` with an app-specific leak marker — that spec is
|
||||||
|
the deploy gate's proof that the guard, not the app, answers an anonymous client.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ is a locator: one row, one clause, no numbers.
|
|||||||
|
|
||||||
| Page | Owns |
|
| Page | Owns |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [[Audiobookshelf]] | Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, SSD state, API bootstrap, authentik SSO, laws. |
|
| [[Audiobookshelf]] | Audiobookshelf on Dankserver: quadlet shape, array bind, SSD state, API bootstrap, authentik SSO, guarded vhost, laws. |
|
||||||
| [[Calibre]] | Calibre is the e-book library manager that backs Calibre-Web. |
|
| [[Calibre]] | Calibre is the e-book library manager that backs Calibre-Web. |
|
||||||
| [[Calibre-Web]] | Calibre-Web is a web application that browses, reads, and downloads eBooks from an existing Calibre library database. |
|
| [[Calibre-Web]] | Calibre-Web is a web application that browses, reads, and downloads eBooks from an existing Calibre library database. |
|
||||||
| [[Copyparty]] | Copyparty is a lightweight file server and media streamer that runs as a single Python binary. |
|
| [[Copyparty]] | Copyparty is a lightweight file server and media streamer that runs as a single Python binary. |
|
||||||
|
|||||||
@@ -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://192.168.0.235:8000",
|
"url": "http://abs.franksdank.cloud/audiobookshelf/",
|
||||||
"docs": "",
|
"docs": "",
|
||||||
"probe": "192.168.0.235:8000",
|
"probe": "192.168.0.235:8000",
|
||||||
"method": "http",
|
"method": "http",
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ 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"];
|
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook", "abs"];
|
||||||
|
|
||||||
// Markers that prove app content leaked past the guard to an anonymous user.
|
// 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 —
|
||||||
|
// for abs that is the Audiobookshelf login's own SSO button label (the app's OIDC
|
||||||
|
// 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"],
|
||||||
"romm.franksdank.cloud": ["RomM"],
|
"romm.franksdank.cloud": ["RomM"],
|
||||||
@@ -26,6 +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"],
|
||||||
};
|
};
|
||||||
|
|
||||||
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