Files
infra-scripts/openbao/install.sh
Damien a6a3b421c5 Add cleanup for temporary directory
Previously the temporary directory was removed via the RETURN trap
but this could be bypassed if the script exited early. Now we
explicitly clean up the directory in all exit paths.
2026-05-26 17:42:24 +02:00

599 lines
20 KiB
Bash
Executable File

#!/bin/bash
# install.sh - OpenBao: LXC creation, installation & update
# Usage:
# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh)"
# From inside LXC : bash /root/install.sh (updates bao binary)
#
# Single entrypoint, three automatic modes:
# 1. Proxmox host, no existing container -> create LXC + install OpenBao
# 2. Proxmox host, container already present -> update packages + upgrade bao
# 3. Inside an LXC -> install bao if missing, otherwise update
#
# The OpenBao binary install/upgrade logic lives in a single reusable function
# (install_or_upgrade_bao) shared by both the create and update paths.
set -euo pipefail
# --- Config (override via environment) ---
CTID="${CTID:-}"
HOSTNAME_LXC="${OPENBAO_HOSTNAME:-openbao}"
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
STORAGE="${STORAGE:-local-lvm}"
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
CORES="${CORES:-2}"
RAM="${RAM:-1024}"
DISK="${DISK:-8}"
BRIDGE="${BRIDGE:-vmbr0}"
LXC_TAG="${LXC_TAG:-openbao}" # stable identifier for the container
OPENBAO_VERSION="${OPENBAO_VERSION:-latest}" # "latest" or e.g. "v2.0.3"
# GitHub releases endpoint for the openbao/openbao repo (used to resolve "latest").
OPENBAO_RELEASES_URL="${OPENBAO_RELEASES_URL:-https://api.github.com/repos/openbao/openbao/releases}"
OPENBAO_LISTEN_ADDR="${OPENBAO_LISTEN_ADDR:-127.0.0.1:8200}"
# Public API address advertised to clients (also used for OIDC / UI redirects).
# Defaults to the local listener; override with the tailnet URL once known,
# e.g. OPENBAO_API_ADDR="https://openbao.<tailnet>.ts.net".
OPENBAO_API_ADDR="${OPENBAO_API_ADDR:-http://${OPENBAO_LISTEN_ADDR}}"
# Optional: pre-authorise the LXC's Tailscale non-interactively.
# Generate at https://login.tailscale.com/admin/settings/keys
TS_AUTHKEY="${TS_AUTHKEY:-}"
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
# testing from a non-main branch, e.g.
# SCRIPT_URL="https://gitea.arnodo.fr/.../branch/feat/lxc-OpenBao/openbao/install.sh"
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh}"
VERSION_FILE="${VERSION_FILE:-/opt/openbao_version.txt}"
BAO_USER="openbao"
BAO_CONFIG_DIR="/etc/openbao"
BAO_DATA_DIR="/var/lib/openbao"
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
# ============================================================
# Generic helpers
# ============================================================
require_root() {
if [[ "$(id -u)" -ne 0 ]]; then
log_error "This script must be run as root (current uid: $(id -u))."
log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root."
exit 1
fi
}
# OpenBao publishes release assets named with the raw `uname -m` arch
# (e.g. x86_64, aarch64), not Go-style amd64/arm64.
get_arch() {
case "$(uname -m)" in
x86_64) echo "x86_64" ;;
aarch64) echo "aarch64" ;;
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
}
# Resolve "latest" -> concrete tag name, otherwise echo input unchanged.
resolve_openbao_version() {
local requested="$1"
if [[ "$requested" != "latest" ]]; then
echo "$requested"
return 0
fi
local tag
tag=$(curl -fsSL "${OPENBAO_RELEASES_URL}/latest" | jq -r '.tag_name')
if [[ -z "$tag" || "$tag" == "null" ]]; then
log_error "Failed to resolve latest OpenBao release from GitHub API."
exit 1
fi
echo "$tag"
}
# ============================================================
# Reusable: install or upgrade the bao binary in-place.
# Used by both fresh-install and update flows.
# Returns 0 on success, exits on hard error.
# ============================================================
install_or_upgrade_bao() {
local tag arch version url tmpdir current
tag=$(resolve_openbao_version "$OPENBAO_VERSION")
arch=$(get_arch)
version="${tag#v}"
current=""
if [[ -f "$VERSION_FILE" ]]; then
current=$(cat "$VERSION_FILE")
fi
if [[ "$current" == "$tag" && -x /usr/local/bin/bao ]]; then
log_info "OpenBao already at $tag, nothing to do."
return 0
fi
# Asset naming convention: bao_<version>_Linux_<arch>.tar.gz
url="https://github.com/openbao/openbao/releases/download/${tag}/bao_${version}_Linux_${arch}.tar.gz"
log_info "Downloading OpenBao ${tag} (${arch}) from ${url}..."
tmpdir=$(mktemp -d)
curl -fsSL "$url" -o "${tmpdir}/bao.tar.gz"
tar -xzf "${tmpdir}/bao.tar.gz" -C "$tmpdir"
if [[ ! -f "${tmpdir}/bao" ]]; then
log_error "Archive did not contain expected 'bao' binary."
rm -rf "$tmpdir"
exit 1
fi
# Stop service if running, swap binary atomically, then restart.
local service_was_running=0
if command -v rc-service >/dev/null 2>&1 && rc-service openbao status >/dev/null 2>&1; then
service_was_running=1
log_info "Stopping openbao service for upgrade..."
rc-service openbao stop || true
fi
if [[ -x /usr/local/bin/bao ]]; then
cp /usr/local/bin/bao "/usr/local/bin/bao.bak.$(date +%s)"
fi
install -m 0755 "${tmpdir}/bao" /usr/local/bin/bao
ln -sf /usr/local/bin/bao /usr/bin/bao
echo "$tag" > "$VERSION_FILE"
log_info "Installed: $(/usr/local/bin/bao --version 2>&1 | head -n1 || true)"
if [[ "$service_was_running" -eq 1 ]]; then
log_info "Restarting openbao service..."
rc-service openbao start
fi
rm -rf "$tmpdir"
}
# ============================================================
# Reusable: bring Tailscale up and publish OpenBao on the tailnet.
# Idempotent: re-running is a no-op once Tailscale is logged in and the
# serve mapping is already in place.
# ============================================================
configure_tailscale_proxy() {
if ! command -v tailscale >/dev/null 2>&1; then
log_warn "tailscale CLI not found, skipping reverse-proxy setup."
return 0
fi
# 1. Authenticate the node (if it isn't already).
local backend_state
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
if [[ "$backend_state" != "Running" ]]; then
if [[ -n "$TS_AUTHKEY" ]]; then
log_info "Bringing Tailscale up with provided auth key..."
tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$HOSTNAME_LXC" \
|| log_warn "tailscale up failed — run it manually inside the LXC."
else
log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied."
log_warn "Finish setup inside the LXC with: tailscale up --ssh"
log_warn "Then publish OpenBao with: tailscale serve --bg --https=443 http://${OPENBAO_LISTEN_ADDR}"
return 0
fi
fi
# 2. Publish the local OpenBao listener on the tailnet (auto-HTTPS).
if tailscale serve status 2>/dev/null | grep -q "${OPENBAO_LISTEN_ADDR}"; then
log_info "Tailscale serve already publishes http://${OPENBAO_LISTEN_ADDR}."
else
log_info "Publishing OpenBao on the tailnet via 'tailscale serve' (HTTPS:443)..."
tailscale serve --bg --https=443 "http://${OPENBAO_LISTEN_ADDR}" \
|| log_warn "tailscale serve failed — enable HTTPS on your tailnet and retry."
fi
local fqdn
fqdn=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // ""' | sed 's/\.$//')
if [[ -n "$fqdn" ]]; then
log_info "OpenBao should now be reachable at: https://${fqdn}"
if [[ "$OPENBAO_API_ADDR" != "https://${fqdn}" ]]; then
log_warn "OPENBAO_API_ADDR is '${OPENBAO_API_ADDR}'."
log_warn "For OIDC / UI redirects, set it to 'https://${fqdn}' and re-run, or edit ${BAO_CONFIG_DIR}/config.hcl."
fi
fi
}
# ============================================================
# Proxmox-host helpers
# ============================================================
# Detect newest Alpine LXC template available from the Proxmox repos.
detect_latest_alpine_template() {
local tmpl
tmpl=$(pveam available --section system 2>/dev/null \
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
| sort -V \
| tail -n1)
if [[ -z "$tmpl" ]]; then
log_warn "Could not query pveam; falling back to a known-good Alpine template."
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
fi
log_info "Selected Alpine template: $tmpl"
echo "$tmpl"
}
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
find_existing_lxc() {
local id host tags
while read -r id _; do
[[ -z "$id" || "$id" == "VMID" ]] && continue
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
echo "$id"
return 0
fi
done < <(pct list | awk 'NR>1 {print $1}')
return 1
}
ensure_template_present() {
local tmpl="$1"
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
pveam update >/dev/null
pveam download "$TEMPLATE_STORAGE" "$tmpl"
else
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
fi
}
# Pick next available CTID if user did not provide one.
allocate_ctid() {
pvesh get /cluster/nextid 2>/dev/null \
|| pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
| jq '[.[].vmid] | max + 1' \
|| echo 100
}
# Inject the script into the container and execute it in the requested mode.
# Forwards the relevant runtime configuration through the environment so the
# inner invocation produces the same config the user requested on the host.
exec_in_lxc() {
local ctid="$1"
local mode="$2" # --install or --update
# Ensure base tooling exists inside the container before piping the script.
pct exec "$ctid" -- sh -c "apk add --no-cache bash curl jq ca-certificates >/dev/null 2>&1"
curl -fsSL "$SCRIPT_URL" \
| pct exec "$ctid" -- env \
SCRIPT_URL="$SCRIPT_URL" \
OPENBAO_VERSION="$OPENBAO_VERSION" \
OPENBAO_HOSTNAME="$HOSTNAME_LXC" \
OPENBAO_LISTEN_ADDR="$OPENBAO_LISTEN_ADDR" \
OPENBAO_API_ADDR="$OPENBAO_API_ADDR" \
TS_AUTHKEY="$TS_AUTHKEY" \
bash -s -- "$mode"
}
# ============================================================
# MODE: Proxmox host — create LXC + install
# ============================================================
create_lxc() {
log_info "=== OpenBao — LXC creation ==="
if [[ -z "$TEMPLATE" ]]; then
TEMPLATE=$(detect_latest_alpine_template)
else
log_info "Using user-provided template: $TEMPLATE"
fi
ensure_template_present "$TEMPLATE"
if [[ -z "$CTID" ]]; then
CTID=$(allocate_ctid)
log_info "Auto-selected CTID: $CTID"
fi
log_info "Creating LXC ${CTID} (${HOSTNAME_LXC})..."
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
--hostname "$HOSTNAME_LXC" \
--cores "$CORES" \
--memory "$RAM" \
--rootfs "${STORAGE}:${DISK}" \
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
--unprivileged 1 \
--features "nesting=1" \
--tags "infra-script,${LXC_TAG}" \
--onboot 1 \
--start 0
# Tailscale needs /dev/net/tun inside the unprivileged container.
log_info "Adding /dev/net/tun passthrough for Tailscale..."
cat >> "/etc/pve/lxc/${CTID}.conf" <<EOF
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
EOF
log_info "Starting LXC ${CTID}..."
pct start "$CTID"
# Wait for network to come up
local tries=0
until pct exec "$CTID" -- sh -c "ip -4 addr show eth0 | grep -q 'inet '" 2>/dev/null; do
tries=$((tries + 1))
if (( tries > 20 )); then
log_error "LXC ${CTID} did not acquire an IP after 20s."
exit 1
fi
sleep 1
done
log_info "Running installer inside LXC ${CTID}..."
exec_in_lxc "$CTID" "--install"
local ip
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
echo ""
log_info "========================================="
log_info "LXC ${CTID} created successfully!"
log_info "========================================="
echo ""
echo " Hostname : ${HOSTNAME_LXC}"
echo " IP : ${ip:-pending}"
echo " API : http://${ip:-<ip>}:8200"
echo ""
echo "Next steps:"
echo " pct enter ${CTID}"
echo " export VAULT_ADDR=http://127.0.0.1:8200"
echo " bao operator init # initialise & retrieve unseal keys + root token"
echo " bao operator unseal # repeat with the unseal keys"
echo ""
}
# ============================================================
# MODE: Proxmox host — update existing LXC
# ============================================================
update_lxc() {
local ctid="$1"
log_info "=== OpenBao — updating existing LXC ${ctid} ==="
if ! pct status "$ctid" | grep -q running; then
log_info "Starting LXC ${ctid}..."
pct start "$ctid"
sleep 3
fi
log_info "Refreshing Alpine packages inside LXC ${ctid}..."
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
log_info "Upgrading bao binary inside LXC ${ctid}..."
exec_in_lxc "$ctid" "--update"
log_info "Update of LXC ${ctid} complete."
}
# ============================================================
# MODE: inside LXC — fresh install of OpenBao
# ============================================================
install_inside_lxc() {
log_info "=== OpenBao — installation ==="
log_info "Updating package index..."
apk update >/dev/null
apk upgrade >/dev/null
log_info "Installing dependencies..."
apk add --no-cache bash curl jq ca-certificates gcompat openrc logrotate tailscale >/dev/null
log_info "Enabling tailscaled..."
rc-update add tailscale default >/dev/null 2>&1 || true
rc-service tailscale start >/dev/null 2>&1 || log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)"
install_or_upgrade_bao
log_info "Creating ${BAO_USER} system user..."
if ! id "$BAO_USER" >/dev/null 2>&1; then
addgroup -S "$BAO_USER" 2>/dev/null || true
adduser -S -D -H -h "$BAO_DATA_DIR" -s /sbin/nologin -G "$BAO_USER" "$BAO_USER"
fi
log_info "Provisioning directories..."
mkdir -p "$BAO_CONFIG_DIR" "$BAO_DATA_DIR/data"
chown -R "${BAO_USER}:${BAO_USER}" "$BAO_DATA_DIR"
chmod 750 "$BAO_DATA_DIR"
if [[ ! -f "${BAO_CONFIG_DIR}/config.hcl" ]]; then
log_info "Writing default ${BAO_CONFIG_DIR}/config.hcl..."
# OpenBao listens on loopback only; Tailscale (running in the same LXC)
# acts as the reverse proxy and terminates TLS via tailnet certificates.
# https://openbao.org/docs/configuration/
cat > "${BAO_CONFIG_DIR}/config.hcl" <<EOF
ui = true
disable_mlock = true
storage "raft" {
path = "${BAO_DATA_DIR}/data"
node_id = "${HOSTNAME_LXC}"
}
listener "tcp" {
address = "${OPENBAO_LISTEN_ADDR}"
tls_disable = 1
}
api_addr = "${OPENBAO_API_ADDR}"
cluster_addr = "http://127.0.0.1:8201"
EOF
chown root:"$BAO_USER" "${BAO_CONFIG_DIR}/config.hcl"
chmod 640 "${BAO_CONFIG_DIR}/config.hcl"
else
log_info "Existing ${BAO_CONFIG_DIR}/config.hcl preserved."
fi
log_info "Installing OpenRC service..."
cat > /etc/init.d/openbao <<'EOF'
#!/sbin/openrc-run
name="OpenBao"
description="OpenBao secrets manager"
command="/usr/local/bin/bao"
command_args="server -config=/etc/openbao/config.hcl"
command_user="openbao:openbao"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
directory="/var/lib/openbao"
output_log="/var/log/openbao.log"
error_log="/var/log/openbao.log"
depend() {
need net
after net
}
start_pre() {
checkpath --directory --owner openbao:openbao --mode 0750 /var/lib/openbao
checkpath --directory --owner openbao:openbao --mode 0750 /var/lib/openbao/data
checkpath --file --owner openbao:openbao --mode 0644 /var/log/openbao.log
}
EOF
chmod +x /etc/init.d/openbao
rc-update add openbao default >/dev/null
cat > /etc/logrotate.d/openbao <<'EOF'
/var/log/openbao.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
}
EOF
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
log_info "Starting openbao service..."
rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log"
log_info "Enabling console auto-login on tty1..."
mkdir -p /etc/conf.d
cat > /etc/conf.d/agetty <<'EOF'
agetty_options="--autologin root --noclear"
EOF
sed -i 's|^tty1::respawn:/sbin/getty.*|tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1|' /etc/inittab
configure_tailscale_proxy
log_info "Configuring MOTD..."
# /etc/profile.d/ runs for every interactive login shell — works for both
# the auto-login tty and Tailscale SSH. Quoted heredoc: every variable is
# resolved at login time, not at install time.
cat > /etc/profile.d/00-openbao.sh <<'MOTD'
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
/"Self"/ { in_self=1 }
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
')
[[ -z "$TS_FQDN" ]] && TS_FQDN="$(hostname).ts.net"
BAO_VERSION=$(cat /opt/openbao_version.txt 2>/dev/null || echo "unknown")
# `bao status` exit codes: 0 = unsealed, 2 = sealed, anything else = error.
VAULT_ADDR=http://127.0.0.1:8200 /usr/local/bin/bao status >/dev/null 2>&1
case $? in
0) SEAL_STATE="unsealed" ;;
2) SEAL_STATE="SEALED (run: bao operator unseal)" ;;
*) SEAL_STATE="unreachable" ;;
esac
echo ""
echo " ___ ____ "
echo " / _ \ _ __ ___ _ __ | __ ) __ _ ___ "
echo "| | | | '_ \ / _ \ '_ \| _ \ / _\` |/ _ \\"
echo "| |_| | |_) | __/ | | | |_) | (_| | (_) |"
echo " \___/| .__/ \___|_| |_|____/ \__,_|\___/"
echo " |_| "
echo ""
echo "OpenBao Secrets Manager (${BAO_VERSION})"
echo "─────────────────────────────────────────"
echo "Access:"
echo " • API (local) : http://127.0.0.1:8200"
echo " • Tailnet : https://${TS_FQDN}"
echo " • Seal status : ${SEAL_STATE}"
echo ""
echo "Useful commands:"
echo " export VAULT_ADDR=http://127.0.0.1:8200"
echo " bao status"
echo " bao operator init # first-time only"
echo " bao operator unseal # after every restart"
echo " rc-service openbao status"
echo " tail -f /var/log/openbao.log"
echo "─────────────────────────────────────────"
echo ""
MOTD
chmod +x /etc/profile.d/00-openbao.sh
log_info "Cleaning up..."
rm -rf /var/cache/apk/*
echo ""
log_info "========================================="
log_info "OpenBao installation complete!"
log_info "========================================="
echo ""
echo "Initialise the server with:"
echo " export VAULT_ADDR=http://127.0.0.1:8200"
echo " bao operator init"
echo " bao operator unseal # repeat with each unseal key share"
echo ""
}
# ============================================================
# MODE: inside LXC — update only
# ============================================================
update_inside_lxc() {
log_info "=== OpenBao — update ==="
apk update >/dev/null
apk upgrade >/dev/null
install_or_upgrade_bao
configure_tailscale_proxy
log_info "Update complete."
}
# ============================================================
# Main — dispatch on explicit mode flag or auto-detect context
# ============================================================
main() {
case "${1:-}" in
--install)
install_inside_lxc
return
;;
--update)
update_inside_lxc
return
;;
esac
if command -v pct >/dev/null 2>&1; then
# Running on a Proxmox host
require_root
local existing=""
if existing=$(find_existing_lxc); then
log_info "Found existing OpenBao LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
update_lxc "$existing"
else
create_lxc
fi
else
# Inside a container (no Proxmox tooling)
require_root
if [[ -x /usr/local/bin/bao ]]; then
update_inside_lxc
else
install_inside_lxc
fi
fi
}
main "$@"