fix(openbao): validate the publish bind before writing any state
deploy.sh narrows the Docker publish bind to OPENBAO_ADDR when that is an IP, but never checked that the host actually holds that address. A typo'd IP therefore failed late, inside `docker compose up`: failed to bind host port 192.160.100.50:8200/tcp: cannot assign requested address ...by which point .env had been seeded and the TLS cert generated with the bad address in its SAN. Neither is rewritten on a re-run (.env is never overwritten, gen-tls.sh never regenerates over an existing pair), so re-running with a corrected OPENBAO_ADDR silently changed nothing. Add host_addrs() + check_bind_addr(), run before anything is written: - lists the host's addresses from plain `ip addr show` -- no -o/scope filters, since busybox ip supports neither -- falling back to ifconfig, and skipping the check when neither exists rather than blocking; - skips 0.0.0.0 / :: / *, and unwraps an [IPv6] publish literal; - SKIP_BIND_CHECK=1 overrides for an address that only comes up later. Resolve the bind compose will really interpolate, which follows compose's own precedence -- shell environment before .env: - exported (automations.sh passes answers via `env VAR=...`, or a standalone OPENBAO_BIND=... run): the environment wins, so warn when .env disagrees, because a later bare `docker compose up` would not; - derived here: that assignment is not exported, so .env wins; - in neither: compose falls back to 0.0.0.0 and publishes the API on every interface -- warn, since that is a silent exposure. Also warn when .env's OPENBAO_ADDR differs from this run's, naming the cert that has to be deleted for the SAN to be regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ internet.
|
|||||||
| Variable | Notes |
|
| Variable | Notes |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `OPENBAO_ADDR` | IP/DNS the Kanrisha tape host uses to reach this vault (goes in the cert SAN; you point the daemon at `https://$OPENBAO_ADDR:8200`). |
|
| `OPENBAO_ADDR` | IP/DNS the Kanrisha tape host uses to reach this vault (goes in the cert SAN; you point the daemon at `https://$OPENBAO_ADDR:8200`). |
|
||||||
| `OPENBAO_BIND` | Host interface the API publishes on. Left at the default, deploy.sh narrows it to `OPENBAO_ADDR` when that is an IP (a published port bypasses the INPUT firewall, so this bind is the real restriction — see [Exposure](#exposure)). |
|
| `OPENBAO_BIND` | Host interface the API publishes on. Left at the default, deploy.sh narrows it to `OPENBAO_ADDR` when that is an IP (a published port bypasses the INPUT firewall, so this bind is the real restriction — see [Exposure](#exposure)). It must be an address this host actually holds — deploy.sh checks that before writing anything and refuses otherwise (`SKIP_BIND_CHECK=1` to override). |
|
||||||
| `OPENBAO_TLS_SANS` | **Extra** SANs beyond `OPENBAO_ADDR` + loopback (which deploy.sh always adds). Read from the environment at deploy time — `export` it before running deploy.sh. |
|
| `OPENBAO_TLS_SANS` | **Extra** SANs beyond `OPENBAO_ADDR` + loopback (which deploy.sh always adds). Read from the environment at deploy time — `export` it before running deploy.sh. |
|
||||||
| `OPENBAO_TAG` | OpenBao image tag (pin it). |
|
| `OPENBAO_TAG` | OpenBao image tag (pin it). |
|
||||||
| `OPENBAO_HSM_PIN` | Only for PKCS#11 auto-unseal. Leave blank for manual unseal. |
|
| `OPENBAO_HSM_PIN` | Only for PKCS#11 auto-unseal. Leave blank for manual unseal. |
|
||||||
@@ -57,6 +57,34 @@ docker compose exec -e BAO_ADDR=https://127.0.0.1:8200 openbao \
|
|||||||
**Store the unseal keys + root token out of band** — ideally age-encrypted with
|
**Store the unseal keys + root token out of band** — ideally age-encrypted with
|
||||||
your backup recipient (`globals/age-pubkey.txt`), never on this host.
|
your backup recipient (`globals/age-pubkey.txt`), never on this host.
|
||||||
|
|
||||||
|
### Re-running after a wrong address
|
||||||
|
|
||||||
|
`deploy.sh` is idempotent, but two things it writes are deliberately **sticky**:
|
||||||
|
`.env` (never overwritten) and `tls/tls.{crt,key}` (never regenerated over an
|
||||||
|
existing pair). So passing a corrected `OPENBAO_ADDR` to a re-run does *not* move
|
||||||
|
the vault — the cert keeps the old SAN, and the old `.env` usually still decides
|
||||||
|
the bind. Which `OPENBAO_BIND` wins follows Compose's own precedence:
|
||||||
|
|
||||||
|
| How `OPENBAO_BIND` is set | What Compose uses |
|
||||||
|
|---------------------------|-------------------|
|
||||||
|
| Exported into deploy.sh's environment (`OPENBAO_BIND=… bash deploy.sh`, or answering the bind prompt — `automations.sh` passes answers via `env VAR=…`) | the **environment** value; `.env` is ignored for this run, so a later hand-run `docker compose up -d` can bind somewhere else |
|
||||||
|
| Derived by deploy.sh (bind prompt left blank → narrowed to `OPENBAO_ADDR`) | the **`.env`** value, since that assignment is never exported |
|
||||||
|
| Absent from both | `0.0.0.0` — **all interfaces** |
|
||||||
|
|
||||||
|
deploy.sh warns on each of those mismatches. To actually change the address:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/openbao
|
||||||
|
docker compose down # keeps the raft volume
|
||||||
|
sed -i 's/OLD_IP/NEW_IP/g' .env # OPENBAO_ADDR + OPENBAO_BIND
|
||||||
|
rm -f tls/tls.crt tls/tls.key # force a new SAN
|
||||||
|
# then re-run deploy.sh with the corrected OPENBAO_ADDR
|
||||||
|
```
|
||||||
|
|
||||||
|
A bind address the host does not hold is caught up front; without that check
|
||||||
|
Docker fails the `up` with `cannot assign requested address` only *after* the
|
||||||
|
bad value is already in `.env` and the cert.
|
||||||
|
|
||||||
## TLS
|
## TLS
|
||||||
|
|
||||||
- **Self-signed (default):** `deploy.sh` runs [`gen-tls.sh`](gen-tls.sh) to
|
- **Self-signed (default):** `deploy.sh` runs [`gen-tls.sh`](gen-tls.sh) to
|
||||||
|
|||||||
@@ -28,10 +28,19 @@ set -euo pipefail
|
|||||||
|
|
||||||
: "${STACK_DIR:=/srv/openbao}"
|
: "${STACK_DIR:=/srv/openbao}"
|
||||||
: "${SKIP_DOCKER_INSTALL:=0}"
|
: "${SKIP_DOCKER_INSTALL:=0}"
|
||||||
|
: "${SKIP_BIND_CHECK:=0}" # 1 = publish on an address this host does not (yet) have
|
||||||
: "${FORCE:=0}"
|
: "${FORCE:=0}"
|
||||||
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
|
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
|
||||||
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
||||||
: "${OPENBAO_ADDR:=}"
|
: "${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}"
|
: "${OPENBAO_BIND:=0.0.0.0}"
|
||||||
: "${DISABLE_SWAP:=1}" # set 0 to skip swapoff (mlock then only best-effort)
|
: "${DISABLE_SWAP:=1}" # set 0 to skip swapoff (mlock then only best-effort)
|
||||||
|
|
||||||
@@ -109,6 +118,43 @@ open_bao_port() {
|
|||||||
fi
|
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
|
||||||
|
addrs="$(host_addrs)"
|
||||||
|
# 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 "No ip/ifconfig to list this host's addresses; 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() {
|
||||||
[[ "$DISABLE_SWAP" == "1" ]] || { warn "DISABLE_SWAP=0 -- mlock will be best-effort."; return; }
|
[[ "$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
|
# Detect active swap via /proc/swaps (a header line + one line per device) so
|
||||||
@@ -186,6 +232,43 @@ if [[ "$OPENBAO_BIND" == "0.0.0.0" ]]; then
|
|||||||
esac
|
esac
|
||||||
fi
|
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"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Lay down the stack
|
# Lay down the stack
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user