Lutz Finsterle 14f46e1082 Add unsealer, internal CA/TLS, auto-unseal, and automated cert+backup
Build out the home-lab OpenBAO deployment beyond the basic node:

- docker-compose: add openbao-unsealer sidecar; main node now transit
  auto-unseals against it (seal config kept in gitignored config/seal.hcl)
- policies/admin.hcl: non-root admin policy; per-engine rules for
  ssh/pki/pki_int/totp/transit
- Internal two-tier CA (pki/ root + pki_int/ intermediate) issues the
  openbao.famfi.home leaf Traefik serves; root CA published under ca/
- scripts/ + systemd/: daily cert renewal and Raft snapshot backups
  (both instances), with scoped tokens stored outside the repo
- README: full runbook (auto-unseal, PKI, renewal, backups, DR/restore)

Secrets (init/unsealer keys, tokens, seal stanza) stay gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:28:49 +02:00
2026-06-28 13:41:17 +02:00

OpenBAO — Home Lab Deployment

Single-node OpenBAO (v2.5.5) running via Docker Compose with integrated Raft storage. Suitable for a self-hosted home lab.

Layout

File Purpose
docker-compose.yml Main node + openbao-unsealer sidecar, Raft volumes
config/openbao.hcl Main server config: Raft storage, TCP listener, UI
config/seal.hcl Transit auto-unseal stanza (git-ignored — holds a token)
config-unsealer/ Config for the transit unsealer instance
policies/admin.hcl Non-root admin policy
scripts/, systemd/ Automated cert renewal (timer + script)
ca/ Internal root CA cert (public — for trusting on devices)

1. Start the server

docker compose up -d
docker compose logs -f openbao   # watch startup

The server starts sealed and uninitialized — this is expected.

2. Initialize (one time only)

This generates the unseal keys and the initial root token. Run it once.

docker compose exec openbao bao operator init \
  -key-shares=5 -key-threshold=3 -format=json > init-output.json

⚠️ init-output.json contains your unseal keys and root token. Store them in a password manager and delete the file afterward. It is git-ignored, but treat it like the master key to everything — because it is.

3. Unseal — automatic (transit auto-unseal)

The main node auto-unseals via the openbao-unsealer sidecar (transit seal, see "Auto-unseal" below). You normally never unseal it by hand. The original 5 Shamir keys are now recovery keys (for root-token regen / recovery operations), not unseal keys.

First-time bring-up only: a brand-new install is Shamir-sealed until you run bao operator init + bao operator unseal (3 keys) once, before migrating to transit auto-unseal.

Auto-unseal (transit via the unsealer sidecar)

openbao-unsealer is a tiny second OpenBAO instance that holds one transit key (autounseal). The main node's config/seal.hcl points at it and unwraps its root key on every start — so main-node restarts/upgrades need no manual unseal.

main openbao ──seal:transit──▶ openbao-unsealer (transit/autounseal)
  • The unsealer itself is Shamir-sealed (1 key). Its key + root token are in unsealer-init.json (git-ignored). On a full host reboot the unsealer comes up sealed, so unseal it once and the main node follows automatically:

    docker compose exec openbao-unsealer bao operator unseal <unsealer-key>
    
  • Caveat: the unsealer currently runs on the same host (SPOF). For real separation, relocate it to another host and point seal.hcl at it. To make host reboots fully hands-off, give the unsealer its own boot-unseal.

4. Log in & use

Day-to-day: use the non-root admin user (userpass auth, admin policy). Credentials are in admin-credentials.txt (git-ignored) — change the password and move it to your password manager.

# Local CLI (plaintext API is bound to loopback only):
export BAO_ADDR=http://127.0.0.1:8200
docker compose exec openbao bao login -method=userpass username=admin

# Break-glass only:
docker compose exec openbao bao login          # paste the root token

The admin policy (policies/admin.hcl) grants full day-to-day administration but not root-only operations (sys/raw, root-token generation, rekey). Keep the root token offline.

Enabled secrets engines

Engine Path Notes
KV v2 secret/ static key/value secrets
SSH ssh/ SSH CA — sign short-lived host/client certs (needs CA + roles configured)
PKI pki/ internal CA, max lease 10y (needs root/intermediate CA generated)
TOTP totp/ 2FA code generation/validation
Transit transit/ encryption-as-a-service (needs a key created)

These are mounted but not yet configured (no CAs/keys/roles). Each engine mounted at a new path has a matching rule in policies/admin.hcl; add one per future engine.

docker compose exec openbao bao kv put secret/myapp/db password=s3cr3t
docker compose exec openbao bao kv get secret/myapp/db

Access via Traefik (LAN-only HTTPS)

OpenBAO is fronted by the Traefik stack (../Traefik) at https://openbao.famfi.home — restricted to 192.168.0.0/16, TLS terminated by Traefik (default self-signed cert).

  • Traefik dynamic config: /srv/TRAEFIK/etc/traefik/traefik.d/openbao.yml
  • Traefik reaches the container by name (http://openbao:8200) over the shared traefik_proxy Docker network.
  • DNS action required: add an A record openbao.famfi.home → 192.168.0.142 (the websecure entrypoint IP) on your LAN DNS, or a hosts entry on clients.
  • The built-in web UI is served at https://openbao.famfi.home once DNS is set.
  • TLS uses a trusted cert from OpenBAO's own internal CA (see PKI section below) — once you install the root CA on a device, no browser warnings and no BAO_SKIP_VERIFY needed.

Internal CA (PKI) & the openbao.famfi.home cert

OpenBAO runs a two-tier internal CA and issues the cert Traefik serves:

Mount Role TTL
pki/ Root CA (famfi.home Internal Root CA) 10y
pki_int/ Intermediate CA (famfi.home Intermediate CA) 5y
pki_int/roles/famfi-home issuing role for *.famfi.home 90d max

The leaf for openbao.famfi.home lives in Traefik at /srv/TRAEFIK/etc/traefik/tls/openbao/ and is loaded via /srv/TRAEFIK/etc/traefik/traefik.d/tls-openbao.yml.

Trust the CA on your devices (one time) using ca/famfi-home-root-ca.pem:

# Linux (Debian/Ubuntu family):
sudo cp ca/famfi-home-root-ca.pem /usr/local/share/ca-certificates/famfi-home-root-ca.crt
sudo update-ca-certificates
# macOS: add to Keychain and mark trusted.  Windows: import to "Trusted Root CAs".
# Browsers (Firefox) use their own store — import there too.

Issue a cert for another .home service:

docker compose exec openbao bao write pki_int/issue/famfi-home \
  common_name="gitea.famfi.home" ttl=2160h

Renewal is automated. scripts/renew-openbao-cert.sh re-issues the leaf and reinstalls it for Traefik (rewriting traefik.d/tls-openbao.yml to force a reload — changing the cert file alone does not trigger one). A systemd timer (systemd/openbao-cert-renew.timer, installed to /etc/systemd/system) runs it daily; the script no-ops until the cert is within 21 days of expiry. The scoped renewal token lives at /etc/openbao-cert-renew.token (root-only).

sudo systemctl list-timers openbao-cert-renew.timer   # next run
sudo /home/lutz/Projects/OpenBAO/scripts/renew-openbao-cert.sh --force  # renew now

Backups (Raft snapshots) — automated

scripts/backup-raft-snapshots.sh snapshots both instances and prunes to the newest KEEP (default 14). A systemd timer (openbao-backup.timer) runs it daily at ~02:30. Snapshots land in /var/backups/openbao/{main,unsealer}/ (root-only, 0600). Scoped backup tokens: /etc/openbao-backup.token, /etc/openbao-unsealer-backup.token.

sudo /home/lutz/Projects/OpenBAO/scripts/backup-raft-snapshots.sh   # run now
sudo systemctl list-timers openbao-backup.timer                     # next run

⚠️ Backups are local to this Pi — if the disk dies you lose data and backups. Add an offsite copy (e.g. rsync the snapshot dirs to a Synology) for real DR. This is the most valuable next hardening step.

Disaster-recovery set (keep these together, offsite)

To rebuild from nothing you need all of:

  1. A main snapshot and an unsealer snapshot (same run).
  2. unsealer-init.json — the unsealer's unseal key (without it the unsealer can't be unsealed, so the main node can't be transit-unsealed).
  3. init-output.json — the main node's recovery keys + root token.

Restore outline

# 1. Restore the unsealer, unseal it (so transit auto-unseal works again):
docker compose cp <unsealer.snap> openbao-unsealer:/tmp/u.snap
docker compose exec openbao-unsealer bao operator raft snapshot restore /tmp/u.snap
docker compose exec openbao-unsealer bao operator unseal <unsealer-key>
# 2. Restore the main node (it auto-unseals via the unsealer):
docker compose cp <main.snap> openbao:/tmp/m.snap
docker compose exec openbao bao operator raft snapshot restore /tmp/m.snap

Hardening checklist (before storing real secrets)

  • Put TLS in front (Traefik, LAN-only, trusted internal-CA cert)
  • Create a non-root admin policy + user; stop using the root token day-to-day
  • Enable auto-unseal (transit via the openbao-unsealer sidecar)
  • Automate cert renewal (systemd timer)
  • Move the root token + recovery keys offline (out of init-output.json)
  • Schedule the snapshot backup (systemd timer, both instances)
  • Copy snapshots offsite (e.g. rsync to a Synology) — backups are local-only
  • Disable or encrypt swap on the host (OpenBAO 2.x dropped mlock support)
  • (Optional) Relocate the unsealer to a second host; consider 3-node HA
Description
No description provided
Readme 128 KiB
Languages
Shell 83.4%
HCL 16.6%