947c899removed this idiom from copyparty, headscale, pocket-id and squid; openbao was missed. Its first-run seed still interpolated values into `sed -i -e "s|^KEY=.*|KEY=${VAL}|"`, which silently corrupts any value containing & (sed expands it to the whole match) and aborts the run under set -e on one containing the s||| delimiter. That was survivable while .env held only an address, a bind and an image tag. It stops being survivable the moment an OIDC client secret or an issuer URL with a query string goes in there, which is the next commit -- so this lands first, on its own, with no behaviour change. Adds the same awk/ENVIRON helper the other four use, replaces the seed's sed block with set_env calls, and collapses the inlined copy of that awk (added indc9761afor the UI write-through) into a call to it. Verified against plain, sec&ret, pipe|val, back\slash, an Authentik-shaped discovery URL with an & query string, p@ss&w|rd and the empty string; plus non-target lines left intact and the 0600 mode preserved across an in-place update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
601 lines
32 KiB
Bash
601 lines
32 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 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, .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 .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"
|
|
|
|
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 " --entrypoint sh openbao/openbao:\${OPENBAO_TAG:-2.5.5} -c 'chown -R openbao:openbao /data /tls'"
|
|
BAO_UID=0
|
|
fi
|
|
log "OpenBao server runs as UID ${BAO_UID}; aligning file/volume ownership."
|
|
chown -R "${BAO_UID}:${BAO_UID}" "$STACK_DIR/config.hcl" "$STACK_DIR/tls"
|
|
# tls.key stays private to that UID; config.hcl + tls.crt are non-secret.
|
|
chmod 0600 "$STACK_DIR/tls/tls.key" 2>/dev/null || true
|
|
# Raft data volume. Gate on what the volume ACTUALLY is, not on whether this is
|
|
# the first run: a volume left root-owned by an earlier deploy (or by a botched
|
|
# UID detection) would otherwise never be repaired, since the first-run flag is
|
|
# false forever after. Reading the current owner costs one container start and
|
|
# still keeps the recursive chown off a healthy live raft dir.
|
|
_data_uid=$(docker compose run --rm --no-deps -T --user 0:0 --entrypoint stat openbao \
|
|
-c '%u' /openbao/data 2>/dev/null | tr -dc '0-9')
|
|
if [[ -z "$_data_uid" ]]; then
|
|
warn "Could not read the raft volume's ownership; skipping the data chown."
|
|
elif [[ "$_data_uid" != "$BAO_UID" ]]; then
|
|
log "Raft volume is owned by UID ${_data_uid}; chowning to ${BAO_UID}..."
|
|
docker compose run --rm --no-deps --user 0:0 --entrypoint chown openbao \
|
|
-R "${BAO_UID}:${BAO_UID}" /openbao/data 2>/dev/null || \
|
|
warn "Could not chown the raft data volume; OpenBao will fail to write storage. chown the openbao-data volume to UID ${BAO_UID}."
|
|
fi
|
|
|
|
log "Starting OpenBao..."
|
|
_bao_before="$(docker compose ps -q openbao 2>/dev/null || true)"
|
|
docker compose up -d --remove-orphans
|
|
_bao_after="$(docker compose ps -q openbao 2>/dev/null || true)"
|
|
if (( CONFIG_CHANGED )) && [[ -n "$_bao_before" && "$_bao_before" == "$_bao_after" ]]; then
|
|
warn "config.hcl changed and compose did not recreate the container; restarting to load it."
|
|
warn "NOTE: a restart re-SEALS the vault -- you will have to unseal again."
|
|
docker compose restart openbao || warn "Restart failed; run: cd ${STACK_DIR} && docker compose restart openbao"
|
|
fi
|
|
|
|
# OpenBao starts SEALED (and, first time, uninitialised), so it reports unhealthy
|
|
# until you init + unseal -- that is expected. Give it a moment to bind.
|
|
sleep 5
|
|
docker compose ps
|
|
|
|
# OpenBao starts SEALED and therefore reports UNHEALTHY until it is initialised
|
|
# and unsealed -- that is expected and the summary below explains it. A
|
|
# RESTARTING container is a different thing entirely: it crashed, compose is
|
|
# looping it, and every command in that summary will fail against it. Say so
|
|
# plainly rather than printing an unqualified DEPLOYED.
|
|
BAO_STATE="$(docker inspect -f '{{.State.Status}}' openbao 2>/dev/null || echo unknown)"
|
|
if [[ "$BAO_STATE" != "running" ]]; then
|
|
warn "Container state is '${BAO_STATE}', not 'running' -- OpenBao is crash-looping, not merely sealed."
|
|
warn "Nothing below will work until that is fixed. Start with:"
|
|
warn " cd ${STACK_DIR} && docker compose logs --tail=60 openbao"
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
================================================================
|
|
DEPLOYED (OpenBao starts SEALED -- finish setup below)
|
|
|
|
Address: https://${OPENBAO_ADDR}:8200
|
|
Stack dir: ${STACK_DIR}
|
|
^ run every 'docker compose' command below from there:
|
|
cd ${STACK_DIR}
|
|
From anywhere else compose reports "no configuration file
|
|
provided: not found".
|
|
TLS: ${STACK_DIR}/tls/tls.crt (give this to the Kanrisha daemon as
|
|
[encryption.openbao].ca_cert)
|
|
|
|
>> Initialise NOW: an uninitialised vault reachable on the LAN can be init'd by
|
|
anyone who connects, capturing the root token + unseal keys. Do step 1 before
|
|
walking away.
|
|
|
|
1. Initialise + unseal (ONCE). Store the unseal keys + root token OUT OF BAND:
|
|
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
|
|
bao operator init -tls-skip-verify
|
|
docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \\
|
|
bao operator unseal -tls-skip-verify <key> # x3, three different keys
|
|
|
|
2. Bootstrap for Kanrisha. Run the Kanrisha repo's deploy/openbao/bootstrap.sh
|
|
from a host that HAS the 'bao' CLI (the Kanrisha host or your workstation --
|
|
this vault host only ships Docker), pointed at this vault. Copy tls.crt there
|
|
first as the BAO_CACERT:
|
|
BAO_ADDR=https://${OPENBAO_ADDR}:8200 BAO_CACERT=/path/to/openbao-ca.crt \\
|
|
BAO_TOKEN=<root> bash bootstrap.sh
|
|
Then point the Kanrisha daemon at:
|
|
[encryption.openbao]
|
|
address = "https://${OPENBAO_ADDR}:8200"
|
|
ca_cert = "/etc/kanrisha/openbao-ca.crt" # = tls.crt above
|
|
|
|
3. Back it up (this vault is the sole recovery path for encrypted tapes). Snapshot
|
|
save is token-gated -- pass a token with sys/storage/raft/snapshot (the root
|
|
token works):
|
|
docker compose exec -T -e BAO_TOKEN=<token> openbao \\
|
|
bao operator raft snapshot save -address=https://127.0.0.1:8200 -tls-skip-verify /tmp/openbao.snap
|
|
docker compose exec -T openbao cat /tmp/openbao.snap > ${STACK_DIR}/openbao.snap
|
|
...then age-encrypt + copy it off-box. See the README for the full DR flow.
|
|
|
|
Manage:
|
|
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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
|