From a1fc77263290846d75678b728c29b1b60a03070d Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Tue, 4 Aug 2026 08:35:13 +0000 Subject: [PATCH 1/2] fix: use a valid runner hook filename GitHub Runner requires job-started hooks to use a supported script extension. Rename the constrained Phase 2 hook to .sh and preserve the enforcement test. Co-Authored-By: Claude Signed-off-by: Joseph Yaksich --- docs/dress-rehearsal.md | 3 ++- .../{runner-job-started => runner-job-started.sh} | 0 ops/dress-rehearsal/runner.service.override.conf | 2 +- test/phase2-candidate.mjs | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) rename ops/dress-rehearsal/{runner-job-started => runner-job-started.sh} (100%) diff --git a/docs/dress-rehearsal.md b/docs/dress-rehearsal.md index ba0049a..9710ace 100644 --- a/docs/dress-rehearsal.md +++ b/docs/dress-rehearsal.md @@ -41,7 +41,8 @@ candidate as running after the failed attempt. The runner service account has no Proxmox, Stable, production, website, or release credentials and is not a member of privileged container/runtime groups. -Its start hook rejects every repository, workflow, job, and event except the +Its root-owned `runner-job-started.sh` start hook rejects every repository, +workflow, job, and event except the Phase 2 deployment job resulting from successful `CI` for `main`. Ordinary PR workflows do not carry the unique runner label. The runner is registered with `--no-default-labels`, so generic `self-hosted`, OS, or architecture selectors diff --git a/ops/dress-rehearsal/runner-job-started b/ops/dress-rehearsal/runner-job-started.sh similarity index 100% rename from ops/dress-rehearsal/runner-job-started rename to ops/dress-rehearsal/runner-job-started.sh diff --git a/ops/dress-rehearsal/runner.service.override.conf b/ops/dress-rehearsal/runner.service.override.conf index 2f71b2d..adf52c7 100644 --- a/ops/dress-rehearsal/runner.service.override.conf +++ b/ops/dress-rehearsal/runner.service.override.conf @@ -1,5 +1,5 @@ [Service] -Environment=ACTIONS_RUNNER_HOOK_JOB_STARTED=/usr/local/lib/1helm-candidate/runner-job-started +Environment=ACTIONS_RUNNER_HOOK_JOB_STARTED=/usr/local/lib/1helm-candidate/runner-job-started.sh NoNewPrivileges=false ProtectSystem=strict ProtectHome=read-only diff --git a/test/phase2-candidate.mjs b/test/phase2-candidate.mjs index 6672d13..d939567 100644 --- a/test/phase2-candidate.mjs +++ b/test/phase2-candidate.mjs @@ -93,7 +93,7 @@ test("rollback fixtures remain local-only and cannot satisfy normal candidate va test("candidate workflow and guest boundary exclude PR code and broad root access", () => { const workflow = read(".github/workflows/candidate.yml"); const helper = read("ops/dress-rehearsal/1helm-candidate-install"); - const hook = read("ops/dress-rehearsal/runner-job-started"); + const hook = read("ops/dress-rehearsal/runner-job-started.sh"); const sudoersExample = "%actions ALL=(root) NOPASSWD: /usr/local/sbin/1helm-candidate-install \"\"\n"; assert.match(workflow, /workflow_run:[\s\S]*workflows: \[CI\][\s\S]*branches: \[main\]/); assert.match(workflow, /workflow_run\.event == 'push'/); @@ -113,6 +113,7 @@ test("candidate workflow and guest boundary exclude PR code and broad root acces assert.match(helper, /actions\\\.runner[\s\S]*systemd-run[\s\S]*\/usr\/local\/sbin\/1helm-candidate-install/); assert.match(helper, /unlink "\$INBOX\/candidate\.json" "\$INBOX\/candidate\.tgz"/); assert.match(read("ops/dress-rehearsal/runner.service.override.conf"), /ProtectSystem=strict[\s\S]*ReadWritePaths=.*candidate\/inbox/); + assert.match(read("ops/dress-rehearsal/runner.service.override.conf"), /ACTIONS_RUNNER_HOOK_JOB_STARTED=\/usr\/local\/lib\/1helm-candidate\/runner-job-started\.sh/); assert.match(hook, /GITHUB_EVENT_NAME.*workflow_run/); assert.match(hook, /run\.get\("event"\) == "push"/); assert.doesNotMatch(sudoersExample, /NOPASSWD:\s*ALL/); From 2a59f059b0311509e1b7c80d65be5bb1c9ea4309 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Tue, 4 Aug 2026 08:38:50 +0000 Subject: [PATCH 2/2] fix: consume candidate inbox files safely Use one unlink invocation per fixed candidate payload and keep a regression assertion for the root boundary. Co-Authored-By: Claude Signed-off-by: Joseph Yaksich --- ops/dress-rehearsal/1helm-candidate-install | 3 ++- test/phase2-candidate.mjs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ops/dress-rehearsal/1helm-candidate-install b/ops/dress-rehearsal/1helm-candidate-install index f05b531..ffcdba7 100755 --- a/ops/dress-rehearsal/1helm-candidate-install +++ b/ops/dress-rehearsal/1helm-candidate-install @@ -43,7 +43,8 @@ if [[ "$LOCAL_PROOF" -eq 0 ]]; then fi # Consume the fixed inbox payloads after the root copy. The unprivileged runner # can then create a fresh set without owning retained candidate bytes. -unlink "$INBOX/candidate.json" "$INBOX/candidate.tgz" +unlink "$INBOX/candidate.json" +unlink "$INBOX/candidate.tgz" [[ "$LOCAL_PROOF" -eq 1 ]] || unlink "$INBOX/provenance.bundle.json" validate_args=(validate "$work/candidate.json" "$work/candidate.tgz" "$work/verified.json") diff --git a/test/phase2-candidate.mjs b/test/phase2-candidate.mjs index d939567..47f8581 100644 --- a/test/phase2-candidate.mjs +++ b/test/phase2-candidate.mjs @@ -111,7 +111,9 @@ test("candidate workflow and guest boundary exclude PR code and broad root acces assert.doesNotMatch(helper, /--local-proof/); assert.match(helper, /awk -F\/.*!found.*found=1/, "large archive inspection consumes tar output instead of causing SIGPIPE under pipefail"); assert.match(helper, /actions\\\.runner[\s\S]*systemd-run[\s\S]*\/usr\/local\/sbin\/1helm-candidate-install/); - assert.match(helper, /unlink "\$INBOX\/candidate\.json" "\$INBOX\/candidate\.tgz"/); + assert.match(helper, /^unlink "\$INBOX\/candidate\.json"$/m); + assert.match(helper, /^unlink "\$INBOX\/candidate\.tgz"$/m); + assert.doesNotMatch(helper, /unlink "\$INBOX\/candidate\.json" "\$INBOX\/candidate\.tgz"/); assert.match(read("ops/dress-rehearsal/runner.service.override.conf"), /ProtectSystem=strict[\s\S]*ReadWritePaths=.*candidate\/inbox/); assert.match(read("ops/dress-rehearsal/runner.service.override.conf"), /ACTIONS_RUNNER_HOOK_JOB_STARTED=\/usr\/local\/lib\/1helm-candidate\/runner-job-started\.sh/); assert.match(hook, /GITHUB_EVENT_NAME.*workflow_run/);