From d5398caece5f3f9b95515b0f0dacb4a679317a2c Mon Sep 17 00:00:00 2001 From: Maximilian Grimm Date: Wed, 26 Aug 2026 21:54:23 +0200 Subject: [PATCH] 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 --- .env.example | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ HARDENING.md | 16 +++++++++++++++- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ compose.yml | 18 ++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..422e345 --- /dev/null +++ b/.env.example @@ -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./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./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()))' +# And register it on the Forgejo side: +# docker compose exec -u git forgejo forgejo forgejo-cli actions register \ +# --name box-runner --labels docker --secret +# A registration token copied from the web UI does NOT work here. +RUNNER_SECRET= +RUNNER_UUID= + diff --git a/HARDENING.md b/HARDENING.md index f2b3358..c773553 100644 --- a/HARDENING.md +++ b/HARDENING.md @@ -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. - 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: ``` @@ -89,7 +103,7 @@ Plus: enable your provider's server snapshots. Two clicks, saves your company. ## 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. Baseline first. Perfect later. diff --git a/README.md b/README.md index be145e7..5e38b33 100644 --- a/README.md +++ b/README.md @@ -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 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./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. +# 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 Before you put real company data on this box, read **[HARDENING.md](HARDENING.md)**. diff --git a/compose.yml b/compose.yml index 935ce7c..4306c7c 100644 --- a/compose.yml +++ b/compose.yml @@ -126,6 +126,24 @@ services: DOMAIN: https://vault.${DOMAIN} ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN} 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: - vaultwarden_data:/data networks: [proxy]