Episode 15: Pocket ID as identity provider, tinyauth gate, offboarding script

Pocket ID replaces Forgejo as the OIDC provider for Outline, Vaultwarden and
NetBird; Forgejo logs in through it. Uptime Kuma sits behind tinyauth via
Caddy forward_auth (group ops), the public status page stays public.

offboarding/offboard.py switches one person off in every service with an API
and prints what a human still has to do: the shared passwords they could read.
Measured: SSO lock alone left 10 of 13 ways open for 45 minutes; the script
leaves 1 of 14, the vault copy on their laptop.

HARDENING.md: the ufw hairpin that broke Vaultwarden SSO and Kuma, the mesh
route into the docker bridges, and a fail2ban exception for Pocket ID.
This commit is contained in:
Maximilian Grimm 2026-09-23 11:52:17 +02:00
parent 3edef38ba3
commit 12542ecb19
9 changed files with 490 additions and 21 deletions

View file

@ -558,3 +558,47 @@ live run the model delivered 4 of 5 routes in 30 s, the script 5 of 5 in
and `git push` failed, because it had exempted `git-upload-pack` only. The one
thing a model could add, the explanation, read "known attack pattern for RCE"
on a request it had just classified as a false positive.
## Episode 15: your own identity provider, and three things the firewall broke quietly
**Containers could not reach the box's own public name.** Since ufw went on in
episode 10, every container on the `proxy` network that called
`https://<service>.<DOMAIN>` hit `[UFW BLOCK] ... DST=<box-ip> DPT=443`. Vaultwarden
single sign-on died with "Failed to discover OpenID provider", and Uptime Kuma
listed git, wiki and vault as down for twelve days straight, about 800 down
heartbeats a day. Kuma only notifies on a status change, and the last one was on
the day ufw went on.
Every OIDC client has to reach the provider, so this is not optional:
```sh
ufw allow proto tcp from 172.18.0.0/16 to <box-ip> port 443 comment "hairpin: containers to OIDC provider"
```
**The mesh could not reach the containers either.** Docker's own `DOCKER` chain
drops traffic into a bridge unless a port is published, and NetBird's ACCEPT in
`FORWARD` comes after it. `trusted_host_interfaces` (episode 5) only fixes the
raw table. Put this into the `DOCKER-USER` block of `/etc/ufw/after.rules`,
before the eth0 lines:
```
-A DOCKER-USER -i wt0 -o br-+ -j ACCEPT
```
**fail2ban locked out people logging in.** The `caddy-probes` jail counts
401/403/404. Pocket ID's login page asks `/api/users/me` (401 without a session)
and the logo (404) on every load. Ten page loads in a minute and the employee is
banned for an hour. Narrow exception in `filter.d/caddy-probes.conf`:
```
ignoreregex = "uri":"/api/users/me"
"uri":"/api/application-images/
```
**Pocket ID's `/setup` belongs to whoever opens it first.** A scanner fetched
the new host within seconds of the certificate. Create the admin right after
`docker compose up -d pocket-id`, before anything else.
**The login gate keeps its own session.** tinyauth checks Pocket ID once and
then trusts its cookie for 24 hours (`TINYAUTH_AUTH_SESSIONEXPIRY`), whatever
happens to the account. `offboarding/offboard.py` deletes those sessions. By hand: delete the person's
rows from the `sessions` table in `tinyauth.db`, or shorten the expiry.