Episode 4: Vaultwarden single sign-on via Forgejo

- compose: SSO_ENABLED, SSO_AUTHORITY and the two client credentials, with the
  undocumented redirect path written down where people will look for it
- .env.example: was referenced by the README since episode 1 but never actually
  committed, so a fresh clone could not follow the quickstart
- README: the four steps, plus the debugging notes that cost the most time
  (no SSO line at startup, 404 on routes without query parameters, HEAD on the
  discovery endpoint, and the three-step member join)
- HARDENING: never copy db.sqlite3 alone while the container runs. WAL mode
  keeps recent writes in a side file, so that backup silently misses data
This commit is contained in:
Maximilian Grimm 2026-08-26 21:54:23 +02:00
parent 0341ae777d
commit d5398caece
4 changed files with 130 additions and 1 deletions

48
.env.example Normal file
View file

@ -0,0 +1,48 @@
# ~$ selfhostedops — copy me: cp .env.example .env
# Generate secrets with: openssl rand -hex 32
# Your base domain. Services live at git.* wiki.* vault.* status.* n8n.* vpn.*
# DNS: one wildcard A (and AAAA) record *.ops.example.com -> your server IP
DOMAIN=ops.example.com
# Let's Encrypt notifications + expiry mails
ACME_EMAIL=you@example.com
TZ=Europe/Berlin
# --- Outline ---
OUTLINE_PG_PASSWORD=CHANGE_ME_openssl_rand_hex_32
OUTLINE_SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
OUTLINE_UTILS_SECRET=CHANGE_ME_openssl_rand_hex_32
# From Forgejo: Site Administration -> Applications -> OAuth2
# Redirect URI: https://wiki.<DOMAIN>/auth/oidc.callback
OUTLINE_OIDC_CLIENT_ID=
OUTLINE_OIDC_CLIENT_SECRET=
# --- Vaultwarden ---
# Admin panel token (argon2 hash recommended, plain string works to start)
VAULTWARDEN_ADMIN_TOKEN=CHANGE_ME_openssl_rand_hex_32
# Flip to false once your team has signed up:
VAULTWARDEN_SIGNUPS_ALLOWED=true
# Single sign-on against your own Forgejo (episode 4).
# From Forgejo: Site Administration -> Applications -> OAuth2 Application
# Redirect URI must be EXACTLY: https://vault.<DOMAIN>/identity/connect/oidc-signin
VAULTWARDEN_SSO_CLIENT_ID=
VAULTWARDEN_SSO_CLIENT_SECRET=
# --- wg-easy (optional, --profile vpn) ---
# Generate with: docker run --rm ghcr.io/wg-easy/wg-easy wgpw 'yourpassword'
WGEASY_PASSWORD_HASH=
# --- CI (episode 2, optional: docker compose --profile ci up -d) ---
# One shared secret registers the runner declaratively. Generate it with:
# openssl rand -hex 20
# Then derive the UUID Forgejo will know it by:
# python3 -c 'import uuid,sys;print(uuid.UUID(bytes=sys.argv[1][:16].encode()))' <secret>
# And register it on the Forgejo side:
# docker compose exec -u git forgejo forgejo forgejo-cli actions register \
# --name box-runner --labels docker --secret <secret>
# A registration token copied from the web UI does NOT work here.
RUNNER_SECRET=
RUNNER_UUID=

View file

@ -58,6 +58,20 @@ apt install -y fail2ban # default ssh jail is enough to start
- `VAULTWARDEN_SIGNUPS_ALLOWED=false` the moment your team is on board. - `VAULTWARDEN_SIGNUPS_ALLOWED=false` the moment your team is on board.
- Use an argon2 `ADMIN_TOKEN` (`vaultwarden hash` / see wiki), not a plain string. - Use an argon2 `ADMIN_TOKEN` (`vaultwarden hash` / see wiki), not a plain string.
A plain one makes Vaultwarden print a NOTICE on every start. It is telling
the truth.
- **Never back up `db.sqlite3` on its own while the container runs.** The
database is in WAL mode: recent writes live in `db.sqlite3-wal` and are not
in the main file yet. Copy it alone and you get a backup that silently
misses your newest users and entries, and you find out on restore day.
Either stop the container first (see section 7), or take a consistent copy:
```bash
docker compose exec vaultwarden sh -c "sqlite3 /data/db.sqlite3 \".backup '/data/backup.sqlite3'\""
```
- Restore the backup somewhere else, once, before you trust it. A backup you
have never restored is a belief, not a backup.
- The admin panel (`/admin`) is for setup — consider blocking it in Caddy afterwards: - The admin panel (`/admin`) is for setup — consider blocking it in Caddy afterwards:
``` ```
@ -89,7 +103,7 @@ Plus: enable your provider's server snapshots. Two clicks, saves your company.
## 8. What this list is NOT ## 8. What this list is NOT
Not covered here, on purpose (own episodes): SSO everywhere, 2FA enforcement Not covered here, on purpose (own episodes): 2FA enforcement
per service, Netbird zero-trust mesh, central logging, CrowdSec, offsite borg. per service, Netbird zero-trust mesh, central logging, CrowdSec, offsite borg.
Baseline first. Perfect later. Baseline first. Perfect later.

View file

@ -140,6 +140,55 @@ command (`examples/deploy-hello-ops.sh`). Read the CI section of
[HARDENING.md](HARDENING.md) before you wire the deploy step — there are two [HARDENING.md](HARDENING.md) before you wire the deploy step — there are two
comfortable ways to hand your whole server to anyone who can push a branch. comfortable ways to hand your whole server to anyone who can push a branch.
## One login for the vault (Episode 4)
Your Forgejo already knows your team. Let it log them into the password
manager too, so offboarding is one switch instead of a checklist.
```bash
# 1. Create the OAuth2 app in Forgejo
# Site Administration -> Applications -> OAuth2 Application
# Name: Vaultwarden
# Redirect URI: https://vault.<DOMAIN>/identity/connect/oidc-signin
# Confidential client: yes
#
# That redirect path is not documented anywhere in the UI, and a wrong one
# only fails AFTER the Forgejo login. Copy it character for character.
# 2. Put the client id and secret in .env
VAULTWARDEN_SSO_CLIENT_ID=...
VAULTWARDEN_SSO_CLIENT_SECRET=...
# 3. Restart just that container
docker compose up -d vaultwarden
# 4. Log in: https://vault.<DOMAIN>
# Type your work mail, hit "Use single sign-on", authorize once in Forgejo.
```
Vaultwarden will then ask you to set a master password. That is not a bug.
Forgejo proved *who you are*; nothing so far produced the key that *decrypts
your vault*. Your master password never leaves your browser, and the server
stores blobs it cannot read. Two different jobs, two different mechanisms.
Debugging notes, all of them learned the hard way:
- Vaultwarden logs nothing about SSO at startup. It builds the OIDC client on
the first login attempt, not before. Watch `docker compose logs -f
vaultwarden` *while* you log in instead: you will see a `prevalidate` call
and then the `oidc-signin` callback coming back with a code.
- `/identity/connect/authorize` and the callback both answer `404` when called
without query parameters. That is route matching, not a wrong path.
- `curl -I` on Forgejo's `/.well-known/openid-configuration` returns 404 because
there is no HEAD route. Use `curl -s` and it is a normal 200.
- Turn on `SSO_SIGNUPS_MATCH_EMAIL` **before** anyone logs in, or you get
duplicate accounts instead of linked ones.
Then make it a team vault: create an organization, add collections, invite
people. Note that joining is three steps, not two: you invite, they log in,
and then an owner has to **confirm** them. Until that confirmation, a member
sees no shared items at all.
## Hardening ## Hardening
Before you put real company data on this box, read **[HARDENING.md](HARDENING.md)**. Before you put real company data on this box, read **[HARDENING.md](HARDENING.md)**.

View file

@ -126,6 +126,24 @@ services:
DOMAIN: https://vault.${DOMAIN} DOMAIN: https://vault.${DOMAIN}
ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN} ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN}
SIGNUPS_ALLOWED: "${VAULTWARDEN_SIGNUPS_ALLOWED:-true}" # set to false after your team joined! SIGNUPS_ALLOWED: "${VAULTWARDEN_SIGNUPS_ALLOWED:-true}" # set to false after your team joined!
# --- Single sign-on via your own Forgejo (episode 4) ---
# Create the OAuth2 app in Forgejo FIRST: Site Administration ->
# Applications -> OAuth2. Its redirect URI must be exactly:
# https://vault.${DOMAIN}/identity/connect/oidc-signin
# That path is not documented in the UI. Getting it wrong fails only
# AFTER the Forgejo login, which is why it costs people twenty minutes.
SSO_ENABLED: "true"
# Just the base URL. Vaultwarden reads Forgejo's discovery document
# (/.well-known/openid-configuration) from here on the first login.
SSO_AUTHORITY: https://git.${DOMAIN}
SSO_CLIENT_ID: ${VAULTWARDEN_SSO_CLIENT_ID}
SSO_CLIENT_SECRET: ${VAULTWARDEN_SSO_CLIENT_SECRET}
# Vaultwarden prepends "openid" itself, so listing it here again shows up
# duplicated on the Forgejo consent screen. Harmless, but untidy.
SSO_SCOPES: "profile email"
# Attach an SSO login to the existing account with the same mail address
# instead of creating a duplicate. Turn this on BEFORE people log in.
SSO_SIGNUPS_MATCH_EMAIL: "true"
volumes: volumes:
- vaultwarden_data:/data - vaultwarden_data:/data
networks: [proxy] networks: [proxy]