docs: the guard-a-new-vhost procedure and Audiobookshelf's second door
Infrastructure/Caddy.md now owns the order of operations for exposing a vhost (proxy provider + application + embedded-outpost claim, then the route, then the name and the chain audit), because the IdP row is invisible state that nobody recreates. Audiobookshelf's Internal-only row and law become Two doors, one app; the homepage link and index.md follow.
This commit is contained in:
1 parent
5e0592381a
commit
603513a15e
3 files changed
+60
-16
No files matched your search
@@ -4,7 +4,7 @@ description: "Caddy is the single reverse proxy for both hosts."
|
||||
tags:
|
||||
- infrastructure
|
||||
- caddy
|
||||
timestamp: 2026-08-29
|
||||
timestamp: 2026-11-03
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in new issue
Block a user