# 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 ```bash 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. ```bash 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: ```bash docker compose exec openbao-unsealer bao operator unseal ``` - 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. ```bash # 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](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](policies/admin.hcl); add one per future engine. ```bash 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](ca/famfi-home-root-ca.pem): ```bash # 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:** ```bash 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). ```bash 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`. ```bash 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 ```bash # 1. Restore the unsealer, unseal it (so transit auto-unseal works again): docker compose cp 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 # 2. Restore the main node (it auto-unseals via the unsealer): docker compose cp openbao:/tmp/m.snap docker compose exec openbao bao operator raft snapshot restore /tmp/m.snap ``` ## Hardening checklist (before storing real secrets) - [x] Put TLS in front (Traefik, LAN-only, trusted internal-CA cert) - [x] Create a non-root admin policy + user; stop using the root token day-to-day - [x] Enable auto-unseal (transit via the `openbao-unsealer` sidecar) - [x] Automate cert renewal (systemd timer) - [ ] Move the root token + recovery keys offline (out of `init-output.json`) - [x] 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