Skip to content

Commit f8ffa2d

Browse files
authored
fix: second audit — reliable rebuild exit detection, harmonia wait, closure+minisig verification (#174)
1 parent c54d347 commit f8ffa2d

1 file changed

Lines changed: 54 additions & 20 deletions

File tree

scripts/enroll.sh

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,14 @@ info "Building... (5-15 min on first run, downloads packages)"
163163
PASS1_LOG="/tmp/sourceos-enroll-pass1-$(date +%s).log"
164164
nixos-rebuild switch --flake "${REPO_ROOT}#${HOST}" --impure 2>&1 | \
165165
tee "${PASS1_LOG}" | \
166-
grep -E '(building|fetching|error|warning|Error|FAILED|activating)' || true
167-
# Check exit code of nixos-rebuild (not the grep)
168-
if ! grep -q 'activating' "${PASS1_LOG}" 2>/dev/null; then
169-
# activating line may be absent on cached builds; check for explicit failure
170-
if grep -qiE '(error:|FAILED|failed to)' "${PASS1_LOG}" 2>/dev/null; then
171-
die "Pass 1 rebuild failed. Full log: ${PASS1_LOG}
166+
grep --line-buffered -E '(building|fetching|error|warning|Error|FAILED|activating)' || true
167+
# Capture nixos-rebuild exit code from PIPESTATUS[0] before any other command resets it.
168+
# The `|| true` on the grep does NOT reset PIPESTATUS — it only applies to the grep's own
169+
# exit, not to the pipeline. This is the reliable way to detect nixos-rebuild failure
170+
# without relying on fragile log-pattern matching.
171+
NB1_EXIT=${PIPESTATUS[0]}
172+
[[ $NB1_EXIT -eq 0 ]] || die "Pass 1 rebuild failed (exit ${NB1_EXIT}). Full log: ${PASS1_LOG}
172173
Check: journalctl -xe | tail -50"
173-
fi
174-
fi
175174
ok "Pass 1 complete ($(elapsed)) — log: ${PASS1_LOG}"
176175

177176
# ── Step 3: Age key ───────────────────────────────────────────────────────────
@@ -373,7 +372,12 @@ step 10 "Build NixOS closure + push to harmonia cache"
373372

374373
info "Building builder-aarch64 system closure..."
375374
CLOSURE=$(nix build "${REPO_ROOT}#nixosConfigurations.${HOST}.config.system.build.toplevel" \
376-
--no-link --print-out-paths 2>&1 | tail -1)
375+
--no-link --print-out-paths 2>/dev/null)
376+
# nix build returns empty stdout on failure (errors go to stderr). Verify both that
377+
# CLOSURE is non-empty and that the path actually exists in the Nix store.
378+
[[ -n "${CLOSURE}" && -e "${CLOSURE}" ]] || \
379+
die "nix build failed — retry with:
380+
nix build ${REPO_ROOT}#nixosConfigurations.${HOST}.config.system.build.toplevel --no-link --show-trace"
377381
ok "Built: ${CLOSURE}"
378382

379383
# Harmonia must be running before we can push (it starts after pass-2 rebuild).
@@ -388,23 +392,41 @@ info "Activating final configuration..."
388392
PASS2_LOG="/tmp/sourceos-enroll-pass2-$(date +%s).log"
389393
nixos-rebuild switch --flake "${REPO_ROOT}#${HOST}" --impure 2>&1 | \
390394
tee "${PASS2_LOG}" | \
391-
grep -E '(building|fetching|error|warning|Error|FAILED|activating)' || true
392-
if grep -qiE '(error:|FAILED|failed to)' "${PASS2_LOG}" 2>/dev/null; then
393-
die "Pass 2 rebuild failed. Full log: ${PASS2_LOG}
395+
grep --line-buffered -E '(building|fetching|error|warning|Error|FAILED|activating)' || true
396+
NB2_EXIT=${PIPESTATUS[0]}
397+
[[ $NB2_EXIT -eq 0 ]] || die "Pass 2 rebuild failed (exit ${NB2_EXIT}). Full log: ${PASS2_LOG}
394398
Check: journalctl -xe | tail -50
395-
The previous generation (pass 1) is still bootable."
396-
fi
399+
The previous generation (pass 1) is still bootable via the systemd-boot menu."
397400
ok "Pass 2 complete ($(elapsed)) — log: ${PASS2_LOG}"
398401

399-
# Now push the closure to the running harmonia cache
402+
# Confirm the activated generation matches the closure we built in step 10.
403+
CURRENT_GEN=$(readlink /run/current-system 2>/dev/null || echo "")
404+
if [[ -n "${CURRENT_GEN}" && "${CURRENT_GEN}" == "${CLOSURE}" ]]; then
405+
ok "Active generation matches built closure"
406+
elif [[ -n "${CURRENT_GEN}" ]]; then
407+
info "Active generation: ${CURRENT_GEN}"
408+
info "Built closure: ${CLOSURE}"
409+
warn "Active generation differs from step-10 closure — check: nixos-rebuild list-generations"
410+
fi
411+
412+
# Now push the closure to the running harmonia cache.
413+
# Wait up to 60s for harmonia (it starts during pass-2 activation); this is
414+
# warn-only — a push failure does not block enrollment. sourceos-syncd will
415+
# repopulate the cache on its first sync cycle.
400416
info "Pushing NixOS closure to local harmonia cache (http://127.0.0.1:8101)..."
401-
sleep 3 # brief pause for harmonia to finish starting
402-
if curl -fsSk http://127.0.0.1:8101/nix-cache-info &>/dev/null; then
417+
_HARM_UP=0
418+
for _i in $(seq 1 12); do
419+
if curl -fsSk --max-time 5 "http://127.0.0.1:8101/nix-cache-info" &>/dev/null; then
420+
_HARM_UP=1; break
421+
fi
422+
sleep 5
423+
done
424+
if [[ $_HARM_UP -eq 1 ]]; then
403425
nix copy --to "http://127.0.0.1:8101" "${CLOSURE}" && \
404426
ok "Closure pushed to harmonia cache" || \
405427
warn "nix copy failed — run manually: nix copy --to 'http://127.0.0.1:8101' ${CLOSURE}"
406428
else
407-
warn "harmonia not yet responding on :8101 — push manually after it starts"
429+
warn "harmonia not responding on :8101 after 60s — push manually after it starts"
408430
warn " nix copy --to 'http://127.0.0.1:8101' ${CLOSURE}"
409431
fi
410432

@@ -458,11 +480,23 @@ if systemctl is-active --quiet nginx 2>/dev/null; then
458480
if curl -fsSk http://127.0.0.1:8101/nix-cache-info &>/dev/null; then
459481
ok "Nix cache at http://127.0.0.1:8101 responding"
460482
fi
461-
if curl -fsSk http://127.0.0.1:8101/nix-cache-info.minisig &>/dev/null; then
462-
ok "minisig endpoint serving correctly"
483+
# Cryptographically verify the minisig, not just that the endpoint responds.
484+
# A stale or wrong-key sig would pass an existence check but fail here.
485+
_SIG_TMP=$(mktemp /tmp/nix-cache-info-XXXXX.minisig)
486+
if curl -fsSk http://127.0.0.1:8101/nix-cache-info.minisig -o "${_SIG_TMP}" 2>/dev/null && \
487+
[[ -s "${_SIG_TMP}" ]]; then
488+
if minisign -V -p "${MINISIGN_PUB}" -m "${MINISIGN_CACHE_INFO}" -x "${_SIG_TMP}" &>/dev/null; then
489+
ok "nix-cache-info minisig: signature valid"
490+
else
491+
warn "minisig served but signature is INVALID — re-sign:"
492+
warn " minisign -S -s ${MINISIGN_SEC} -m ${MINISIGN_CACHE_INFO} -x ${MINISIGN_CACHE_INFO_SIG}"
493+
HEALTHY=0
494+
fi
463495
else
464496
warn "minisig not yet at /nix-cache-info.minisig — check nginx + ${MINISIGN_CACHE_INFO_SIG}"
497+
HEALTHY=0
465498
fi
499+
rm -f "${_SIG_TMP}"
466500
fi
467501

468502
if [[ $HEALTHY -eq 1 ]]; then

0 commit comments

Comments
 (0)