#!/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 \\ bash bootstrap.sh # export BAO_TOKEN first; do not pass it inline 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 its secrets). Snapshot save is token-gated; update.sh prompts for the token with echo off and hands it to the container over stdin, so it never reaches a command line: cd ${STACK_DIR} && bash update.sh snapshot ...then age-encrypt + copy it off-box. See the README for the full DR flow. Do NOT use `-e BAO_TOKEN=`: that puts the token in the docker process's argv, and /proc//cmdline is world-readable. Manage (run these from ${STACK_DIR}): bash update.sh check # declared/running/latest + seal state 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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 DUb74sHHHA5zt+RKc3DVviqs6soWp7A2yIyCTxabrvIvf0zzmlMPZEtH9c1xniTBsrhowRGmqY8e 7FUWJ2+Gx4NVPllc5bQi+PDZ9CZKyE6r6MtoCdWcar9946CmQezLE1UIi0ed98/eDs4nyGvj4cma XMh9ENwS6RK4LXNc7UhIxxUmdHj24+TsYrTfeUQQgLSkPfI1HUQWTpJQz99YsjEPzfbR8HRS8L5G 7bA/ejs4O7kY98occmvOMVZiOxN6+25CRVGBeKtdczR9oAVtVleViXxLV1Fe6eZVVfwZy8A+0mu1 xf5yeMJGwYGpZ7JukEumF1DAphDHDTg+OBOwCmSq5dphobkf5KTzG33YhpQLSSVpzLeUjQOSCfCJ IqxUdzByOqUzk6fxHR8ntWlW+9vE91613dAjAzTXg0wi4jjCBJM8UzWbImAcLYu/IRCF6SLiLvRO Sjxd3XyPC1oq8yjgWvqmM7krdgtWwkTfIOBHUBfDbroEgSDDd2D47wP4sgECKPWOmNwbPmSqspBC 0kpisqRVJ6FC0BHkAOwL9d4j01nTYD1xrn02yJK0IKh0kd5j8+vZ7cF8vVtVNn6Z2GNU6intN1BZ Cw98XaptU9Dki1OPLvKYn/Pfknxi+GOdzPHg+M1+8+mm6wn86/kp/d0JXVl+3R+/m4xPLs4OBped D/fNFrcYk1uv1X66SZfTPj2w0Vi59Gyi9sHJ6E1v/1P54L5ZdDZTzH1k6kozk2Q5WMsf1d8mEb2e Lpppxxye1rrxeK6vCKOWg9MN34z3dRTG8hN2jp9/Zm4icMMP9PpecUFd0Nvy1f5/i/+6vOypxHFl 78OHLzdouvU3kgKPaLdrb9v3RIukWZwWlzQn1/vFlM8/3//yvvlSTG7KRxtf7vMTPeh6nWidM833 NS8EVh4sVAz5+Nnk+qu7+2Icx/MZtXL17Gefq2f0640mzneavv2EfrgZzIovU3hvJYeDQ369ksV7 Jo0XL0s/6O0/eMSuUT2v58Heo4mwGLnqIxUFk9p6+52SZj1J1Z6bnNNbTzrUTh2MvtdO23xa7rxN Z9DNBglm+Hby7uB45WV1FRnTg3i+2RLiYyH8Zz91dnYuuy93tsPLrz7wl04oPlc/RaSKL5vUGb91 0giTHs7ZCS8/e2SO7qzfNzxfPr5WN7y8++S8lY68viRVXvpRbkzo3+QOfWcBUYTRWv1vQrbMRUjK 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 bgCgAAA=