From a4c4b7a40cc3d0521299da75948e9618a67a3fdb Mon Sep 17 00:00:00 2001 From: John McChesney TenEyck Jr <59268465+jmcte@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:49:22 +0100 Subject: [PATCH 1/3] Scrub GitHub credentials before runner execution --- .github/workflows/pr-fast-ci.yml | 4 +-- docker/runner-entrypoint.ps1 | 28 +++++++++++++++-- docker/runner-entrypoint.sh | 15 ++++++--- scripts/ci/run-fast-checks.sh | 5 ++- scripts/guest/macos-runner-bootstrap.sh | 1 + scripts/smoke/actions-runner/run.sh | 6 ++++ test/smoke-harness.test.ts | 41 ++++++++++++++++++++++++- test/workflow.test.ts | 2 +- 8 files changed, 91 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-fast-ci.yml b/.github/workflows/pr-fast-ci.yml index 912950e..0ef1659 100644 --- a/.github/workflows/pr-fast-ci.yml +++ b/.github/workflows/pr-fast-ci.yml @@ -78,9 +78,9 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v6 + - uses: ./actions/setup-shell-safe-node with: - node-version: "24" + node-version: 24.14.1 - uses: pnpm/action-setup@v6 with: diff --git a/docker/runner-entrypoint.ps1 b/docker/runner-entrypoint.ps1 index 9a815ed..f2f7e20 100644 --- a/docker/runner-entrypoint.ps1 +++ b/docker/runner-entrypoint.ps1 @@ -74,6 +74,31 @@ function Request-RunnerToken { return $response.token } + +function Invoke-ActionsRunner { + $credentialNames = @( + "GITHUB_PAT", + "GITHUB_APP_ID", + "GITHUB_APP_INSTALLATION_ID", + "GITHUB_APP_PRIVATE_KEY" + ) + $savedCredentials = @{} + + foreach ($name in $credentialNames) { + $savedCredentials[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + [Environment]::SetEnvironmentVariable($name, $null, "Process") + } + + try { + & .\run.cmd 2>&1 | Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append + return $LASTEXITCODE + } finally { + foreach ($name in $credentialNames) { + [Environment]::SetEnvironmentVariable($name, $savedCredentials[$name], "Process") + } + } +} + function Clear-RunnerState { Remove-Item -Force -ErrorAction SilentlyContinue ` (Join-Path $env:RUNNER_HOME ".runner"), ` @@ -178,8 +203,7 @@ try { & .\config.cmd @configArgs $script:RunnerConfigured = $true Write-RunnerLog "starting runner $env:RUNNER_NAME" - & .\run.cmd 2>&1 | Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append - exit $LASTEXITCODE + exit (Invoke-ActionsRunner) } finally { Pop-Location } diff --git a/docker/runner-entrypoint.sh b/docker/runner-entrypoint.sh index c83050e..2464797 100755 --- a/docker/runner-entrypoint.sh +++ b/docker/runner-entrypoint.sh @@ -16,11 +16,19 @@ run_runner_bash() { shift || true if [[ "${runner_exec_mode}" == "root" ]]; then - env RUNNER_ALLOW_RUNASROOT=1 RUNNER_EXECUTION_MODE="${runner_exec_mode}" "$@" bash -lc "${command}" + env "$@" RUNNER_ALLOW_RUNASROOT=1 RUNNER_EXECUTION_MODE="${runner_exec_mode}" bash -lc "${command}" return fi - env RUNNER_EXECUTION_MODE="${runner_exec_mode}" "$@" gosu runner bash -lc "${command}" + env "$@" RUNNER_EXECUTION_MODE="${runner_exec_mode}" gosu runner bash -lc "${command}" +} + +run_actions_runner() { + run_runner_bash "cd '${RUNNER_HOME}' && exec ./run.sh" \ + -u GITHUB_PAT \ + -u GITHUB_APP_ID \ + -u GITHUB_APP_INSTALLATION_ID \ + -u GITHUB_APP_PRIVATE_KEY } cleanup_local_state() { @@ -254,5 +262,4 @@ runner_configured="true" audit_event runner_registered log "starting runner ${RUNNER_NAME}" -run_runner_bash "cd '${RUNNER_HOME}' && exec ./run.sh" \ - 2>&1 | tee -a "${RUNNER_LOG_DIR}/runner.log" +run_actions_runner 2>&1 | tee -a "${RUNNER_LOG_DIR}/runner.log" diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh index 6d9340d..675a96e 100755 --- a/scripts/ci/run-fast-checks.sh +++ b/scripts/ci/run-fast-checks.sh @@ -11,7 +11,10 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$repo_root" -corepack enable +corepack_bin_dir="${COREPACK_BIN_DIR:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/corepack-bin}" +mkdir -p "${corepack_bin_dir}" +corepack enable --install-directory "${corepack_bin_dir}" +export PATH="${corepack_bin_dir}:${PATH}" corepack prepare pnpm@10.32.1 --activate pnpm install --frozen-lockfile diff --git a/scripts/guest/macos-runner-bootstrap.sh b/scripts/guest/macos-runner-bootstrap.sh index b827570..cfa4544 100755 --- a/scripts/guest/macos-runner-bootstrap.sh +++ b/scripts/guest/macos-runner-bootstrap.sh @@ -108,6 +108,7 @@ RUNNER_CONFIGURED="true" log "starting runner ${RUNNER_NAME}" ( + unset GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY cd "${RUNNER_ROOT}" ./run.sh ) diff --git a/scripts/smoke/actions-runner/run.sh b/scripts/smoke/actions-runner/run.sh index e19fc1a..8a02658 100755 --- a/scripts/smoke/actions-runner/run.sh +++ b/scripts/smoke/actions-runner/run.sh @@ -4,6 +4,12 @@ set -euo pipefail printf '%s run.sh stub executed\n' "$(date -Iseconds)" >> "${RUNNER_STATE_DIR}/run.log" printf 'run path: %s\n' "$(pwd)" >> "${RUNNER_STATE_DIR}/run-context.log" printf 'run mode: %s\n' "${RUNNER_EXECUTION_MODE:-unknown}" >> "${RUNNER_STATE_DIR}/run-context.log" +for credential_name in GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY; do + if [[ -n "${!credential_name:-}" ]]; then + printf 'leaked credential: %s\n' "${credential_name}" >> "${RUNNER_STATE_DIR}/run-context.log" + exit 1 + fi +done mkdir -p "${RUNNER_WORK_DIR}/workspace" touch "${RUNNER_WORK_DIR}/workspace/job.txt" echo "job output" diff --git a/test/smoke-harness.test.ts b/test/smoke-harness.test.ts index 9c47bb8..60c3ad4 100644 --- a/test/smoke-harness.test.ts +++ b/test/smoke-harness.test.ts @@ -5,6 +5,12 @@ import path from "node:path"; import { afterEach, describe, expect, test } from "vitest"; const tempRoots: string[] = []; +const runnerCredentialNames = [ + "GITHUB_PAT", + "GITHUB_APP_ID", + "GITHUB_APP_INSTALLATION_ID", + "GITHUB_APP_PRIVATE_KEY", +]; afterEach(() => { for (const tempRoot of tempRoots.splice(0)) { @@ -18,6 +24,14 @@ function makeTempRoot() { return tempRoot; } +function withoutRunnerCredentials(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const scrubbedEnv = { ...env }; + for (const name of runnerCredentialNames) { + delete scrubbedEnv[name]; + } + return scrubbedEnv; +} + // Resolve as soon as the mock API prints its readiness sentinel on // stdout. This is event-driven (no fixed-interval polling), and rejects // immediately if the child fails to spawn or exits early, so the only @@ -124,7 +138,7 @@ describe("runner registration smoke harness", () => { const resolvedRunnerHome = fs.realpathSync(runnerHome); const env = { - ...process.env, + ...withoutRunnerCredentials(process.env), RUNNER_EXECUTION_MODE: "runner", RUNNER_STATE_DIR: tempRoot, RUNNER_WORK_DIR: workDir, @@ -180,6 +194,31 @@ describe("runner registration smoke harness", () => { expect(fs.existsSync(path.join(workDir, "workspace", "job.txt"))).toBe(true); }); + test("scrubs GitHub credentials before launching runner jobs", () => { + const linuxEntrypoint = fs.readFileSync( + path.resolve("docker/runner-entrypoint.sh"), + "utf8" + ); + const windowsEntrypoint = fs.readFileSync( + path.resolve("docker/runner-entrypoint.ps1"), + "utf8" + ); + const macosBootstrap = fs.readFileSync( + path.resolve("scripts/guest/macos-runner-bootstrap.sh"), + "utf8" + ); + + for (const name of runnerCredentialNames) { + expect(linuxEntrypoint).toContain(`-u ${name}`); + expect(windowsEntrypoint).toContain(`\"${name}\"`); + } + expect(linuxEntrypoint).toContain("run_actions_runner 2>&1"); + expect(windowsEntrypoint).toContain("exit (Invoke-ActionsRunner)"); + expect(macosBootstrap).toContain( + "unset GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY" + ); + }); + test("wires the smoke script to run both execution modes and verify cleanup", () => { const script = fs.readFileSync( path.resolve("scripts/smoke-test.sh"), diff --git a/test/workflow.test.ts b/test/workflow.test.ts index a2cd75e..2f32bd1 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -247,7 +247,7 @@ describe("CI workflow", () => { } expect( (workflow.jobs["fast-checks"].steps as Array>).some( - (step) => step.uses === "actions/setup-node@v6" + (step) => step.uses === "./actions/setup-shell-safe-node" ) ).toBe(true); From 89440936e73dbca922422b901a29719864c29983 Mon Sep 17 00:00:00 2001 From: Hephaestus Date: Mon, 22 Jun 2026 23:06:31 +0000 Subject: [PATCH 2/3] Preserve Windows runner exit code --- docker/runner-entrypoint.ps1 | 10 +++++++--- test/smoke-harness.test.ts | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docker/runner-entrypoint.ps1 b/docker/runner-entrypoint.ps1 index f2f7e20..4a85f8c 100644 --- a/docker/runner-entrypoint.ps1 +++ b/docker/runner-entrypoint.ps1 @@ -90,8 +90,11 @@ function Invoke-ActionsRunner { } try { - & .\run.cmd 2>&1 | Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append - return $LASTEXITCODE + & .\run.cmd 2>&1 | + Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append | + Out-Null + $runnerExitCode = $LASTEXITCODE + return $runnerExitCode } finally { foreach ($name in $credentialNames) { [Environment]::SetEnvironmentVariable($name, $savedCredentials[$name], "Process") @@ -203,7 +206,8 @@ try { & .\config.cmd @configArgs $script:RunnerConfigured = $true Write-RunnerLog "starting runner $env:RUNNER_NAME" - exit (Invoke-ActionsRunner) + $runnerExitCode = Invoke-ActionsRunner + exit $runnerExitCode } finally { Pop-Location } diff --git a/test/smoke-harness.test.ts b/test/smoke-harness.test.ts index 60c3ad4..a1aa272 100644 --- a/test/smoke-harness.test.ts +++ b/test/smoke-harness.test.ts @@ -213,7 +213,10 @@ describe("runner registration smoke harness", () => { expect(windowsEntrypoint).toContain(`\"${name}\"`); } expect(linuxEntrypoint).toContain("run_actions_runner 2>&1"); - expect(windowsEntrypoint).toContain("exit (Invoke-ActionsRunner)"); + expect(windowsEntrypoint).toContain("Out-Null"); + expect(windowsEntrypoint).toContain("$runnerExitCode = Invoke-ActionsRunner"); + expect(windowsEntrypoint).toContain("exit $runnerExitCode"); + expect(windowsEntrypoint).not.toContain("exit (Invoke-ActionsRunner)"); expect(macosBootstrap).toContain( "unset GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY" ); From 253f1dee55d84c0352b5f40626d935e5ba678327 Mon Sep 17 00:00:00 2001 From: Hephaestus Date: Tue, 23 Jun 2026 17:53:22 +0100 Subject: [PATCH 3/3] Scrub GitHub token env vars --- docker/runner-entrypoint.ps1 | 2 ++ docker/runner-entrypoint.sh | 2 ++ scripts/guest/macos-runner-bootstrap.sh | 2 +- test/smoke-harness.test.ts | 4 +++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/runner-entrypoint.ps1 b/docker/runner-entrypoint.ps1 index 4a85f8c..2e35f6d 100644 --- a/docker/runner-entrypoint.ps1 +++ b/docker/runner-entrypoint.ps1 @@ -78,6 +78,8 @@ function Request-RunnerToken { function Invoke-ActionsRunner { $credentialNames = @( "GITHUB_PAT", + "GITHUB_TOKEN", + "GH_TOKEN", "GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY" diff --git a/docker/runner-entrypoint.sh b/docker/runner-entrypoint.sh index 2464797..aec807e 100755 --- a/docker/runner-entrypoint.sh +++ b/docker/runner-entrypoint.sh @@ -26,6 +26,8 @@ run_runner_bash() { run_actions_runner() { run_runner_bash "cd '${RUNNER_HOME}' && exec ./run.sh" \ -u GITHUB_PAT \ + -u GITHUB_TOKEN \ + -u GH_TOKEN \ -u GITHUB_APP_ID \ -u GITHUB_APP_INSTALLATION_ID \ -u GITHUB_APP_PRIVATE_KEY diff --git a/scripts/guest/macos-runner-bootstrap.sh b/scripts/guest/macos-runner-bootstrap.sh index cfa4544..fcc387c 100755 --- a/scripts/guest/macos-runner-bootstrap.sh +++ b/scripts/guest/macos-runner-bootstrap.sh @@ -108,7 +108,7 @@ RUNNER_CONFIGURED="true" log "starting runner ${RUNNER_NAME}" ( - unset GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY + unset GITHUB_PAT GITHUB_TOKEN GH_TOKEN GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY cd "${RUNNER_ROOT}" ./run.sh ) diff --git a/test/smoke-harness.test.ts b/test/smoke-harness.test.ts index a1aa272..3662db2 100644 --- a/test/smoke-harness.test.ts +++ b/test/smoke-harness.test.ts @@ -7,6 +7,8 @@ import { afterEach, describe, expect, test } from "vitest"; const tempRoots: string[] = []; const runnerCredentialNames = [ "GITHUB_PAT", + "GITHUB_TOKEN", + "GH_TOKEN", "GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY", @@ -218,7 +220,7 @@ describe("runner registration smoke harness", () => { expect(windowsEntrypoint).toContain("exit $runnerExitCode"); expect(windowsEntrypoint).not.toContain("exit (Invoke-ActionsRunner)"); expect(macosBootstrap).toContain( - "unset GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY" + "unset GITHUB_PAT GITHUB_TOKEN GH_TOKEN GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY" ); });