`BAO_TOKEN=<root> bash update.sh update` -- which the previous commit and the pre-existing DR runbook both recommended -- leaks the token twice over. It goes into the shell history, and `docker compose exec -e BAO_TOKEN=<value>` puts it in the docker process's argv, where /proc/<pid>/cmdline makes it readable by every user on the host. The second is the worse one and is not fixed by anything the operator does at their prompt. The token now reaches the container over STDIN and is never an argument to anything: the container's own shell reads one line, exports it, and execs bao. Verified locally that the child process sees the exact value -- including &, | and a backslash -- while its argv contains zero occurrences of it. How a token is supplied, in order: an already-exported BAO_TOKEN; a 0600 file named by the new BAO_TOKEN_FILE; otherwise an echo-off prompt. With no token and no terminal it dies telling the operator to use BAO_TOKEN_FILE rather than an inline assignment, and says why. Added a `snapshot` subcommand so the DR runbook no longer needs an inline pipeline at all. That runbook and the deploy banner both carried the -e form before this deployment had an updater, so both are corrected: taking a backup is now `bash update.sh snapshot`, and restore keeps the stdin shape rather than -e. The Kanrisha bootstrap lines say to export the token first instead of passing it inline. The conf file still refuses to hold a token, but now points at BAO_TOKEN_FILE as the unattended answer -- and suggests a snapshot-policy token rather than the root token for it. Not verified without a live host: that `docker compose exec -T` forwards stdin as expected. If it does not, the snapshot fails loudly with a permission error and the upgrade aborts before touching anything, which is the safe direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
728 lines
41 KiB
Bash
728 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 <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 \\
|
|
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=
|