#!/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() { # : 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:-} -> ${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 <> 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 # 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= 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= 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= 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==