selfhostedops-stack/offboarding/README.md
Maximilian Grimm 12542ecb19 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.
2026-09-23 11:52:17 +02:00

78 lines
3.7 KiB
Markdown

# Offboarding (episode 15)
Someone leaves. Switching them off in the single sign-on is not offboarding.
Measured on the demo box, with a test employee who had the access a real one has:
| After | Ways still open |
| --- | --- |
| Forgejo login prohibited (Forgejo was the SSO provider) | **10 of 13**, unchanged for 45 minutes |
| Pocket ID account disabled, nothing else | **12 of 14** |
| `./offboard.py <user>` (about one second) | **1 of 14**: the vault copy on their laptop |
What stayed open with only the SSO switched off: the wiki session (valid for
three months) and its API key, the vault sync (Forgejo kept handing out fresh
tokens to the locked account), a new vault login with the master password, the
local vault copy, the mesh peer (expires after 24 h), the automation account,
both notification logins, and the monitoring password everybody knows.
## Run it
On the box, as root:
```sh
cd /opt/selfhostedops-stack/offboarding
cp offboard.env.example offboard.env && chmod 600 offboard.env # fill in once
./offboard.py sam --dry-run # looks, changes nothing
./offboard.py sam
```
It disables the person in Pocket ID first (no new logins, refresh tokens
refused), then per service: deletes the tinyauth gate sessions, prohibits the
Forgejo login and deletes the SSH keys, suspends the Outline user, disables the
Vaultwarden user (which also logs out every device), deletes the NetBird user
with its peers, deletes the n8n user, deletes the ntfy user with its tokens.
Every step can run twice. A step that fails says why and the others still run.
Then it prints what only a human can do. The important line is this one:
```
Vaultwarden: remove the user from organization 'Northwind Labs' (needs an org
owner) and CHANGE THE 6 SHARED PASSWORDS they could read. A copy on their
laptop still opens with their master password, offline, forever.
```
That is not a figure of speech. After the script, on the test laptop, with the
network namespace cut off, `bw unlock` still opened all six shared entries in
clear text. No server-side switch reaches a file on someone else's disk.
Rotation is the offboarding step for everything they could read.
## What the keys in offboard.env may do
| Key | Scope | Why |
| --- | --- | --- |
| `POCKET_ID_API_KEY` | admin API, set an expiry | disable the user |
| `FORGEJO_ADMIN_TOKEN` | `write:admin`, `write:user` | prohibit login, delete keys, list tokens |
| `OUTLINE_API_KEY` | an admin's key | suspend |
| `NETBIRD_PAT` | service user, role admin, 30 days | delete user and peers |
| `N8N_API_KEY` | only `user:list`, `user:read`, `user:delete` | delete the user |
This file is the most powerful thing on the box after root. Keep it at 600,
give every key an expiry, and renew them on purpose.
## Traps we hit while building it
- **Vaultwarden admin routes want JSON.** `POST /admin/users/<id>/disable`
without a JSON content type is not a 403, it is a 404, and it looks exactly
like "user not found".
- **Vaultwarden rate-limits the admin login** (a handful per five minutes). Two
test runs back to back and the third one gets 429. The script says so.
- **An old n8n API key cannot delete users.** Keys created before scopes
existed, or with the wrong ones, get 403. Create one with `user:delete`.
- **Forgejo tokens:** an admin can list a user's tokens (`Sudo:` header) but
deleting one needs the owner's password. `prohibit_login` blocks them, so the
script names them instead.
- **Outline 1.10** lists an admin only their own API keys. Suspending the user
refuses their keys and browser sessions anyway (measured: 401 and 403).
- **Group restriction in Pocket ID is a separate switch.** Setting allowed
groups on a client via the API leaves `isGroupRestricted` false, and the UI
then says "Any user can sign in to this client". Set both.