Files
automations/deployments/openbao/deploy.sh
T
57_WolveandClaude Opus 5 2d3d322b0e feat(openbao): add update.sh, an updater that refuses to strand a sealed vault
Follows the copyparty/ergo updater idiom -- check/update/run/install/uninstall,
a conf file the environment overrides, the version pinned into .env so the
running release is explicit, DRY_RUN -- but inverts its central assumption.

copyparty and ergo come back by themselves after a recreate. OpenBao comes back
SEALED: with the default Shamir seal a restart needs three unseal keys typed in
by a human. A scheduled `latest` update would therefore take the vault offline
at 03:00 and leave it there. So the scheduled path defaults to UPDATE_POLICY=
notify and never changes the running version; `install` schedules a daily CHECK
and says so. UPDATE_POLICY=auto opts in, and is STILL refused unless an
uncommented `seal` stanza is present in config.hcl -- only auto-unseal makes an
unattended update defensible.

`update` preflights before touching anything, because every one of these fails
worse halfway through than up front:
- the container must be running;
- the vault must be UNSEALED, since a sealed vault cannot produce a snapshot and
  there would be no rollback plan;
- BAO_TOKEN must be present, because the snapshot is token-gated on
  sys/storage/raft/snapshot;
- the target must not cross into 2.7.x while a built-in seal "pkcs11" stanza is
  active. That stanza is REMOVED in 2.7.0, not deprecated, so the vault would
  start with no way to unseal at all.

The snapshot is the rollback plan, not a formality: OpenBao's upgrade guide
states that reverting the image alone does not roll back the data store. It is
streamed out with `exec -T ... cat` rather than `compose cp`, which emits a TAR
wrapper that will not restore; written 0600 to /var/backups/openbao; and checked
for being a non-empty valid gzip archive, with a failure treated as fatal.
SKIP_SNAPSHOT=1 exists and warns exactly what it costs.

A failed pull or start rolls the OPENBAO_TAG pin back and restarts the previous
version. BAO_TOKEN is deliberately never written to the conf file: a long-lived
root token sitting next to the vault it opens defeats the vault.

Verified: the 2.7-with-active-pkcs11 refusal and its three negative cases
(2.6.2 active, 3.x active, 2.7 commented); auto-unseal detection distinguishing
a commented stanza from a live one; set_env/env_get; the release-tag parse with
and without a leading v; and all four `run` policy branches, including that
auto refuses without auto-unseal and that an unknown policy dies.

Not verified without a live host: the snapshot, pull and recreate themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:21:11 -05:00

713 lines
40 KiB
Bash

#!/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, update.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, update.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}"
: "${SKIP_BIND_CHECK:=0}" # 1 = publish on an address this host does not (yet) have
: "${FORCE:=0}"
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
: "${OPENBAO_ADDR:=}"
# Whether OPENBAO_BIND arrived in this script's ENVIRONMENT (automations.sh
# passes answers via `env VAR=... bash deploy.sh`, and a standalone run may
# export it too). If it did it is still exported when we reach compose, which
# prefers the shell environment over $STACK_DIR/.env; if this script derives it
# below instead, the assignment is NOT exported and the .env wins. Must be read
# before the := default, which would make an unset var look set.
BIND_FROM_ENV=0
[[ -n "${OPENBAO_BIND+x}" ]] && BIND_FROM_ENV=1
: "${OPENBAO_BIND:=0.0.0.0}"
# Same question for the UI switch: an explicit OPENBAO_UI=0 has to be told
# apart from "not mentioned", or a re-run would silently re-enable the UI.
UI_FROM_ENV=0
[[ -n "${OPENBAO_UI+x}" ]] && UI_FROM_ENV=1
: "${OPENBAO_UI:=}"
: "${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
# dockerd is often started in the background (esp. openrc) and returns before
# the socket is listening -- poll so the first `docker compose` call doesn't
# race it and abort under set -e.
local i
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && return
sleep 1
done
warn "Docker daemon not ready after 30s; continuing (compose may fail -- check 'docker info')."
}
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
}
# Addresses currently assigned to this host, one per line. Parses plain
# `ip addr show` output -- no -o/scope filters, since busybox ip (what Alpine
# ships by default) supports neither -- with an ifconfig fallback.
host_addrs() {
if command -v ip >/dev/null 2>&1; then
ip addr show 2>/dev/null | awk '$1=="inet"||$1=="inet6"{split($2,a,"/"); print a[1]}'
elif command -v ifconfig >/dev/null 2>&1; then
ifconfig 2>/dev/null | awk '$1=="inet"||$1=="inet6"{v=$2; if(v=="addr:") v=$3; sub(/^addr:/,"",v); split(v,a,"/"); if(a[1]!="") print a[1]}'
fi
}
# A published port can only bind an address this host actually owns. Docker does
# not find that out until `up`, where it fails with a bare "cannot assign
# requested address" -- by which point this script has seeded .env and burned the
# address into the cert SAN, neither of which a re-run rewrites. So check first.
check_bind_addr() {
local bind="$1" bare addrs
bare="${bind#[}"; bare="${bare%]}" # unwrap an [IPv6] publish literal
case "$bare" in ''|0.0.0.0|'::'|'*') return 0 ;; esac
if [[ "$SKIP_BIND_CHECK" == "1" ]]; then
warn "SKIP_BIND_CHECK=1 -- not checking whether ${bare} is local."
return 0
fi
# `|| true` is load-bearing: host_addrs ends in a pipeline, and under
# `set -o pipefail` a probe that fails AFTER printing usable addresses (or an
# absent awk) would make this plain assignment non-zero and kill the whole
# deploy at this line, silently -- before the fail-open below is ever reached.
addrs="$(host_addrs || true)"
# Empty means the probe found no tool to ask, not that the address is absent
# -- do not block a deploy on that.
if [[ -z "$addrs" ]]; then
warn "Could not list this host's addresses (no ip/ifconfig, or it failed); skipping the bind check."
return 0
fi
if printf '%s\n' "$addrs" | grep -qxF "$bare"; then
return 0
fi
warn "Addresses on this host: $(printf '%s\n' "$addrs" | tr '\n' ' ')"
die "Nothing here is assigned ${bare}, so Docker cannot publish 8200 on it. Fix OPENBAO_ADDR / OPENBAO_BIND (compose reads an exported OPENBAO_BIND first, then ${STACK_DIR}/.env), or set SKIP_BIND_CHECK=1 if the address only comes up later."
}
disable_swap() {
[[ "$DISABLE_SWAP" == "1" ]] || { warn "DISABLE_SWAP=0 -- mlock will be best-effort."; return; }
# Detect active swap via /proc/swaps (a header line + one line per device) so
# this works on musl/BusyBox too, where `swapon --show` does not exist.
if [[ -r /proc/swaps ]] && [[ "$(wc -l < /proc/swaps)" -gt 1 ]]; then
log "Disabling swap (mlock keeps key material off disk)..."
swapoff -a || warn "swapoff failed -- disable swap manually."
else
log "No active swap."
fi
# Persist: comment any swap lines in fstab so it stays off across reboots.
# [[:space:]] (not \s) so the match works under musl/BusyBox grep/sed.
if [[ -f /etc/fstab ]] && grep -qE '^[^#].*[[:space:]]swap[[:space:]]' /etc/fstab; then
sed -i.bak -E 's|^([^#].*[[:space:]]swap[[:space:]].*)$|# \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 update.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) +
# any extra SANs the operator exported in OPENBAO_TLS_SANS.
ADDR_KIND=dns
if [[ "$OPENBAO_ADDR" =~ ^[0-9.]+$ ]]; then
ADDR_SAN="IP:${OPENBAO_ADDR}"; ADDR_KIND=ipv4
elif [[ "$OPENBAO_ADDR" == *:* ]]; then
ADDR_SAN="IP:${OPENBAO_ADDR}"; ADDR_KIND=ipv6
else
ADDR_SAN="DNS:${OPENBAO_ADDR}"
fi
SANS="DNS:localhost,IP:127.0.0.1,${ADDR_SAN}"
[[ -n "${OPENBAO_TLS_SANS:-}" ]] && SANS="${SANS},${OPENBAO_TLS_SANS}"
# A Docker-published port bypasses the host INPUT firewall, so the interface bind
# is the real restriction. If the operator left OPENBAO_BIND at the all-interfaces
# default and OPENBAO_ADDR is an IP, narrow the publish to just that LAN IP. IPv6
# literals must be bracketed in the compose port mapping ([addr]:8200:8200).
if [[ "$OPENBAO_BIND" == "0.0.0.0" ]]; then
case "$ADDR_KIND" in
ipv4) OPENBAO_BIND="$OPENBAO_ADDR"; log "Binding the API to ${OPENBAO_BIND} only (set OPENBAO_BIND to override)." ;;
ipv6) OPENBAO_BIND="[${OPENBAO_ADDR}]"; log "Binding the API to ${OPENBAO_BIND} only (set OPENBAO_BIND to override)." ;;
*) warn "OPENBAO_BIND=0.0.0.0 and OPENBAO_ADDR is a DNS name -- API publishes on ALL interfaces. Set OPENBAO_BIND to a LAN IP to narrow it." ;;
esac
fi
# Which OPENBAO_BIND `docker compose` interpolates decides where the port lands,
# and compose reads the shell environment BEFORE $STACK_DIR/.env. So an exported
# value wins; one derived above does not, and the .env -- which deploy.sh never
# rewrites -- wins instead. Resolve which, say so when the two disagree, and
# confirm the address is really on this box before anything is written to disk.
EFFECTIVE_BIND="$OPENBAO_BIND"
if [[ -f "$STACK_DIR/.env" ]]; then
ENV_BIND=$(sed -n 's/^OPENBAO_BIND=//p' "$STACK_DIR/.env" | tail -n1)
ENV_ADDR=$(sed -n 's/^OPENBAO_ADDR=//p' "$STACK_DIR/.env" | tail -n1)
if [[ "$BIND_FROM_ENV" == "1" ]]; then
# Exported, so compose prefers it -- but a stale .env line still bites a
# later hand-run `docker compose up` that has no such environment.
if [[ -n "$ENV_BIND" && "$ENV_BIND" != "$OPENBAO_BIND" ]]; then
warn "OPENBAO_BIND=${OPENBAO_BIND} came from the environment, so compose prefers it: THIS run binds ${OPENBAO_BIND}."
warn "But ${STACK_DIR}/.env still says ${ENV_BIND} -- update that line, or a later plain 'docker compose up -d' will bind ${ENV_BIND}."
fi
elif [[ -n "$ENV_BIND" ]]; then
EFFECTIVE_BIND="$ENV_BIND"
if [[ "$ENV_BIND" != "$OPENBAO_BIND" ]]; then
warn "${STACK_DIR}/.env pins OPENBAO_BIND=${ENV_BIND}. Nothing was exported this run, so compose uses that, not the ${OPENBAO_BIND} derived here -- edit the .env to change the bind."
fi
else
# .env exists but has no OPENBAO_BIND line (hand-edited?): compose falls
# back to the compose-file default, which publishes on everything.
EFFECTIVE_BIND=0.0.0.0
warn "${STACK_DIR}/.env has no OPENBAO_BIND line and none was exported -- compose falls back to 0.0.0.0, publishing the API on ALL interfaces."
warn "Add 'OPENBAO_BIND=${OPENBAO_BIND}' to ${STACK_DIR}/.env to narrow it."
fi
if [[ -n "$ENV_ADDR" && "$ENV_ADDR" != "$OPENBAO_ADDR" ]]; then
warn "${STACK_DIR}/.env still says OPENBAO_ADDR=${ENV_ADDR}, and an existing cert in ${STACK_DIR}/tls is never regenerated over."
warn "To actually move the vault to ${OPENBAO_ADDR}: edit that .env, then 'rm -f ${STACK_DIR}/tls/tls.crt ${STACK_DIR}/tls/tls.key', then re-run."
fi
fi
check_bind_addr "$EFFECTIVE_BIND"
# config.hcl is rendered from OPENBAO_UI, so resolve which value actually
# applies: one passed to this run wins, else whatever .env already deploys,
# else the default. Without this, `OPENBAO_UI=0 bash deploy.sh` against an
# existing node would look like it worked and change nothing.
EFFECTIVE_UI="$OPENBAO_UI"
if [[ "$UI_FROM_ENV" != "1" && -f "$STACK_DIR/.env" ]]; then
_env_ui=$(sed -n 's/^OPENBAO_UI=//p' "$STACK_DIR/.env" | tail -n1)
[[ -n "$_env_ui" ]] && EFFECTIVE_UI="$_env_ui"
fi
[[ -n "$EFFECTIVE_UI" ]] || EFFECTIVE_UI=1
case "$EFFECTIVE_UI" in
1|true|yes|on) UI_HCL=true ;;
0|false|no|off) UI_HCL=false ;;
*) die "OPENBAO_UI must be 1 or 0 (got '${EFFECTIVE_UI}')." ;;
esac
# ----------------------------------------------------------------------------
# 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"
# config.hcl holds no secrets and is read by the in-container server process
# (which may be a non-root user) over a read-only mount -- keep it world-readable.
# Rendered rather than copied, so @UI@ reflects OPENBAO_UI. A bind-mounted
# file's CONTENTS are not part of the compose config hash, so `up -d` alone
# would leave a changed config.hcl unloaded -- track whether it actually
# changed and restart below only then, because restarting re-SEALS the vault.
CONFIG_CHANGED=0
_UI_HCL="$UI_HCL" awk '{ gsub(/@UI@/, ENVIRON["_UI_HCL"]); print }' \
"$SCRIPT_DIR/config.hcl" > "$SCRIPT_DIR/config.hcl.rendered"
# `cmp -s` exits 1 when they differ, so it stays inside an `if` condition:
# `cmp -s A B && CONFIG_CHANGED=1` would trip set -e whenever they matched.
if [[ -f "$STACK_DIR/config.hcl" ]] && ! cmp -s "$STACK_DIR/config.hcl" "$SCRIPT_DIR/config.hcl.rendered"; then
CONFIG_CHANGED=1
fi
install -m 0644 "$SCRIPT_DIR/config.hcl.rendered" "$STACK_DIR/config.hcl"
log "Web UI: ${UI_HCL} (OPENBAO_UI=${EFFECTIVE_UI})"
install -m 0750 "$SCRIPT_DIR/gen-tls.sh" "$STACK_DIR/gen-tls.sh"
# The updater. Deliberately NOT run by deploy.sh: upgrading a live vault seals
# it, so it is an operator-invoked step rather than part of a deploy.
install -m 0750 "$SCRIPT_DIR/update.sh" "$STACK_DIR/update.sh"
ENV_FILE="$STACK_DIR/.env"
set_env() { # <KEY> <value>: update KEY in .env, or append if absent
# The value goes through the ENVIRONMENT, never interpolated into a sed
# script. Interpolating it corrupts any value containing & (sed expands it
# to the whole match) and aborts the run on one containing the s|||
# delimiter -- both reachable for an OIDC client secret or an issuer URL
# with a query string. Same helper as 947c899 gave the other stacks.
local key="$1" val="$2" tmp
if [[ ! -f "$ENV_FILE" ]]; then
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
return 0
fi
tmp="$(mktemp)"
_SE_KEY="$key" _SE_VAL="$val" awk '
BEGIN { k = ENVIRON["_SE_KEY"]; v = ENVIRON["_SE_VAL"]; seen = 0 }
!seen && index($0, k "=") == 1 { print k "=" v; seen = 1; next }
{ print }
END { if (!seen) print k "=" v }
' "$ENV_FILE" > "$tmp"
cat "$tmp" > "$ENV_FILE" # rewrite in place: keeps the 0600 mode/owner
rm -f "$tmp"
}
if [[ ! -f "$ENV_FILE" ]]; then
log "Seeding $ENV_FILE..."
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
set_env OPENBAO_ADDR "$OPENBAO_ADDR"
set_env OPENBAO_BIND "$OPENBAO_BIND"
set_env OPENBAO_TLS_SANS "$SANS"
set_env OPENBAO_UI "$EFFECTIVE_UI"
else
log ".env exists; leaving it alone."
# ...except a UI switch passed to THIS run: config.hcl is rendered from
# it above, so letting .env keep the old value means the next run
# silently reverts the UI. Only touch the key when it was actually passed.
if [[ "$UI_FROM_ENV" == "1" ]]; then
_cur_ui=$(sed -n 's/^OPENBAO_UI=//p' "$ENV_FILE" | tail -n1)
if [[ "$_cur_ui" != "$EFFECTIVE_UI" ]]; then
set_env OPENBAO_UI "$EFFECTIVE_UI"
log " OPENBAO_UI: ${_cur_ui:-<unset>} -> ${EFFECTIVE_UI}"
fi
fi
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
install_docker
cd "$STACK_DIR"
log "Pulling image..."
docker compose pull
# The OpenBao server process runs as the image's own user -- root in some image
# variants, a non-root service user in others -- and reads its config + TLS over
# read-only bind mounts and writes the raft data volume. So those must be owned by
# that UID. Detect it from the pulled image (-T: no TTY on the piped stdout) and
# align ownership; a no-op when the image runs as root.
# Ask for the account the ENTRYPOINT switches to, not the one a probe starts as.
# The image's entrypoint runs as root and then does `su-exec openbao "$@"` before
# exec'ing the server, so `--entrypoint id -u` bypasses that drop and reports 0.
# Chowning to 0 on the strength of that leaves the server running as the
# unprivileged account with a root-owned raft volume, and it crash-loops on
# "failed to open bolt file: /openbao/data/vault.db: permission denied".
BAO_UID=$(docker compose run --rm --no-deps -T --entrypoint sh openbao \
-c 'id -u openbao 2>/dev/null' 2>/dev/null | tr -dc '0-9')
if [[ -z "$BAO_UID" ]]; then
# No such account: this image runs the server as whatever the entrypoint
# started as, so the older probe is the right answer here.
BAO_UID=$(docker compose run --rm --no-deps -T --entrypoint id openbao -u 2>/dev/null | tr -dc '0-9')
fi
if [[ -z "$BAO_UID" ]]; then
# Both probes failed. Falling back to root is the DANGEROUS direction -- it is
# exactly what produced the crash loop -- so say how to recover.
warn "Could not detect the OpenBao service account; assuming root."
warn "If OpenBao crash-loops with a permission error on /openbao/data, run:"
warn " cd ${STACK_DIR} && docker compose down"
warn " docker run --rm -v openbao_openbao-data:/data -v ${STACK_DIR}/tls:/tls \\"
warn " --user 0:0 --entrypoint sh openbao/openbao:\${OPENBAO_TAG:-2.6.2} -c 'chown -R openbao:openbao /data /tls'"
BAO_UID=0
fi
log "OpenBao server runs as UID ${BAO_UID}; aligning file/volume ownership."
chown -R "${BAO_UID}:${BAO_UID}" "$STACK_DIR/config.hcl" "$STACK_DIR/tls"
# tls.key stays private to that UID; config.hcl + tls.crt are non-secret.
chmod 0600 "$STACK_DIR/tls/tls.key" 2>/dev/null || true
# Raft data volume. Gate on what the volume ACTUALLY is, not on whether this is
# the first run: a volume left root-owned by an earlier deploy (or by a botched
# UID detection) would otherwise never be repaired, since the first-run flag is
# false forever after. Reading the current owner costs one container start and
# still keeps the recursive chown off a healthy live raft dir.
_data_uid=$(docker compose run --rm --no-deps -T --user 0:0 --entrypoint stat openbao \
-c '%u' /openbao/data 2>/dev/null | tr -dc '0-9')
if [[ -z "$_data_uid" ]]; then
warn "Could not read the raft volume's ownership; skipping the data chown."
elif [[ "$_data_uid" != "$BAO_UID" ]]; then
log "Raft volume is owned by UID ${_data_uid}; chowning to ${BAO_UID}..."
docker compose run --rm --no-deps --user 0:0 --entrypoint chown openbao \
-R "${BAO_UID}:${BAO_UID}" /openbao/data 2>/dev/null || \
warn "Could not chown the raft data volume; OpenBao will fail to write storage. chown the openbao-data volume to UID ${BAO_UID}."
fi
log "Starting OpenBao..."
_bao_before="$(docker compose ps -q openbao 2>/dev/null || true)"
docker compose up -d --remove-orphans
_bao_after="$(docker compose ps -q openbao 2>/dev/null || true)"
if (( CONFIG_CHANGED )) && [[ -n "$_bao_before" && "$_bao_before" == "$_bao_after" ]]; then
warn "config.hcl changed and compose did not recreate the container; restarting to load it."
warn "NOTE: a restart re-SEALS the vault -- you will have to unseal again."
docker compose restart openbao || warn "Restart failed; run: cd ${STACK_DIR} && docker compose restart openbao"
fi
# 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
# OpenBao starts SEALED and therefore reports UNHEALTHY until it is initialised
# and unsealed -- that is expected and the summary below explains it. A
# RESTARTING container is a different thing entirely: it crashed, compose is
# looping it, and every command in that summary will fail against it. Say so
# plainly rather than printing an unqualified DEPLOYED.
BAO_STATE="$(docker inspect -f '{{.State.Status}}' openbao 2>/dev/null || echo unknown)"
if [[ "$BAO_STATE" != "running" ]]; then
warn "Container state is '${BAO_STATE}', not 'running' -- OpenBao is crash-looping, not merely sealed."
warn "Nothing below will work until that is fixed. Start with:"
warn " cd ${STACK_DIR} && docker compose logs --tail=60 openbao"
fi
cat <<EOF
================================================================
DEPLOYED (OpenBao starts SEALED -- finish setup below)
Address: https://${OPENBAO_ADDR}:8200
Stack dir: ${STACK_DIR}
^ run every 'docker compose' command below from there:
cd ${STACK_DIR}
From anywhere else compose reports "no configuration file
provided: not found".
TLS: ${STACK_DIR}/tls/tls.crt (give this to the Kanrisha daemon as
[encryption.openbao].ca_cert)
>> Initialise NOW: an uninitialised vault reachable on the LAN can be init'd by
anyone who connects, capturing the root token + unseal keys. Do step 1 before
walking away.
1. Initialise + unseal (ONCE). Store the unseal keys + root token OUT OF BAND:
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
bao operator init -tls-skip-verify
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
bao operator unseal -tls-skip-verify <key> # x3, three different keys
2. Bootstrap for Kanrisha. Run the Kanrisha repo's deploy/openbao/bootstrap.sh
from a host that HAS the 'bao' CLI (the Kanrisha host or your workstation --
this vault host only ships Docker), pointed at this vault. Copy tls.crt there
first as the BAO_CACERT:
BAO_ADDR=https://${OPENBAO_ADDR}:8200 BAO_CACERT=/path/to/openbao-ca.crt \\
BAO_TOKEN=<root> bash bootstrap.sh
Then point the Kanrisha daemon at:
[encryption.openbao]
address = "https://${OPENBAO_ADDR}:8200"
ca_cert = "/etc/kanrisha/openbao-ca.crt" # = tls.crt above
3. Back it up (this vault is the sole recovery path for encrypted tapes). Snapshot
save is token-gated -- pass a token with sys/storage/raft/snapshot (the root
token works):
docker compose exec -T -e BAO_TOKEN=<token> openbao \\
bao operator raft snapshot save -address=https://127.0.0.1:8200 -tls-skip-verify /tmp/openbao.snap
docker compose exec -T openbao cat /tmp/openbao.snap > ${STACK_DIR}/openbao.snap
...then age-encrypt + copy it off-box. See the README for the full DR flow.
Manage (run these from ${STACK_DIR}):
bash update.sh check # declared/running/latest + seal state
BAO_TOKEN=<token> bash update.sh update # snapshot, then upgrade (comes back SEALED)
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
if [[ "$BAO_STATE" != "running" ]]; then
warn "Reminder: the container is '${BAO_STATE}'. Fix that before step 1 above."
fi
# 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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
AA==