From 5e83ecc2ef6cd6208253f703d1c5c1f6366bf56b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 22:00:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20[nomad-step-2]=20S2-fix-F=20?= =?UTF-8?q?=E2=80=94=20wire=20tools/vault-seed-.sh=20into=20bin/disin?= =?UTF-8?q?to=20--with=20=20(#928)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tools/vault-seed-forgejo.sh` existed and worked, but `bin/disinto init --backend=nomad --with forgejo` never invoked it, so a fresh LXC with an empty Vault hit `Template Missing: vault.read(kv/data/disinto/shared/ forgejo)` and the forgejo alloc timed out inside deploy.sh's 240s healthy_deadline — operator had to run the seeder + `nomad alloc restart` by hand to recover. In `_disinto_init_nomad`, after `vault-import.sh` (or its skip branch) and before `deploy.sh`, iterate `--with ` and auto-invoke `tools/vault-seed-.sh` when the file exists + is executable. Services without a seeder are silently skipped — Step 3+ services (woodpecker, chat, etc.) can ship their own seeder without touching `bin/disinto`. VAULT_ADDR is passed explicitly because cluster-up.sh writes the profile.d export during this same init run (current shell hasn't sourced it yet) and `vault-seed-forgejo.sh` — unlike its sibling vault-* scripts — requires the caller to set VAULT_ADDR instead of defaulting it via `_hvault_default_env`. Mirror the loop in the --dry-run plan so the operator-visible plan matches the real run. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 59 ++++++++++++++++++++++++++++++++++- tests/disinto-init-nomad.bats | 22 +++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/bin/disinto b/bin/disinto index f9bfe04..0a78db6 100755 --- a/bin/disinto +++ b/bin/disinto @@ -783,9 +783,29 @@ _disinto_init_nomad() { fi if [ -n "$with_services" ]; then + # Vault seed plan (S2.6, #928): one line per service whose + # tools/vault-seed-.sh ships. Services without a seeder are + # silently skipped — the real-run loop below mirrors this, + # making `--with woodpecker` in Step 3 auto-invoke + # tools/vault-seed-woodpecker.sh once that file lands without + # any further change to bin/disinto. + local seed_hdr_printed=false + local IFS=',' + for svc in $with_services; do + svc=$(echo "$svc" | xargs) # trim whitespace + local seed_script="${FACTORY_ROOT}/tools/vault-seed-${svc}.sh" + if [ -x "$seed_script" ]; then + if [ "$seed_hdr_printed" = false ]; then + echo "── Vault seed dry-run ─────────────────────────────────" + seed_hdr_printed=true + fi + echo "[seed] [dry-run] ${seed_script} --dry-run" + fi + done + [ "$seed_hdr_printed" = true ] && echo "" + echo "── Deploy services dry-run ────────────────────────────" echo "[deploy] services to deploy: ${with_services}" - local IFS=',' for svc in $with_services; do svc=$(echo "$svc" | xargs) # trim whitespace # Validate known services first @@ -893,6 +913,43 @@ _disinto_init_nomad() { echo "[import] no --import-env/--import-sops — skipping; set them or seed kv/disinto/* manually before deploying secret-dependent services" fi + # Seed Vault for services that ship their own seeder (S2.6, #928). + # Convention: tools/vault-seed-.sh — auto-invoked when --with + # is requested. Runs AFTER vault-import so that real imported values + # win over generated seeds when both are present; each seeder is + # idempotent on a per-key basis (see vault-seed-forgejo.sh's + # "missing → generate, present → unchanged" contract), so re-running + # init does not rotate existing keys. Services without a seeder are + # silently skipped — keeps this loop forward-compatible with Step 3+ + # services that may ship their own seeder without touching bin/disinto. + # + # VAULT_ADDR is passed explicitly because cluster-up.sh writes the + # profile.d export *during* this same init run, so the current shell + # hasn't sourced it yet; sibling vault-* scripts (engines/policies/ + # auth/import) default VAULT_ADDR internally via _hvault_default_env, + # but vault-seed-forgejo.sh requires the caller to set it. + if [ -n "$with_services" ]; then + local vault_addr="${VAULT_ADDR:-http://127.0.0.1:8200}" + local IFS=',' + for svc in $with_services; do + svc=$(echo "$svc" | xargs) # trim whitespace + local seed_script="${FACTORY_ROOT}/tools/vault-seed-${svc}.sh" + if [ -x "$seed_script" ]; then + echo "" + echo "── Seeding Vault for ${svc} ───────────────────────────" + if [ "$(id -u)" -eq 0 ]; then + VAULT_ADDR="$vault_addr" "$seed_script" || exit $? + else + if ! command -v sudo >/dev/null 2>&1; then + echo "Error: vault-seed-${svc}.sh must run as root and sudo is not installed" >&2 + exit 1 + fi + sudo -n "VAULT_ADDR=$vault_addr" -- "$seed_script" || exit $? + fi + fi + done + fi + # Deploy services if requested if [ -n "$with_services" ]; then echo "" diff --git a/tests/disinto-init-nomad.bats b/tests/disinto-init-nomad.bats index f38805e..8467ebb 100644 --- a/tests/disinto-init-nomad.bats +++ b/tests/disinto-init-nomad.bats @@ -155,6 +155,28 @@ setup_file() { [[ "$output" == *"[deploy] dry-run complete"* ]] } +# S2.6 / #928 — every --with that ships tools/vault-seed-.sh +# must auto-invoke the seeder before deploy.sh runs. forgejo is the +# only service with a seeder today, so the dry-run plan must include +# its seed line when --with forgejo is set. The seed block must also +# appear BEFORE the deploy block (seeded secrets must exist before +# nomad reads the template stanza) — pinned here by scanning output +# order. Services without a seeder (e.g. unknown hypothetical future +# ones) are silently skipped by the loop convention. +@test "disinto init --backend=nomad --with forgejo --dry-run prints seed plan before deploy plan" { + run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with forgejo --dry-run + [ "$status" -eq 0 ] + [[ "$output" == *"Vault seed dry-run"* ]] + [[ "$output" == *"tools/vault-seed-forgejo.sh --dry-run"* ]] + # Order: seed header must appear before deploy header. + local seed_line deploy_line + seed_line=$(echo "$output" | grep -n "Vault seed dry-run" | head -1 | cut -d: -f1) + deploy_line=$(echo "$output" | grep -n "Deploy services dry-run" | head -1 | cut -d: -f1) + [ -n "$seed_line" ] + [ -n "$deploy_line" ] + [ "$seed_line" -lt "$deploy_line" ] +} + @test "disinto init --backend=nomad --with forgejo,forgejo --dry-run handles comma-separated services" { run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with forgejo,forgejo --dry-run [ "$status" -eq 0 ] From f21408028006182a9c66d4df6b251c02c3d5a308 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 22:14:05 +0000 Subject: [PATCH 2/2] fix: [review-r1] seed loop sudo invocation bypasses sudoers env_reset (#929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sudo -n "VAULT_ADDR=$vault_addr" -- "$seed_script"` passed VAULT_ADDR as a sudoers env-assignment argument. With the default `env_reset=on` policy (almost all distros), sudo silently discards env assignments unless the variable is in `env_keep` — and VAULT_ADDR is not. The seeder then hit its own precondition check at vault-seed-forgejo.sh:109 and died with "VAULT_ADDR unset", breaking the fresh-LXC non-root acceptance path the PR was written to close. Fix: run `env` as the command under sudo — `sudo -n -- env "VAULT_ADDR=$vault_addr" "$seed_script"` — so VAULT_ADDR is set in the child process directly, unaffected by sudoers env handling. The root (non-sudo) branch already used shell-level env assignment and was correct. Adds a grep-level regression guard that pins the `env VAR=val` invocation and negative-asserts the unsafe bare-argument form. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/disinto | 9 ++++++++- tests/disinto-init-nomad.bats | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/bin/disinto b/bin/disinto index 0a78db6..5f57927 100755 --- a/bin/disinto +++ b/bin/disinto @@ -928,6 +928,13 @@ _disinto_init_nomad() { # hasn't sourced it yet; sibling vault-* scripts (engines/policies/ # auth/import) default VAULT_ADDR internally via _hvault_default_env, # but vault-seed-forgejo.sh requires the caller to set it. + # + # The non-root branch invokes the seeder as `sudo -n -- env VAR=val + # script` rather than `sudo -n VAR=val -- script`: sudo treats bare + # `VAR=val` args as sudoers env-assignments, which the default + # `env_reset=on` policy silently discards unless the variable is in + # `env_keep` (VAULT_ADDR is not). Using `env` as the actual command + # sets VAULT_ADDR in the child process regardless of sudoers policy. if [ -n "$with_services" ]; then local vault_addr="${VAULT_ADDR:-http://127.0.0.1:8200}" local IFS=',' @@ -944,7 +951,7 @@ _disinto_init_nomad() { echo "Error: vault-seed-${svc}.sh must run as root and sudo is not installed" >&2 exit 1 fi - sudo -n "VAULT_ADDR=$vault_addr" -- "$seed_script" || exit $? + sudo -n -- env "VAULT_ADDR=$vault_addr" "$seed_script" || exit $? fi fi done diff --git a/tests/disinto-init-nomad.bats b/tests/disinto-init-nomad.bats index 8467ebb..21f4303 100644 --- a/tests/disinto-init-nomad.bats +++ b/tests/disinto-init-nomad.bats @@ -177,6 +177,22 @@ setup_file() { [ "$seed_line" -lt "$deploy_line" ] } +# Regression guard (PR #929 review): `sudo -n VAR=val -- cmd` is subject +# to sudoers env_reset policy and silently drops VAULT_ADDR unless it's +# in env_keep (it isn't in default configs). vault-seed-forgejo.sh +# requires VAULT_ADDR and dies at its own precondition check if unset, +# so the non-root branch MUST invoke `sudo -n -- env VAR=val cmd` so +# that `env` sets the variable in the child process regardless of +# sudoers policy. This grep-level guard catches a revert to the unsafe +# form that silently broke non-root seed runs on a fresh LXC. +@test "seed loop invokes sudo via 'env VAR=val' (bypasses sudoers env_reset)" { + run grep -F 'sudo -n -- env "VAULT_ADDR=' "$DISINTO_BIN" + [ "$status" -eq 0 ] + # Negative: no bare `sudo -n "VAR=val" --` form anywhere in the file. + run grep -F 'sudo -n "VAULT_ADDR=' "$DISINTO_BIN" + [ "$status" -ne 0 ] +} + @test "disinto init --backend=nomad --with forgejo,forgejo --dry-run handles comma-separated services" { run "$DISINTO_BIN" init placeholder/repo --backend=nomad --with forgejo,forgejo --dry-run [ "$status" -eq 0 ]