Initial Commit
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -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
|
||||
100
README.md
Normal file
100
README.md
Normal file
@@ -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)
|
||||
33
config/openbao.hcl
Normal file
33
config/openbao.hcl
Normal file
@@ -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
|
||||
39
docker-compose.yml
Normal file
39
docker-compose.yml
Normal file
@@ -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:
|
||||
39
policies/admin.hcl
Normal file
39
policies/admin.hcl
Normal file
@@ -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"] }
|
||||
Reference in New Issue
Block a user