diff --git a/README.md b/README.md index 73a4580..836f2a9 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,22 @@ These scripts automate the deployment of personal infrastructure components. The - **Idempotent-ish**: Safe to re-run (where possible) - **Curl-friendly**: Designed for one-liner deployment from a fresh server - **Multi-OS**: Supports Debian and Alpine-based deployments +- **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination +- **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files) +- **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access) +- **Keep it simple**: One script per service, plain bash, no frameworks — readability over cleverness ### Available Scripts -| Script | Description | Usage | -|--------|-------------|-------| -| [`proxy/install.sh`](proxy/) | Reverse proxy with Tailscale + Nginx Proxy Manager | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh` \| `bash` | -| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` | -| [`seedbox/install.sh`](seedbox/) | ISO seedbox with Transmission + NFS | `NFS_SERVER= curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/seedbox/install.sh` \| `bash` | -| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` | +| Script | Description | Usage | +| ------------------------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| [`proxy/install.sh`](proxy/) | Reverse proxy with Tailscale + Nginx Proxy Manager | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh` \| `bash` | +| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` | +| [`seedbox/install.sh`](seedbox/) | ISO seedbox with Transmission + NFS | `NFS_SERVER= curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/seedbox/install.sh` \| `bash` | +| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` | ### Requirements - Fresh Debian 12/13 installation (proxy, netlab, seedbox) or Proxmox VE host (gitea-runner) - User with sudo privileges (do not run as root) — except gitea-runner which runs as root on Proxmox -- Internet access \ No newline at end of file +- Internet access diff --git a/openbao/README.md b/openbao/README.md new file mode 100644 index 0000000..184c629 --- /dev/null +++ b/openbao/README.md @@ -0,0 +1,123 @@ +# OpenBao + +Automated installation and update script for an [OpenBao](https://openbao.org) secrets-manager +server running inside an Alpine LXC on Proxmox. + +### Features + +Single script, automatic mode selection: + +| Context | Action | +| ---------------------------------------------------- | --------------------------------------------------------------------------------- | +| From Proxmox host, no existing OpenBao container | Detects newest Alpine template, creates LXC, installs `bao` + OpenRC service | +| From Proxmox host, OpenBao container already present | Reuses the existing LXC, refreshes packages, upgrades `bao` to the latest release | +| From inside an LXC, no `bao` binary | Installs OpenBao from scratch | +| From inside an LXC, `bao` already present | Updates the binary only (no config / data changes) | + +The container is identified by hostname **and** the `openbao` tag, so it is +re-found across reruns even if the CTID was auto-allocated the first time. + +### Requirements + +- Proxmox VE host with `pveam`, `pct`, `pvesh`, `jq` available +- Internet access from both the host (template download) and the LXC (binary download) +- Script must be run as **root** on the Proxmox host (enforced; the Web UI shell qualifies) + +### Usage + +#### Full install (from Proxmox shell) + +```bash +bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)" +``` + +Re-running the exact same command later upgrades packages inside the LXC and +brings the `bao` binary to the latest release, without touching the config or +the raft data directory. + +#### Customisation + +Every parameter is exposed as an environment variable: + +| Variable | Default | Description | +| --------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) | +| `OPENBAO_HOSTNAME` | `openbao` | LXC hostname (also used as raft `node_id`) | +| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` | +| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk | +| `TEMPLATE_STORAGE` | `local` | Storage where Alpine templates live | +| `CORES` | `2` | vCPU cores | +| `RAM` | `1024` | RAM in MiB | +| `DISK` | `8` | Root disk size in GB | +| `BRIDGE` | `vmbr0` | Network bridge | +| `LXC_TAG` | `openbao` | Stable tag used to re-discover the container | +| `OPENBAO_VERSION` | `latest` | Pin a specific release (e.g. `v2.0.3`) or `latest` | +| `OPENBAO_LISTEN_ADDR` | `127.0.0.1:8200` | TCP listener address. Loopback by default — Tailscale fronts it. | +| `OPENBAO_API_ADDR` | `http://` | Public API URL (used for UI / OIDC redirects). Set to `https://..ts.net` once known. | +| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at ). If unset, finish `tailscale up` manually inside the LXC. | + +```bash +CTID=210 OPENBAO_HOSTNAME=vault CORES=4 RAM=2048 \ + bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)" +``` + +#### First-time initialisation + +OpenBao starts sealed. Once the LXC is up: + +```bash +pct enter +export VAULT_ADDR=http://127.0.0.1:8200 +bao operator init # save the unseal keys + root token somewhere safe +bao operator unseal # repeat with each key share until unsealed +``` + +#### Tailscale reverse proxy + +The listener binds to `127.0.0.1:8200` only — Tailscale (running inside the +same LXC) acts as the reverse proxy and terminates TLS via tailnet +certificates. + +If `TS_AUTHKEY` was supplied at install time, the script runs +`tailscale up` and `tailscale serve --bg --https=443 http://127.0.0.1:8200` +automatically. OpenBao then becomes reachable at +`https://..ts.net`. + +Otherwise, finish setup manually inside the LXC: + +```bash +pct enter +tailscale up --ssh +tailscale serve --bg --https=443 http://127.0.0.1:8200 +tailscale status # prints the tailnet FQDN +``` + +Then point `OPENBAO_API_ADDR` at that FQDN and rerun the script so the UI / OIDC redirects use it: + +```bash +OPENBAO_API_ADDR=https://openbao..ts.net \ + bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh)" +``` + +> HTTPS in `tailscale serve` requires HTTPS to be enabled on your tailnet +> (Admin console → DNS → HTTPS Certificates). + +#### Update (from inside the LXC) + +```bash +curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh | bash +``` + +The script auto-detects the presence of `/usr/local/bin/bao` and switches to +update mode. The OpenRC service is stopped, the binary is swapped (the old one +is kept as `bao.bak.`), then the service is restarted. + +### Architecture + +- **OS**: latest Alpine LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale +- **Binary**: official `bao` release from `github.com/openbao/openbao`, installed in `/usr/local/bin` +- **Service**: OpenRC, runs as user `openbao`, logs to `/var/log/openbao.log` (rotated daily, 7 days retained) +- **Network**: listener bound to `127.0.0.1:8200`; **Tailscale** runs in the LXC and acts as the reverse proxy (`tailscale serve --https=443`) +- **Config**: `/etc/openbao/config.hcl` — raft storage, TLS disabled on the listener (Tailscale terminates TLS), `disable_mlock = true` for unprivileged LXC +- **Data**: `/var/lib/openbao/data` (raft) +- **Version tracking**: `/opt/openbao_version.txt` records the currently installed tag for idempotent reruns diff --git a/openbao/install.sh b/openbao/install.sh new file mode 100755 index 0000000..c8be7f3 --- /dev/null +++ b/openbao/install.sh @@ -0,0 +1,610 @@ +#!/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..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__Linux_.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" </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:-}: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" < /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..." + # Alpine ships busybox getty by default; agetty (from util-linux) is what + # supports --autologin. + apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null + + # Replace any existing tty1 entry, then append our autologin line. Doing it + # in two steps (delete + append) is more robust than an in-place sed against + # a pattern that may drift across Alpine releases. + sed -i '/^tty1::/d' /etc/inittab + echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab + + # Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot. + kill -HUP 1 2>/dev/null || true + + # Kick any getty/agetty still attached to tty1 so init respawns it *now* with + # the new line — otherwise the first web-console session lands on the stale + # process and the operator has to type `exit` once before autologin kicks in. + pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true + + 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 "$@"