commit 95e1b676e2d0229e915fbc8f3a98342990da09e0 Author: Lutz Finsterle Date: Sun Jun 28 13:41:17 2026 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbc8dbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# NEVER commit unseal keys, root tokens, or storage data +unseal-keys.json +init-output.json +admin-credentials.txt +*.token +config/tls/ +data/ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e86ed2 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# 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` | Container definition (port 8200, Raft data volume, IPC_LOCK) | +| `config/openbao.hcl` | Server config: Raft storage, TCP listener, UI | +| `.gitignore` | Keeps unseal keys / tokens / data out of git | + +## 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 + +OpenBAO starts sealed after every restart. Provide 3 of the 5 keys: + +```bash +docker compose exec openbao bao operator unseal # run 3x, paste a key each time +``` + +## 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. + +A KV v2 secrets engine is mounted at `secret/`: + +```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. +- Because the cert is self-signed, CLI clients pointed at the HTTPS name need the + CA trusted or `BAO_SKIP_VERIFY=true` (local CLI can just use the loopback + http://127.0.0.1:8200 instead). + +## Backups (Raft snapshots) + +```bash +docker compose exec openbao bao operator raft snapshot save /openbao/file/snap.bak +docker compose cp openbao:/openbao/file/snap.bak ./snap-$(date +%F).bak +``` + +## Hardening checklist (before storing real secrets) + +- [ ] Put TLS in front (reverse proxy) or enable native TLS in `openbao.hcl` +- [ ] Create a non-root admin policy + token; stop using the root token day-to-day +- [ ] Revoke or store the root token offline +- [ ] Enable auto-unseal (e.g. transit/KMS) if you don't want manual unseal on reboot +- [ ] Schedule the snapshot backup above +- [ ] Disable or encrypt swap on the host (OpenBAO 2.x dropped mlock support) diff --git a/config/openbao.hcl b/config/openbao.hcl new file mode 100644 index 0000000..a4ab583 --- /dev/null +++ b/config/openbao.hcl @@ -0,0 +1,33 @@ +ui = true + +# Integrated Raft storage — self-contained, supports snapshot backups. +# Path is /openbao/file because that directory is pre-created in the image +# owned by the unprivileged "openbao" user; a named volume mounted there +# inherits that ownership and is writable after the entrypoint drops root. +storage "raft" { + path = "/openbao/file" + node_id = "openbao-node-1" +} + +# API listener. TLS is disabled here for an initial home-lab setup; the +# expectation is that a reverse proxy (Caddy/Traefik/nginx) terminates TLS, +# or you enable native TLS below. Do NOT expose this port to the internet +# without TLS in front of it. +listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = 1 + + # To enable native TLS instead of a reverse proxy: + # tls_disable = 0 + # tls_cert_file = "/openbao/config/tls/cert.pem" + # tls_key_file = "/openbao/config/tls/key.pem" +} + +# Advertised addresses. api_addr is the external URL clients/UI use — here the +# Traefik-fronted HTTPS name. cluster_addr is for Raft peer traffic (single node). +api_addr = "https://openbao.famfi.home" +cluster_addr = "https://openbao:8201" + +# NOTE: OpenBAO 2.x removed mlock support. Instead, disable or encrypt swap +# on the host so secrets can't leak to disk. See: +# https://openbao.org/docs/install/#post-installation-hardening diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..11a1340 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + openbao: + image: openbao/openbao:2.5.5 + container_name: openbao + restart: unless-stopped + # The image entrypoint already runs `bao server -config=/openbao/config` + # (the whole config dir), so we only pass the subcommand here. + command: server + ports: + # Plaintext API bound to loopback only — local CLI/admin use. LAN clients + # reach OpenBAO via Traefik (TLS) at https://openbao.famfi.home instead. + - "127.0.0.1:8200:8200" + environment: + BAO_ADDR: "http://127.0.0.1:8200" + networks: + - default + - traefik_proxy + volumes: + - ./config:/openbao/config:ro + - openbao-data:/openbao/file + healthcheck: + # 200 = unsealed+active. Accept sealed (501) and standby (429) too, + # so the container is "healthy" once the API is responding at all. + test: ["CMD", "bao", "status", "-address=http://127.0.0.1:8200"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 10s + +networks: + # Project-internal network (default). + default: + # Shared with the Traefik stack so Traefik can reach this container by name + # (http://openbao:8200). Created by the Traefik compose project. + traefik_proxy: + external: true + +volumes: + openbao-data: diff --git a/policies/admin.hcl b/policies/admin.hcl new file mode 100644 index 0000000..a555d3b --- /dev/null +++ b/policies/admin.hcl @@ -0,0 +1,39 @@ +# Admin policy — broad day-to-day administration WITHOUT being root. +# Deliberately omits the most dangerous root-only capabilities (sys/raw, +# generating root tokens, re-keying, raw storage access). Use the root token +# only for break-glass operations; this policy is for normal admin work. + +# Manage auth methods +path "auth/*" { capabilities = ["create", "read", "update", "patch", "delete", "list", "sudo"] } +path "sys/auth/*" { capabilities = ["create", "read", "update", "delete", "sudo"] } +path "sys/auth" { capabilities = ["read"] } + +# Manage ACL policies +path "sys/policies/acl/*" { capabilities = ["create", "read", "update", "delete", "list"] } +path "sys/policies/acl" { capabilities = ["list"] } + +# Manage secrets engines (mount/tune/unmount) +path "sys/mounts/*" { capabilities = ["create", "read", "update", "delete", "sudo"] } +path "sys/mounts" { capabilities = ["read"] } + +# Work with secrets data in mounted engines. +path "secret/*" { capabilities = ["create", "read", "update", "patch", "delete", "list"] } +path "ssh/*" { capabilities = ["create", "read", "update", "patch", "delete", "list", "sudo"] } +# NOTE: each secrets engine mounted at a NEW path needs its own rule here +# (e.g. add `path "pki/*"` when you enable a PKI engine). The UI runs a +# preflight capability check on the mount path, so a missing rule = 403. + +# Lease management +path "sys/leases/*" { capabilities = ["create", "read", "update", "delete", "list", "sudo"] } + +# Operational visibility +path "sys/health" { capabilities = ["read", "sudo"] } +path "sys/seal-status" { capabilities = ["read"] } +path "sys/mounts/+/tune" { capabilities = ["read", "update"] } +path "sys/capabilities" { capabilities = ["create", "update"] } +path "sys/capabilities-self" { capabilities = ["create", "update"] } + +# Allow token self-management +path "auth/token/lookup-self" { capabilities = ["read"] } +path "auth/token/renew-self" { capabilities = ["update"] } +path "auth/token/revoke-self" { capabilities = ["update"] }