diff --git a/README.md b/README.md index cdd32a0..d624df5 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,13 @@ deployments// # one folder per stack | [`squid`](deployments/squid/) | SSL-bump caching forward proxy — static-content cache + TLS interception via a local CA. **The exception: a forward proxy, not a Caddy/LE site.** | — | | [`copyparty`](deployments/copyparty/) | Portable file server — web UI/WebDAV behind Caddy, plus direct **SFTP** + **FTPS**. Ships a security-notices-aware updater. | — | | [`simplex`](deployments/simplex/) | SimpleX SMP + XFTP relay with Tor hidden services + encrypted backups. | globals/age-pubkey.txt | +| [`openbao`](deployments/openbao/) | Hardened tape-encryption key store for [Kanrisha](deployments/kanrisha/). **Exception: native TLS on the LAN, no Caddy/LE.** raft storage, mlock, self-signed or Smallstep-ACME cert, manual/HSM unseal. | globals/age-pubkey.txt (backups) | ## Conventions -- **Alpine + Docker Compose + Caddy/Let's Encrypt** across every stack (except - `squid`, which is a forward proxy with a local TLS-interception CA — no Caddy/LE). +- **Alpine + Docker Compose + Caddy/Let's Encrypt** across every stack, with two + exceptions: `squid` (a forward proxy with a local TLS-interception CA) and + `openbao` (a same-LAN secrets store with native TLS) — neither uses Caddy/LE. - **`build.sh` → `deploy.sh`**: each stack's `build.sh` embeds its `docker-compose.yml` / `Caddyfile` / `.env.example` into a single self-contained `deploy.sh` (base64 tar.gz). That one file can be `scp`'d to a diff --git a/automations.sh b/automations.sh index 65c5197..d0e3244 100644 --- a/automations.sh +++ b/automations.sh @@ -63,7 +63,7 @@ fi . "$ROOT/scripts/lib.sh" load_globals -DEPLOYMENTS=(pocket-id beszel headscale webfinger squid copyparty simplex) +DEPLOYMENTS=(pocket-id beszel headscale webfinger squid copyparty simplex openbao) SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update) # ---------------------------------------------------------------------------- @@ -126,6 +126,9 @@ ask_deployment_vars() { ask XFTP_QUOTA "XFTP disk quota" optional ask SSH_PORT "SSH port" optional ask ALLOWED_IP "Your IP to whitelist in sshguard" optional ;; + openbao) + ask OPENBAO_ADDR "LAN address the Kanrisha tape host reaches the vault at (IP or DNS)" + ask OPENBAO_BIND "Host IP to bind the API on (blank = 0.0.0.0)" optional ;; esac } diff --git a/deployments/openbao/.env.example b/deployments/openbao/.env.example new file mode 100644 index 0000000..72e3c92 --- /dev/null +++ b/deployments/openbao/.env.example @@ -0,0 +1,31 @@ +# Copy to .env and fill in. docker compose picks .env up automatically. +# deploy.sh seeds .env from this on first run; an existing .env is never +# overwritten. Never commit the populated .env. + +# ─── Reachability ─────────────────────────────────────────────────────────── +# Address (IP or DNS name) the KANRISHA tape host uses to reach this vault on +# the LAN. Seeds the self-signed cert SAN below and is what you point +# [encryption.openbao].address at (https://${OPENBAO_ADDR}:8200). +OPENBAO_ADDR=10.0.0.10 + +# Interface the published API port binds to on the host. Default is all +# interfaces; set the LAN IP to avoid listening anywhere else. +OPENBAO_BIND=0.0.0.0 + +# ─── TLS ──────────────────────────────────────────────────────────────────── +# Comma-separated SANs for the self-signed cert deploy.sh generates. Include +# EVERY name/IP the tape host might use (deploy.sh appends OPENBAO_ADDR + the +# loopback for you). To use a CA-signed cert instead (e.g. Smallstep over ACME), +# drop tls.crt + tls.key into ./tls and these are ignored — see the README. +OPENBAO_TLS_SANS=DNS:localhost,IP:127.0.0.1 +OPENBAO_TLS_DAYS=825 + +# ─── Image tag ────────────────────────────────────────────────────────────── +# Pin for reproducible deploys. +OPENBAO_TAG=2.5.5 + +# ─── Auto-unseal (optional; default is MANUAL unseal) ─────────────────────── +# Only used when the seal "pkcs11" stanza is enabled in config.hcl (and the HSM +# module/device is mounted into the container). Otherwise leave blank and unseal +# manually after each restart. +OPENBAO_HSM_PIN= diff --git a/deployments/openbao/.gitignore b/deployments/openbao/.gitignore new file mode 100644 index 0000000..74dc083 --- /dev/null +++ b/deployments/openbao/.gitignore @@ -0,0 +1,3 @@ +# Never commit generated TLS material (the listener key) or a populated .env. +/tls/ +.env diff --git a/deployments/openbao/README.md b/deployments/openbao/README.md new file mode 100644 index 0000000..0b2d572 --- /dev/null +++ b/deployments/openbao/README.md @@ -0,0 +1,109 @@ +# openbao + +Hardened [OpenBao](https://openbao.org) — the tape-encryption key store for +[Kanrisha](../kanrisha/). Deliberately **separate from the tape host**: a +compromise of the tape node must not reach the vault, and OpenBao's +mlock/TLS/unseal lifecycle is cleaner on its own box. + +Unlike the other stacks here there is **no Caddy / Let's Encrypt** — a secrets +store terminates TLS itself and is reached over the **LAN**, not the public +internet. + +- **Native TLS** on the listener — self-signed by default, or a CA-signed cert + from your Smallstep CA over ACME. +- **Integrated raft storage** — snapshot-based DR. +- **mlock on** — key material never hits swap. +- **Manual unseal by default**, or PKCS#11 HSM auto-unseal. + +> ⚠️ This vault is the **sole recovery path for encrypted tapes.** Losing the +> OpenBao data *and* the unseal keys/root token loses every encrypted tape. Take +> raft snapshots and store the unseal material out of band (below). + +## Required `.env` values + +| Variable | Notes | +|----------|-------| +| `OPENBAO_ADDR` | IP/DNS the Kanrisha tape host uses to reach this vault (goes in the cert SAN; you point the daemon at `https://$OPENBAO_ADDR:8200`). | +| `OPENBAO_BIND` | Host interface the API binds to. Default all; set the LAN IP to narrow it. | +| `OPENBAO_TLS_SANS` | Extra SANs for the self-signed cert (deploy.sh adds `OPENBAO_ADDR` + loopback). | +| `OPENBAO_TAG` | OpenBao image tag (pin it). | +| `OPENBAO_HSM_PIN` | Only for PKCS#11 auto-unseal. Leave blank for manual unseal. | + +See [`.env.example`](.env.example) for the full list. + +## Deploy + +```bash +./automations.sh # Deploy on this host → deploy: openbao +# or, non-interactive: +OPENBAO_ADDR=10.0.0.10 SKIP_PROMPTS=1 bash deployments/openbao/deploy.sh +``` + +`deploy.sh` installs Docker, generates a self-signed TLS cert (if none present), +seeds `.env`, disables swap (for mlock), firewalls `8200/tcp`, and brings the +stack up. OpenBao starts **sealed** — initialise + unseal once: + +```bash +docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \ + bao operator init -tls-skip-verify # prints 5 unseal keys + root token +docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \ + bao operator unseal -tls-skip-verify # x3, three different keys +``` + +**Store the unseal keys + root token out of band** — ideally age-encrypted with +your backup recipient (`globals/age-pubkey.txt`), never on this host. + +## TLS + +- **Self-signed (default):** `deploy.sh` runs [`gen-tls.sh`](gen-tls.sh) to + create `./tls/tls.{crt,key}` with `OPENBAO_ADDR` in the SAN. Hand `tls.crt` to + the Kanrisha daemon as `[encryption.openbao].openbao_ca_cert`. +- **Smallstep CA over ACME (option):** issue a cert from your `step-ca` and drop + it in `./tls` instead — `gen-tls.sh` then no-ops. e.g. with the `step` client: + + ```bash + step ca certificate "$OPENBAO_ADDR" ./tls/tls.crt ./tls/tls.key \ + --provisioner acme --acme https://ca.lan/acme/acme/directory + # renew on a timer: step ca renew --daemon ./tls/tls.crt ./tls/tls.key + ``` + + Give the Kanrisha daemon your Smallstep **root** as `openbao_ca_cert` (then it + trusts the vault without `-tls-skip-verify`). + +## Auto-unseal (optional) + +Default is manual unseal after each restart. For hands-off restarts, enable the +`seal "pkcs11"` stanza in [`config.hcl`](config.hcl), mount the PKCS#11 module + +device into the `openbao` service, and set `OPENBAO_HSM_PIN` in `.env`. + +## Bootstrap for Kanrisha + +Once unsealed, enable KV v2 + AppRole and seed the domain key with the Kanrisha +bootstrap script (from the Kanrisha repo, `deploy/openbao/bootstrap.sh`): + +```bash +BAO_ADDR=https://$OPENBAO_ADDR:8200 BAO_CACERT=./tls/tls.crt \ + BAO_TOKEN= bash bootstrap.sh +``` + +It enables the `kanrisha-tape` KV-v2 mount (with **effectively-unlimited** +`max_versions` so a key rotation never orphans old tapes), creates the AppRole + +policy, seeds the domain key, and prints the `[encryption.openbao]` block for the +Kanrisha config. Point the daemon at `address = "https://$OPENBAO_ADDR:8200"`. + +## Backup / DR + +The vault is the sole recovery path for encrypted tapes — back it up: + +```bash +# Consistent raft snapshot (safe while running): +docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \ + bao operator raft snapshot save -tls-skip-verify /openbao/data/snapshot.snap +# then copy it off-box, age-encrypted: +docker compose cp openbao:/openbao/data/snapshot.snap - | \ + age -r "$(cat ../../globals/age-pubkey.txt)" > openbao-$(date +%F).snap.age +``` + +Restore: `bao operator raft snapshot restore`. Snapshots do **not** contain the +unseal keys — you still need those (and the root token) to unseal a restored +vault, which is why they are stored separately. diff --git a/deployments/openbao/build.sh b/deployments/openbao/build.sh new file mode 100644 index 0000000..41e9a0e --- /dev/null +++ b/deployments/openbao/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# build.sh -- (re)embed docker-compose.yml, config.hcl, gen-tls.sh and +# .env.example into deploy.sh as a base64-encoded tar.gz payload after +# __ARCHIVE_BELOW__. Idempotent: strips any existing payload first. + +set -euo pipefail + +DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +SCRIPT="$DIR/deploy.sh" +MARKER="__ARCHIVE_BELOW__" + +[[ -f "$SCRIPT" ]] || { echo "deploy.sh not found at $SCRIPT" >&2; exit 1; } +for f in docker-compose.yml config.hcl gen-tls.sh .env.example; do + [[ -f "$DIR/$f" ]] || { echo "Missing $DIR/$f" >&2; exit 1; } +done + +PAYLOAD=$(tar -czf - -C "$DIR" docker-compose.yml config.hcl gen-tls.sh .env.example | base64) + +TMP=$(mktemp) +trap 'rm -f "$TMP"' EXIT + +sed "/^${MARKER}\$/,\$d" "$SCRIPT" > "$TMP" +{ + echo "$MARKER" + echo "$PAYLOAD" +} >> "$TMP" + +mv "$TMP" "$SCRIPT" +chmod +x "$SCRIPT" +trap - EXIT + +size=$(wc -c < "$SCRIPT") +echo "Built $SCRIPT (${size} bytes)" diff --git a/deployments/openbao/cloud-init.yml b/deployments/openbao/cloud-init.yml new file mode 100644 index 0000000..4c72096 --- /dev/null +++ b/deployments/openbao/cloud-init.yml @@ -0,0 +1,37 @@ +#cloud-config +# +# OpenBao (Kanrisha tape-encryption key store) — harden SSH, then deploy, on a +# fresh Alpine/Debian/Alma host on the SAME LAN as the Kanrisha tape host. +# +# Fill in REPO_URL and the values in the runcmd block, then paste this as the +# instance user-data. No public DNS / 80 / 443 needed — this is a LAN vault with +# native TLS on 8200. After boot, initialise + unseal once (see the README). + +packages: + - git + +runcmd: + - hostnamectl set-hostname openbao || true + - | + set -e + REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git + REPO_BRANCH=main + HARDEN_SSH=1 # harden SSH on this fresh VM (set 0 to skip) + SSH_PORT=22 + ALLOWED_IP= # optional: whitelist your client IP in sshguard + git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations + cd /opt/automations + + # Harden SSH: PQ KEX, key-only auth, sshguard. Seeds root from + # globals/authorized_keys (or SSH_KEYS_URL). + if [ "$HARDEN_SSH" = 1 ]; then + SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" SKIP_PROMPTS=1 FORCE=1 \ + bash scripts/harden-ssh.sh + fi + + # Deploy OpenBao. OPENBAO_ADDR is the LAN address the Kanrisha tape host + # reaches this vault at — it goes in the TLS cert SAN. deploy.sh disables + # swap (for mlock) and firewalls 8200/tcp. + OPENBAO_ADDR=10.0.0.10 \ + SKIP_PROMPTS=1 \ + bash deployments/openbao/deploy.sh diff --git a/deployments/openbao/config.hcl b/deployments/openbao/config.hcl new file mode 100644 index 0000000..d728637 --- /dev/null +++ b/deployments/openbao/config.hcl @@ -0,0 +1,51 @@ +# OpenBao — hardened, same-LAN tape-encryption key store for Kanrisha. +# +# Native TLS on the listener (no reverse proxy in front), integrated raft +# storage (clean snapshot-based DR), and mlock enabled. This vault is the SOLE +# recovery path for encrypted tapes — losing the OpenBao data AND the unseal +# material loses every encrypted tape. Back it up (raft snapshots) and store the +# unseal keys/root token out of band (see the README + DR runbook). + +storage "raft" { + path = "/openbao/data" + node_id = "openbao-1" +} + +# TLS-terminating listener. tls.crt/tls.key are self-signed by deploy.sh on first +# run, or a CA-signed pair you drop in ./tls (e.g. issued by your Smallstep CA +# over ACME — see the README). The Kanrisha daemon trusts this via +# [encryption.openbao].openbao_ca_cert. +listener "tcp" { + address = "0.0.0.0:8200" + tls_cert_file = "/openbao/tls/tls.crt" + tls_key_file = "/openbao/tls/tls.key" + # Optional mTLS — require + verify a client cert from the tape host: + # tls_require_and_verify_client_cert = true + # tls_client_ca_file = "/openbao/tls/client-ca.crt" +} + +# mlock keeps key material off swap — REQUIRED. Needs cap_add: [IPC_LOCK] + +# ulimits memlock unlimited (docker-compose.yml) and swap disabled on the host. +# Do NOT add `disable_mlock = true`. + +# Single-node raft: advertising 127.0.0.1 is fine because a lone node is always +# ACTIVE and never redirects clients (they connect straight to the LAN address). +# For HA / multiple nodes, set BAO_API_ADDR + BAO_CLUSTER_ADDR to each node's +# reachable LAN address in the compose env instead. +api_addr = "https://127.0.0.1:8200" +cluster_addr = "https://127.0.0.1:8201" +ui = false + +# ── Auto-unseal (optional) — DEFAULT IS MANUAL UNSEAL ──────────────────────── +# With no seal stanza OpenBao starts SEALED and needs `bao operator unseal` (x3) +# after every restart. To auto-unseal against a PKCS#11 HSM/token, uncomment and +# fill this in, mount the PKCS#11 module + device into the container, and set +# OPENBAO_HSM_PIN in .env: +# +# seal "pkcs11" { +# lib = "/usr/lib/softhsm/libsofthsm2.so" +# slot = "0" +# pin = "env://BAO_HSM_PIN" +# key_label = "kanrisha-unseal" +# mechanism = "0x1087" # CKM_AES_GCM +# } diff --git a/deployments/openbao/deploy.sh b/deployments/openbao/deploy.sh new file mode 100644 index 0000000..9bf9589 --- /dev/null +++ b/deployments/openbao/deploy.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +# +# deploy.sh -- deploy the hardened OpenBao tape-encryption key store on a +# same-LAN host (Alpine / Debian / Alma). Single-node, runs as root. +# +# What this does: +# 1. Installs docker + compose if missing. +# 2. Lays down docker-compose.yml, config.hcl, gen-tls.sh in $STACK_DIR. +# 3. Seeds .env on first run (OPENBAO_ADDR into the cert SAN); never +# overwrites an existing .env. +# 4. Generates a self-signed TLS cert (if ./tls is empty) -- drop a CA-signed +# pair there instead to use your Smallstep CA. +# 5. Disables swap (so mlock is meaningful) and opens 8200/tcp. +# 6. Pulls images and brings the stack up. OpenBao starts SEALED -- initialise +# + unseal once afterwards (printed at the end). +# +# Idempotent: re-run to apply config changes / pull new images. +# +# Self-contained: docker-compose.yml, config.hcl, gen-tls.sh, .env.example are +# embedded as a base64 tar.gz at the bottom. Rebuild with build.sh after edits. +# +# Usage: +# OPENBAO_ADDR=10.0.0.10 bash deploy.sh # interactive prompt for the rest +# OPENBAO_ADDR=vault.lan SKIP_PROMPTS=1 bash deploy.sh +# STACK_DIR=/opt/openbao bash deploy.sh + +set -euo pipefail + +: "${STACK_DIR:=/srv/openbao}" +: "${SKIP_DOCKER_INSTALL:=0}" +: "${FORCE:=0}" +: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts +[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1 +: "${OPENBAO_ADDR:=}" +: "${OPENBAO_BIND:=0.0.0.0}" +: "${DISABLE_SWAP:=1}" # set 0 to skip swapoff (mlock then only best-effort) + +log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } + +[[ $EUID -eq 0 ]] || die "Run as root." + +# --------------------------------------------------------------------------- +# OS detection + Docker install (Alpine / Debian / Alma). Inlined so this +# deploy.sh stays self-contained when scp'd standalone. +# --------------------------------------------------------------------------- +osfam() { + local id="" like="" + if [[ -r /etc/os-release ]]; then + id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")" + like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")" + fi + case " $id $like " in + *" alpine "*) echo alpine ;; + *" debian "*|*" ubuntu "*) echo debian ;; + *" rhel "*|*" fedora "*|*" centos "*) echo rhel ;; + *) echo "${id:-unknown}" ;; + esac +} + +install_docker() { + [[ "$SKIP_DOCKER_INSTALL" == "1" ]] && { log "Skipping Docker install."; return; } + if command -v docker >/dev/null 2>&1; then + log "Docker already installed: $(docker --version)" + else + log "Installing Docker (OS: $(osfam))..." + case "$(osfam)" in + alpine) apk add -q docker docker-cli-compose openrc ;; + debian|rhel) command -v curl >/dev/null 2>&1 || \ + { command -v apt-get >/dev/null 2>&1 && apt-get install -y -qq curl; } || \ + { command -v dnf >/dev/null 2>&1 && dnf install -y -q curl; } + curl -fsSL https://get.docker.com | sh ;; + *) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;; + esac + fi + if command -v rc-update >/dev/null 2>&1; then + rc-update add docker default >/dev/null 2>&1 || true + rc-service docker status >/dev/null 2>&1 || rc-service docker start + elif command -v systemctl >/dev/null 2>&1; then + systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true + fi +} + +open_bao_port() { + # Register 8200/tcp (the vault API). Prefer the host firewall when present; + # else ufw/firewalld if active. Restrict the source to the tape host where + # you can -- this is a secrets store, not a public service. + if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then + log "Registering 8200/tcp with host firewall..." + printf '8200/tcp\n' > /etc/firewall/ports.d/openbao.rule + /usr/local/sbin/firewall-apply + elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then + log "ufw active -- allowing 8200/tcp..." + ufw allow 8200/tcp >/dev/null + elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then + log "firewalld active -- allowing 8200/tcp..." + firewall-cmd -q --add-port=8200/tcp --permanent + firewall-cmd -q --reload + fi +} + +disable_swap() { + [[ "$DISABLE_SWAP" == "1" ]] || { warn "DISABLE_SWAP=0 -- mlock will be best-effort."; return; } + if swapon --show 2>/dev/null | grep -q .; then + log "Disabling swap (mlock keeps key material off disk)..." + swapoff -a || warn "swapoff failed -- disable swap manually." + fi + # Persist: comment any swap lines in fstab so it stays off across reboots. + if [[ -f /etc/fstab ]] && grep -qE '^[^#].*\sswap\s' /etc/fstab; then + sed -i.bak -E 's|^([^#].*\sswap\s.*)$|# \1 # disabled for OpenBao mlock|' /etc/fstab + log "Commented swap entries in /etc/fstab (backup: /etc/fstab.bak)." + fi +} + +# ---------------------------------------------------------------------------- +# Extract embedded archive (docker-compose.yml, config.hcl, gen-tls.sh, .env.example) +# ---------------------------------------------------------------------------- +SCRIPT_DIR=$(mktemp -d -t openbao-deploy.XXXXXX) +trap 'rm -rf "$SCRIPT_DIR"' EXIT + +extract_archive() { + grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \ + | tail -n +2 \ + | base64 -d \ + | tar -xz -C "$SCRIPT_DIR" 2>/dev/null || true +} +extract_archive +# Fallback: run straight from the source dir (before build.sh embeds a payload). +if [[ ! -f "$SCRIPT_DIR/docker-compose.yml" ]]; then + SRC=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + for f in docker-compose.yml config.hcl gen-tls.sh .env.example; do + [[ -f "$SRC/$f" ]] || die "Missing $f (no embedded payload and not in $SRC -- run build.sh)." + cp "$SRC/$f" "$SCRIPT_DIR/$f" + done +fi + +# ---------------------------------------------------------------------------- +# Prompt for required values +# ---------------------------------------------------------------------------- +prompt() { + local var="$1" msg="$2" cur="${!1}" + [[ -n "$cur" ]] && return + [[ "$SKIP_PROMPTS" == "1" ]] && die "$var required (set it in the environment; running with SKIP_PROMPTS=1)." + read -r -p "$msg: " "$var" +} +prompt OPENBAO_ADDR "LAN address the Kanrisha tape host reaches this vault at (IP or DNS)" +[[ -n "$OPENBAO_ADDR" ]] || die "OPENBAO_ADDR is required." + +# Build the cert SAN list: loopback + whatever OPENBAO_ADDR is (IP vs DNS). +if [[ "$OPENBAO_ADDR" =~ ^[0-9.]+$ || "$OPENBAO_ADDR" == *:* ]]; then + ADDR_SAN="IP:${OPENBAO_ADDR}" +else + ADDR_SAN="DNS:${OPENBAO_ADDR}" +fi +SANS="DNS:localhost,IP:127.0.0.1,${ADDR_SAN}" + +# ---------------------------------------------------------------------------- +# Lay down the stack +# ---------------------------------------------------------------------------- +log "Setting up $STACK_DIR..." +install -d -m 0750 "$STACK_DIR" +install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml" +install -m 0640 "$SCRIPT_DIR/config.hcl" "$STACK_DIR/config.hcl" +install -m 0750 "$SCRIPT_DIR/gen-tls.sh" "$STACK_DIR/gen-tls.sh" + +ENV_FILE="$STACK_DIR/.env" +if [[ ! -f "$ENV_FILE" ]]; then + log "Seeding $ENV_FILE..." + install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE" + sed -i \ + -e "s|^OPENBAO_ADDR=.*|OPENBAO_ADDR=${OPENBAO_ADDR}|" \ + -e "s|^OPENBAO_BIND=.*|OPENBAO_BIND=${OPENBAO_BIND}|" \ + -e "s|^OPENBAO_TLS_SANS=.*|OPENBAO_TLS_SANS=${SANS}|" \ + "$ENV_FILE" +else + log ".env exists; leaving it alone." +fi + +# ---------------------------------------------------------------------------- +# TLS: self-signed unless a cert is already present (drop in a CA-signed pair +# to use your Smallstep CA -- see the README). +# ---------------------------------------------------------------------------- +install -d -m 0750 "$STACK_DIR/tls" +OPENBAO_TLS_SANS="$SANS" bash "$STACK_DIR/gen-tls.sh" "$STACK_DIR/tls" + +disable_swap +open_bao_port + +# ---------------------------------------------------------------------------- +# Bring up the stack +# ---------------------------------------------------------------------------- +if [[ "$FORCE" != "1" ]]; then + printf '\nAbout to start OpenBao from %s (TLS on https://%s:8200). Continue? [y/N] ' "$STACK_DIR" "$OPENBAO_ADDR" + read -r ans + [[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; } +fi + +cd "$STACK_DIR" +log "Pulling image..." +docker compose pull +log "Starting OpenBao..." +docker compose up -d --remove-orphans + +# OpenBao starts SEALED (and, first time, uninitialised), so it reports unhealthy +# until you init + unseal -- that is expected. Give it a moment to bind. +sleep 5 +docker compose ps + +cat < # x3, three different keys + +2. Bootstrap for Kanrisha (from the Kanrisha repo's deploy/openbao/bootstrap.sh): + BAO_ADDR=https://${OPENBAO_ADDR}:8200 BAO_CACERT=${STACK_DIR}/tls/tls.crt \\ + BAO_TOKEN= bash bootstrap.sh + Then point the Kanrisha daemon at: + [encryption.openbao] + address = "https://${OPENBAO_ADDR}:8200" + openbao_ca_cert = "/etc/kanrisha/openbao-ca.crt" # = tls.crt above + +3. Back it up (this vault is the sole recovery path for encrypted tapes): + docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\ + bao operator raft snapshot save -tls-skip-verify /openbao/data/snap.snap + ...copy it off-box, age-encrypted. See the README for the full DR flow. + +Manage: + docker compose logs -f + docker compose pull && docker compose up -d # update + docker compose down # stop, keep the vault data + docker compose down -v # stop, WIPE the vault (DESTROYS keys) + +Re-running this script is idempotent (it won't re-init or touch .env / tls). +================================================================ +EOF + +# IMPORTANT: nothing executable below this line. Everything after +# __ARCHIVE_BELOW__ is the embedded tar.gz payload (base64), added by build.sh. +exit 0 +__ARCHIVE_BELOW__ +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 +pZZaaqmlllpqqaWWWmr5/5N/AAPPvDEAUAAA diff --git a/deployments/openbao/docker-compose.yml b/deployments/openbao/docker-compose.yml new file mode 100644 index 0000000..333ee0b --- /dev/null +++ b/deployments/openbao/docker-compose.yml @@ -0,0 +1,50 @@ +# OpenBao stack — hardened tape-encryption key store for Kanrisha. +# +# Topology (same-LAN, NO reverse proxy): +# Kanrisha tape host --https--> openbao:8200 (native TLS on the vault itself) +# +# Unlike the other stacks in this repo there is no Caddy / Let's Encrypt: a +# secrets store terminates TLS itself and is reached over the LAN, not the +# public internet. TLS is a self-signed cert (deploy.sh generates it) or a +# CA-signed pair you drop in ./tls (e.g. via your Smallstep CA over ACME). + +name: openbao + +volumes: + openbao-data: # raft integrated storage — the vault's persistent state + +services: + openbao: + image: openbao/openbao:${OPENBAO_TAG:-2.5.5} + container_name: openbao + command: server -config=/openbao/config/config.hcl + restart: unless-stopped + # mlock: keep key material off swap. IPC_LOCK + unlimited memlock let the + # process lock its memory; the HOST must also have swap disabled. + cap_add: + - IPC_LOCK + ulimits: + memlock: -1 + ports: + # Native-TLS API. Bind to the LAN IP (OPENBAO_BIND) to avoid exposing it on + # every interface; firewall it to the tape host (deploy.sh does this). + - "${OPENBAO_BIND:-0.0.0.0}:8200:8200" + volumes: + - ./config.hcl:/openbao/config/config.hcl:ro + - ./tls:/openbao/tls:ro + - openbao-data:/openbao/data + environment: + # PKCS#11 auto-unseal PIN — only read when the seal "pkcs11" stanza is + # enabled in config.hcl. Harmless (unused) otherwise. + BAO_HSM_PIN: "${OPENBAO_HSM_PIN:-}" + # HA / multi-node only: advertise this node's reachable LAN address. + # BAO_API_ADDR: "https://${OPENBAO_ADDR}:8200" + # BAO_CLUSTER_ADDR: "https://${OPENBAO_ADDR}:8201" + # 0 = unsealed (healthy); 2 = sealed -> unhealthy so a node needing an unseal + # is visible. -tls-skip-verify because the listener may use a self-signed cert. + healthcheck: + test: ["CMD", "bao", "status", "-address=https://127.0.0.1:8200", "-tls-skip-verify"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/deployments/openbao/gen-tls.sh b/deployments/openbao/gen-tls.sh new file mode 100644 index 0000000..cbfc773 --- /dev/null +++ b/deployments/openbao/gen-tls.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# gen-tls.sh — generate a self-signed TLS cert + key for the OpenBao listener, +# if one is not already present. Never overwrites existing files, so to use a +# CA-signed cert (e.g. issued by your Smallstep CA over ACME) just drop tls.crt + +# tls.key into the target dir instead and this becomes a no-op. +# +# Usage: bash gen-tls.sh [TLS_DIR] (default ./tls) +# Env: OPENBAO_TLS_SANS comma SAN list, e.g. "DNS:vault.lan,IP:10.0.0.10,IP:127.0.0.1" +# OPENBAO_TLS_DAYS validity in days (default 825 — the max browsers accept) + +set -euo pipefail + +DIR="${1:-./tls}" +SANS="${OPENBAO_TLS_SANS:-DNS:localhost,IP:127.0.0.1}" +DAYS="${OPENBAO_TLS_DAYS:-825}" + +mkdir -p "$DIR" + +if [[ -f "$DIR/tls.crt" && -f "$DIR/tls.key" ]]; then + echo "[gen-tls] TLS material already present in $DIR — leaving it alone." + exit 0 +fi + +command -v openssl >/dev/null 2>&1 || { echo "[gen-tls] openssl not found" >&2; exit 1; } + +# ECDSA P-256 self-signed cert. The SAN must include every address the tape host +# (and CLI) use to reach the vault, or their TLS verification fails. +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ + -keyout "$DIR/tls.key" -out "$DIR/tls.crt" -days "$DAYS" \ + -subj "/CN=openbao" -addext "subjectAltName=${SANS}" + +chmod 600 "$DIR/tls.key" +chmod 644 "$DIR/tls.crt" + +echo "[gen-tls] Generated self-signed OpenBao cert in $DIR" +echo "[gen-tls] SANs: ${SANS} validity: ${DAYS}d" +echo "[gen-tls] Give $DIR/tls.crt to the Kanrisha daemon as" +echo "[gen-tls] [encryption.openbao].openbao_ca_cert (so it trusts this listener)."