deploy.sh's closing banner and README.md both instructed the operator to run
`bao operator unseal -tls-skip-verify <key>`, three times. That places an
unseal key in the docker process's argv, and /proc/<pid>/cmdline is
world-readable -- so every user on the host can read it. Three of those shares
reconstruct the master key, which makes this a worse instance of the leak class
0eb7f26 fixed for the token.
`bao operator unseal` with NO key argument prompts for it with echo suppressed.
Both places now say to pass nothing, and say why.
These lines predate the updater and this session's work; anyone who followed the
printed instructions has been exposing unseal shares to `ps` on that host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
731 lines
41 KiB
Bash
731 lines
41 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 # x3, three different keys
|
|
Pass NO key argument: it then prompts with echo off. A key on the command
|
|
line lands in the docker process's argv, and /proc/<pid>/cmdline is
|
|
world-readable -- three of those reconstruct the master key.
|
|
|
|
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=<value>`: that puts the token in the docker
|
|
process's argv, and /proc/<pid>/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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
|
|
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
|
|
ICQb6ipQE8pueC7vZFNwCCqFxOFmbbHm0++aosWMwIEnIMNTEQy+PRr8+Ep8y/n5Va8IwnhY3AfQ
|
|
FzWpOPG4BJ7SOYeJJN/zBbO5htbVVR3odXh2Mno/GJ1vmQKdb/gncfmnUHxPg5rgOvtalqFIFeE3
|
|
uy/cr7/5RrRF9+u92W5nl6P+7WKpAw8fCVFjApbRbVJlgV+2myILE/2ngxwNP9OHOoWJrQXBsgSb
|
|
kafuF3r7n/aOtKttK/tdv0J14NhKsAzpZBaI08ME2nKaJj2QNjMTUhC2IJrgpZYIocH/fe76NsuY
|
|
TOj0zIx12iS2n56e7rvv7gs5EBorMCVs4BP3dnPXJEcjSbxOX8DBxb/gycgJBu+qfDBOeOzRwe4R
|
|
QLErk+LHn7YRmWny7PJd3DRz/nX3m73nsB0YmSewe92QCRqw7Pfh9zATfg/AGsJP6/HUTPUFfQcI
|
|
UICE8KG1sr4Gsza6jSTuduMNxV/+Ln5vpgAcHaLrw06kI+03u8934FuAbosekvhTycCmB3MEIQCn
|
|
IQl7lXyMZ0E7GfB+0WjAUkC8o7O88rEUFFYOaSQ5nw5Dq9m0WJlESqHDrWdHmu54c8ts45asG3ER
|
|
6XrZ+Xllo9vpjBveDNegtRegMQ83OIi1fXcX5UWjfIXi5DnJ/BKzp2IXnh88gNmkJGnUk0XT+FjO
|
|
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
|
|
bgCgAAA=
|