selfhostedops-stack/offboarding
Maximilian Grimm 54ab43dac7 Episode 15: offboarding waits out the Vaultwarden admin rate limit, fail2ban exception only on the provider host
Measured clean run: Pocket ID disable alone left 12 of 14 ways open, the
script (0.9 s, 9 of 9 steps) left 1 of 14.
2026-09-23 12:08:08 +02:00
..
offboard.env.example Episode 15: Pocket ID as identity provider, tinyauth gate, offboarding script 2026-09-23 11:52:17 +02:00
offboard.py Episode 15: offboarding waits out the Vaultwarden admin rate limit, fail2ban exception only on the provider host 2026-09-23 12:08:08 +02:00
README.md Episode 15: offboarding waits out the Vaultwarden admin rate limit, fail2ban exception only on the provider host 2026-09-23 12:08:08 +02:00

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> (0.9 s, all 9 steps ok) 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:

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 now waits 60 s and retries, up to six times, and prints each wait.
  • 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.