From dc9761a66867dcf54dffe6ef863a2cad8cfaad51 Mon Sep 17 00:00:00 2001 From: William Gill Date: Wed, 16 Sep 2026 15:01:56 -0500 Subject: [PATCH] feat(openbao): serve the web UI, so init/unseal can happen in a browser config.hcl shipped `ui = false`, so there was no UI to reach even with working connectivity. That forces init and unseal through `docker compose exec`, which puts the unseal keys and root token into a server shell's history and scrollback -- the one place they should never be. Make it a substitution point driven by OPENBAO_UI, default on. This adds no exposure: the UI is served on the same listener, and anything that can reach :8200 can already do everything through the API. What it changes is where the unseal material is displayed -- the operator's browser instead of the host. Rendering config.hcl rather than copying it needed three things to be right: - Precedence. OPENBAO_UI passed to a run wins; otherwise the value .env already deploys applies; otherwise the default. Without that, `OPENBAO_UI=0 bash deploy.sh` against an existing node would report success and change nothing. Env-presence is captured before the ":=" default, so an explicit 0 is distinguishable from "not mentioned" and a re-run cannot silently re-enable the UI. A value passed this run is written back to .env. - Reload. A bind-mounted file's CONTENTS are not part of the compose config hash, so `up -d` leaves a changed config.hcl unloaded -- the same trap fixed in copyparty (76d2a09) and simplex (0f22735). Restart only on an actual change, and say loudly that a restart RE-SEALS the vault, because that is not a free action on a running one. - `cmp -s` stays inside an `if` condition; as `cmp -s A B && CONFIG_CHANGED=1` it would trip set -e whenever the files matched. Verified: default renders `ui = true` and seeds .env; an identical re-run reports no change; .env's 0 is honoured when nothing is passed; a passed 1 beats .env's 0 and is written through; a non-boolean value dies. Note for LAN-isolated hosts: the listener publishes on OPENBAO_BIND only, so a browser on another subnet still cannot reach it. The generated cert already carries DNS:localhost + IP:127.0.0.1 in its SANs, so `ssh -L 8200::8200` and https://localhost:8200 works against the existing cert without widening the publish. Co-Authored-By: Claude Opus 5 --- deployments/openbao/.env.example | 7 ++ deployments/openbao/config.hcl | 7 +- deployments/openbao/deploy.sh | 203 ++++++++++++++++++++----------- 3 files changed, 147 insertions(+), 70 deletions(-) diff --git a/deployments/openbao/.env.example b/deployments/openbao/.env.example index 75ebe12..a4bc9c7 100644 --- a/deployments/openbao/.env.example +++ b/deployments/openbao/.env.example @@ -25,6 +25,13 @@ OPENBAO_BIND=0.0.0.0 OPENBAO_TLS_SANS= OPENBAO_TLS_DAYS=825 +# ─── Web UI ───────────────────────────────────────────────────────────────── +# 1 = serve the built-in web UI on the same listener (default), 0 = API only. +# Not a new exposure: anything that can reach :8200 can already do everything +# via the API. With the UI on you can initialise and unseal from a browser, +# which keeps the unseal keys out of a server shell's history. +OPENBAO_UI=1 + # ─── Image tag ────────────────────────────────────────────────────────────── # Pin for reproducible deploys. OPENBAO_TAG=2.5.5 diff --git a/deployments/openbao/config.hcl b/deployments/openbao/config.hcl index 178ff1a..b3b39ed 100644 --- a/deployments/openbao/config.hcl +++ b/deployments/openbao/config.hcl @@ -34,7 +34,12 @@ listener "tcp" { # reachable LAN address in the compose env instead. api_addr = "https://127.0.0.1:8200" cluster_addr = "https://127.0.0.1:8201" -ui = false +# The built-in web UI. Substituted by deploy.sh from OPENBAO_UI. It adds NO new +# exposure: it is served on this same listener, and anything reachable on :8200 +# can already do everything through the API. Turning it on is what lets an +# operator initialise/unseal and log in from a browser, so the unseal keys are +# shown in the browser instead of a server shell's scrollback. +ui = @UI@ # ── Auto-unseal (optional) — DEFAULT IS MANUAL UNSEAL ──────────────────────── # With no seal stanza OpenBao starts SEALED and needs `bao operator unseal` (x3) diff --git a/deployments/openbao/deploy.sh b/deployments/openbao/deploy.sh index 8465f26..4cb166c 100644 --- a/deployments/openbao/deploy.sh +++ b/deployments/openbao/deploy.sh @@ -42,6 +42,12 @@ set -euo pipefail 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' "$*"; } @@ -273,6 +279,22 @@ if [[ -f "$STACK_DIR/.env" ]]; then 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 # ---------------------------------------------------------------------------- @@ -281,7 +303,20 @@ 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. -install -m 0644 "$SCRIPT_DIR/config.hcl" "$STACK_DIR/config.hcl" +# 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" @@ -292,9 +327,27 @@ if [[ ! -f "$ENV_FILE" ]]; then -e "s|^OPENBAO_ADDR=.*|OPENBAO_ADDR=${OPENBAO_ADDR}|" \ -e "s|^OPENBAO_BIND=.*|OPENBAO_BIND=${OPENBAO_BIND}|" \ -e "s|^OPENBAO_TLS_SANS=.*|OPENBAO_TLS_SANS=${SANS}|" \ + -e "s|^OPENBAO_UI=.*|OPENBAO_UI=${EFFECTIVE_UI}|" \ "$ENV_FILE" 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 + _t=$(mktemp) + _SE_V="$EFFECTIVE_UI" awk ' + BEGIN { v = ENVIRON["_SE_V"]; seen = 0 } + !seen && index($0, "OPENBAO_UI=") == 1 { print "OPENBAO_UI=" v; seen = 1; next } + { print } + END { if (!seen) print "OPENBAO_UI=" v } + ' "$ENV_FILE" > "$_t" + cat "$_t" > "$ENV_FILE"; rm -f "$_t" + log " OPENBAO_UI: ${_cur_ui:-} -> ${EFFECTIVE_UI}" + fi + fi fi # ---------------------------------------------------------------------------- @@ -372,7 +425,14 @@ elif [[ "$_data_uid" != "$BAO_UID" ]]; then 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. @@ -452,71 +512,76 @@ fi # __ARCHIVE_BELOW__ is the embedded tar.gz payload (base64), added by build.sh. exit 0 __ARCHIVE_BELOW__ -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 -X4KPvLtZu4qKIa3tFPAIGeCmxp/ybTK4xJv/Yr3HgS74fagIJsvTYOnz/QYrb11jzOjlkVyM2+TB -RwviL8rrPsx1Www3Y7pPwyouDfNtGb7c8tnbMh+/JWMyT7kHN+WKqylM98qytDa1a3OfbKtCDYM9 -L+/TSDpuQ4IoW+1VbbJkhFLW2DlMliX21lZh+6hJaZrWtKY1rWlNa1rTmta0pjWtaU1rWtOa1rSm -Na1pTWta05rWtKY1rWlNa1rTmvZf1v4G3yMW1ABQAAA= +H4sIAAAAAAAAA+1a/3LbuLXO33oKXHlnV5o1KcmO473KeKdaW00068iuZXdvJ81oKRGSWJMES5C2 +ddPM9CH6hH2SfucA/CE52bRTN729I8QTmyRwABycX9858NX8VqbOXEWJ0tJdR+GzJ29dtBfPnz/r +vuj2jo96/Lvb7fF7tOcvjo+e9Y4OjrovDg+PD9Gvd9jt9Z6J7tMv5XHLdealQjw7Op7+pMI7+SXm +/D/U9sRFIuMfPCXAh/mt+Ouf/yJWXurLWPoi8xLpyHierpMsULG4lWt0U6kUC5WKH704DfTKcxt7 +jT1xrRIVquVatLQXSed8MN4X4wuRyjuZaimSVD2s2310FOVApi9WSmfCcVZZlmjH+V4orGfmqf53 +B90uOrdiLwvupLg+nwgsIVtJceflYSaCTMtw0ebJb+IwuJX8UeG/1GxGi4AGBBqrSBR9xcrxFCtx +6vn+WnTEucy+0WJottgXHmhpOU9lpu1GM5lGAZYgNa/ATCq82BdM1puvwCeFPfLkvOtYZfQAUkk+ +C4M5VgEqscxcQ0ILTxAVRwdL4vJcpplo+TIJ1drVK7EE71OeMcjaAoymVZ0Oiu6JF6RirXLhpyqh +HbqdLNSiJd2lK+4Cj76lYhJ5YagzmWCkWd/g9M2w7TYaMY6nX3C50bhTYR5J3W+I4p3je5nXF9z2 +ROotMt7BktbkM1u8pWRBKQ8DPExwzAEmjDNifiYbDS3Tu2C+QZr+FCKIQKBcQqf49tX7i8vh+IfB +xfR68KrvHLhH7tEHHjBXceYF4Mp0c/HmWxThOPqCpsM2YcriRbA8Kch2zLP95a7mIQ9LJek9jjyP +Q6m1g20lifQbZtNRCLPYh7yDfyT0ETaUBl4o1GIh9L2XuGJ0eTo9vzj9UXxLJIIoIOZEkkeKUBoR +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