0eb7f2666360240d3ffabc9617a1238665bbc3bd
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0eb7f26663 |
fix(openbao): never put the vault token on a command line
`BAO_TOKEN=<root> bash update.sh update` -- which the previous commit and the pre-existing DR runbook both recommended -- leaks the token twice over. It goes into the shell history, and `docker compose exec -e BAO_TOKEN=<value>` puts it in the docker process's argv, where /proc/<pid>/cmdline makes it readable by every user on the host. The second is the worse one and is not fixed by anything the operator does at their prompt. The token now reaches the container over STDIN and is never an argument to anything: the container's own shell reads one line, exports it, and execs bao. Verified locally that the child process sees the exact value -- including &, | and a backslash -- while its argv contains zero occurrences of it. How a token is supplied, in order: an already-exported BAO_TOKEN; a 0600 file named by the new BAO_TOKEN_FILE; otherwise an echo-off prompt. With no token and no terminal it dies telling the operator to use BAO_TOKEN_FILE rather than an inline assignment, and says why. Added a `snapshot` subcommand so the DR runbook no longer needs an inline pipeline at all. That runbook and the deploy banner both carried the -e form before this deployment had an updater, so both are corrected: taking a backup is now `bash update.sh snapshot`, and restore keeps the stdin shape rather than -e. The Kanrisha bootstrap lines say to export the token first instead of passing it inline. The conf file still refuses to hold a token, but now points at BAO_TOKEN_FILE as the unattended answer -- and suggests a snapshot-policy token rather than the root token for it. Not verified without a live host: that `docker compose exec -T` forwards stdin as expected. If it does not, the snapshot fails loudly with a permission error and the upgrade aborts before touching anything, which is the safe direction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2d3d322b0e |
feat(openbao): add update.sh, an updater that refuses to strand a sealed vault
Follows the copyparty/ergo updater idiom -- check/update/run/install/uninstall, a conf file the environment overrides, the version pinned into .env so the running release is explicit, DRY_RUN -- but inverts its central assumption. copyparty and ergo come back by themselves after a recreate. OpenBao comes back SEALED: with the default Shamir seal a restart needs three unseal keys typed in by a human. A scheduled `latest` update would therefore take the vault offline at 03:00 and leave it there. So the scheduled path defaults to UPDATE_POLICY= notify and never changes the running version; `install` schedules a daily CHECK and says so. UPDATE_POLICY=auto opts in, and is STILL refused unless an uncommented `seal` stanza is present in config.hcl -- only auto-unseal makes an unattended update defensible. `update` preflights before touching anything, because every one of these fails worse halfway through than up front: - the container must be running; - the vault must be UNSEALED, since a sealed vault cannot produce a snapshot and there would be no rollback plan; - BAO_TOKEN must be present, because the snapshot is token-gated on sys/storage/raft/snapshot; - the target must not cross into 2.7.x while a built-in seal "pkcs11" stanza is active. That stanza is REMOVED in 2.7.0, not deprecated, so the vault would start with no way to unseal at all. The snapshot is the rollback plan, not a formality: OpenBao's upgrade guide states that reverting the image alone does not roll back the data store. It is streamed out with `exec -T ... cat` rather than `compose cp`, which emits a TAR wrapper that will not restore; written 0600 to /var/backups/openbao; and checked for being a non-empty valid gzip archive, with a failure treated as fatal. SKIP_SNAPSHOT=1 exists and warns exactly what it costs. A failed pull or start rolls the OPENBAO_TAG pin back and restarts the previous version. BAO_TOKEN is deliberately never written to the conf file: a long-lived root token sitting next to the vault it opens defeats the vault. Verified: the 2.7-with-active-pkcs11 refusal and its three negative cases (2.6.2 active, 3.x active, 2.7 commented); auto-unseal detection distinguishing a commented stanza from a live one; set_env/env_get; the release-tag parse with and without a leading v; and all four `run` policy branches, including that auto refuses without auto-unseal and that an unknown policy dies. Not verified without a live host: the snapshot, pull and recreate themselves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f8c839a163 |
feat(openbao): pin 2.6.2, and correct the mlock and recovery-path claims
2.5.5 was two releases behind. Verified against the openbao/openbao source at
tag v2.6.2 that the bump is safe for an existing vault: raft's on-disk format
is unchanged (identical bbolt / hashicorp-raft / raft-boltdb pins,
byte-identical physical/raft/types.proto), snapshots restore in both
directions, and every stanza this config.hcl uses still parses -- the config
parser only moved import paths and the listener diff is additive.
Three corrections that came out of that check.
deploy.sh's printed crash-loop recovery command was already broken by 2.6.0.
That release adds `USER openbao` to the alpine image, so the container no
longer starts as root; the `docker run ... --entrypoint sh ... chown` it tells
the operator to run now executes as uid 100 and fails. Added `--user 0:0`, and
dropped the stale 2.5.5 literal from the same string. The detection logic
itself is unaffected -- `id -u openbao` still reads /etc/passwd and still
returns 100, verified by extracting the passwd layer from both published
images -- so
|
||
|
|
185f404549 |
fix(openbao): write .env through set_env, not an interpolated sed
|
||
|
|
dc9761a668 |
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 ( |
||
|
|
a02524ae6e |
fix(openbao): detect the account the entrypoint drops to, not the probe's
The image's entrypoint starts as root and then runs `su-exec openbao "$@"` before exec'ing the server. deploy.sh probed with `--entrypoint id -u`, which BYPASSES the entrypoint, so it reported UID 0 and chowned config.hcl, tls/ and the raft volume to root -- while the server ran as uid 100 and could write none of it: error initializing storage of type raft: failed to create fsm: failed to open bolt file: open /openbao/data/vault.db: permission denied The container crash-looped on that, and tls.key (0600 root:root) would have failed the listener straight afterwards. Confirmed on the affected host: the probe reports 0, `id -u openbao` reports 100, and the entrypoint's line 92 is `set -- su-exec openbao "$@"`. Ask for the account the entrypoint switches to, falling back to the old probe when the image has no such account (then the server really does run as whatever the entrypoint started as). Preferring the service account is also the safe direction to be wrong in: root ignores file permissions, so chowning to the unprivileged uid still works if the server turns out to run as root, whereas the reverse is fatal. Gate the raft chown on the volume's ACTUAL ownership rather than on a first-run flag. The flag was false forever after the first deploy, so a volume left root-owned by an earlier run -- exactly the state this bug created -- could never be repaired by re-running; the operator had to chown it by hand. Reading the owner costs one container start and still keeps the recursive chown off a healthy live raft dir. FIRST_RUN is now unused, so it is gone. Also from the same run, two reporting failures: - `docker compose ps` printed "Restarting (1)" and the script went on to print an unqualified DEPLOYED banner. It now inspects the container state and, when it is not `running`, says plainly that this is a crash loop rather than the expected sealed-and-unhealthy state -- before and after the banner, with the logs command to run. - Every `docker compose` command in that banner assumed the project directory. deploy.sh runs them from $STACK_DIR itself, so the omission only bit the operator afterwards, with "no configuration file provided: not found". The banner now says to cd there first and quotes that error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bf52426299 |
fix(openbao): keep a failing address probe from aborting the deploy
host_addrs() ends in a pipeline, so under `set -o pipefail` a probe that
exits non-zero -- even after printing perfectly usable addresses, or
because awk is missing -- made `addrs="$(host_addrs)"` non-zero, and
`set -e` killed deploy.sh at that line.
Nothing was printed when it did: the 2>/dev/null had already swallowed the
tool's own error and the fail-open guard on the next lines was never
reached, so the operator got a bare exit 1 mid-deploy with nothing to
diagnose. Worst case it aborted a deploy whose bind address was CORRECT --
reproduced with an `ip` stub that prints the matching address, then exits 1.
Capture with `|| true` so the emptiness test actually drives the fail-open
the comment beside it already promised. Neutralising inside host_addrs
instead would not cover a missing awk, since pipefail takes the rightmost
non-zero status.
Re-verified the check is not weakened: a healthy probe with the address
genuinely absent still lists the host's addresses and dies with the full
message, and all seven .env/environment precedence cases are unchanged.
Found by adversarial review of
|
||
|
|
920edc50b3 |
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> |
||
|
|
b027de2182 |
fix(openbao): follow-ups from adversarial fix-verification
An adversarial re-verification of
|
||
|
|
0812f345a8 |
fix(openbao): sanity-audit fixes — 4 blockers + hardening
A multi-agent sanity audit of the freshly-merged deployment found four end-to-end blockers (and several smaller issues); all fixed here. HIGH (were blocking): - deploy.sh never called install_docker(), so `docker compose pull` hit command-not-found on any host without Docker. Now called before the compose steps. - The container's server process runs as the image's own (often non-root) user but the mounted config/TLS were root-owned 0640/0600 and the raft volume root-owned -> vault crash-looped, never binding :8200. deploy.sh now detects the image UID after pull and aligns ownership of config.hcl, ./tls and the data volume (a no-op when the image runs as root); config.hcl is installed 0644 (holds no secrets). - Docs told operators to set the daemon key `openbao_ca_cert`, but the Kanrisha daemon's key is `ca_cert` (config.go, mapstructure:"ca_cert"). The wrong key is fatal on strict unmarshal / leaves TLS unverified. Renamed in all 5 places (config.hcl, gen-tls.sh, deploy.sh x2, README). - DR backup used `docker compose cp openbao:… -`, which emits a TAR stream, so the age-encrypted snapshot was tar-wrapped and would not restore. Switched to `docker compose exec -T openbao cat` for the raw bytes, wrote the snapshot to a scratch path (not the live raft dir), and documented the matching restore. MEDIUM: - Swap detection used `swapon --show` (absent on BusyBox) and `\s` (GNU-only) -> silently no-op on Alpine, leaving swap on. Now uses /proc/swaps and [[:space:]] so mlock hardening actually holds on musl. - A Docker-published port bypasses the host INPUT firewall, so the source rule was illusory. deploy.sh now narrows OPENBAO_BIND to OPENBAO_ADDR when it is an IP, the compose/README/.env comments state the reality, and a new Exposure section + an init-immediately warning were added. - Fixed broken ../kanrisha/ and deployments/kanrisha/ links (separate repo). LOW: - OPENBAO_TLS_SANS is now honored (folded into the SAN list from the env). - .gitignore excludes *.snap / *.snap.age. - Bootstrap note clarifies bootstrap.sh needs the `bao` CLI (run it from the Kanrisha host/workstation, not this Docker-only vault host). - README multi-OS count corrected (eight stacks) + automations.sh header lists openbao. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2efc9dbffb |
feat(openbao): hardened same-LAN tape-encryption key store for Kanrisha
A dedicated OpenBao deployment, kept OFF the Kanrisha tape host so a compromise of the tape node can't reach the vault. - Native TLS on the listener (self-signed by default via gen-tls.sh, or a CA-signed cert from a Smallstep CA over ACME) — no Caddy/Let's Encrypt; reached over the LAN, not the public internet. - Integrated raft storage (clean snapshot-based DR). - mlock on (cap_add IPC_LOCK + memlock unlimited + host swapoff in deploy.sh). - Manual unseal by default; optional PKCS#11 HSM auto-unseal. - deploy.sh: Docker install (Alpine/Debian/Alma), self-signed cert, .env seed, swapoff, firewall 8200/tcp, compose up; then prints init/unseal + the KV-v2 + AppRole bootstrap for Kanrisha + the raft-snapshot DR flow. Self-contained (config payload embedded by build.sh). - Registered in automations.sh + the README deployment table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |