Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,8 @@ Please find more details about this security note on [GitHub documentation](http

> ⚠️ **`reuse: stop` (warm pools) makes this worse.** With reuse, a runner's disk carries over between jobs, so a later job can read a previous job's residue (checked-out code, caches, credentials written to disk). Only use `reuse: stop` for a **single trusted repository's** CI. Never combine it with public-repo / untrusted-PR workloads. The default `reuse: terminate` gives every job a fresh instance.

**Runners carry only their unique label.** The action registers every runner with `--no-default-labels`, so it never gets the implicit `self-hosted` / `Linux` / `X64` labels. A job can only be scheduled onto it via the exact per-run `label` output, never via `runs-on: [self-hosted, linux, x64]`. Without this, any job in the repository — including a fork PR's own job once its workflow is approved — could queue for up to 24 hours and take the next runner a trusted run starts, along with its instance role, VPC access and the registration token in IMDS user-data. Keep `runs-on: ${{ needs.start-runner.outputs.label }}` in your workflows; a generic `self-hosted` target will no longer match. Still restrict fork workflow approval to all outside collaborators on public repos (**Settings → Actions → Fork pull request workflows from outside collaborators**): a unique label is defence in depth, not a substitute.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for release notes and breaking changes. Pin the moving major tag (`@v4`) for the latest release in that line, or a specific version (`@v4.0.0`) to pin exactly.
Expand Down
12 changes: 10 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105472,7 +105472,7 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist
'cd /home/runner/actions-runner',
'rm -f .runner .credentials .credentials_rsaparams',
'gh_runner_phone_home configuring',
'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --ephemeral --unattended --disableupdate',
'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --no-default-labels --ephemeral --unattended --disableupdate',
'GH_RUNNER_STEP=registered',
'gh_runner_phone_home registered',
'sudo -u runner -H ./run.sh',
Expand Down Expand Up @@ -105579,6 +105579,14 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist
// RUNNER_ALLOW_RUNASROOT=1 escape hatch is gone. Runner has its own
// home under /home/runner/ and writes config.sh state there.
//
// - --no-default-labels on config.sh: the runner carries ONLY the unique
// per-run label, never the implicit self-hosted/Linux/X64 set. Without
// it, any job in the repo (including a fork PR's own job, once approved
// or from a returning contributor) could target `runs-on: [self-hosted,
// linux, x64]`, queue for up to 24h, and grab the runner the next time a
// trusted run starts one -- along with its instance role, subnet, EIP and
// the registration token in IMDS user-data. Requires runner >= 2.299.0.
//
// - --ephemeral --unattended --disableupdate on config.sh: one-job
// runner, no interactive prompts, no runtime self-update during the
// session. GitHub auto-deregisters ephemeral runners after their job,
Expand Down Expand Up @@ -105708,7 +105716,7 @@ function buildUserData({ runnerVersion, owner, repo, label, githubRegistrationTo
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
'GH_RUNNER_STEP=configuring',
'gh_runner_phone_home configuring',
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --ephemeral --unattended --disableupdate`,
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --no-default-labels --ephemeral --unattended --disableupdate`,
'GH_RUNNER_STEP=registered',
'gh_runner_phone_home registered',
'./run.sh',
Expand Down
12 changes: 10 additions & 2 deletions src/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist
'cd /home/runner/actions-runner',
'rm -f .runner .credentials .credentials_rsaparams',
'gh_runner_phone_home configuring',
'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --ephemeral --unattended --disableupdate',
'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --no-default-labels --ephemeral --unattended --disableupdate',
'GH_RUNNER_STEP=registered',
'gh_runner_phone_home registered',
'sudo -u runner -H ./run.sh',
Expand Down Expand Up @@ -539,6 +539,14 @@ function buildReusableUserData({ runnerVersion, owner, repo, label, githubRegist
// RUNNER_ALLOW_RUNASROOT=1 escape hatch is gone. Runner has its own
// home under /home/runner/ and writes config.sh state there.
//
// - --no-default-labels on config.sh: the runner carries ONLY the unique
// per-run label, never the implicit self-hosted/Linux/X64 set. Without
// it, any job in the repo (including a fork PR's own job, once approved
// or from a returning contributor) could target `runs-on: [self-hosted,
// linux, x64]`, queue for up to 24h, and grab the runner the next time a
// trusted run starts one -- along with its instance role, subnet, EIP and
// the registration token in IMDS user-data. Requires runner >= 2.299.0.
//
// - --ephemeral --unattended --disableupdate on config.sh: one-job
// runner, no interactive prompts, no runtime self-update during the
// session. GitHub auto-deregisters ephemeral runners after their job,
Expand Down Expand Up @@ -668,7 +676,7 @@ function buildUserData({ runnerVersion, owner, repo, label, githubRegistrationTo
'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1',
'GH_RUNNER_STEP=configuring',
'gh_runner_phone_home configuring',
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --ephemeral --unattended --disableupdate`,
`./config.sh --url "https://github.com/${owner}/${repo}" --token "${githubRegistrationToken}" --labels "${label}" --no-default-labels --ephemeral --unattended --disableupdate`,
'GH_RUNNER_STEP=registered',
'gh_runner_phone_home registered',
'./run.sh',
Expand Down
4 changes: 2 additions & 2 deletions tests/phone-home-detail.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe('phone-home failure-detail capture (real bash execution)', () => {
'\nGH_REGISTER_SCRIPT',
).replace(/^#!\/bin\/bash\n/, '');

const marker = 'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --ephemeral --unattended --disableupdate';
const marker = 'sudo -u runner -H env DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ./config.sh --url "$GH_REPO_URL" --token "$GH_TOKEN" --labels "$GH_LABEL" --no-default-labels --ephemeral --unattended --disableupdate';
const value = runAndCapturePhoneHome(registerScriptBody, {
marker,
fakeCommand: LONG_MULTILINE_FAKE_CMD.call,
Expand Down Expand Up @@ -186,7 +186,7 @@ describe('phone-home failure-detail capture (real bash execution)', () => {
const ud = buildUserData({ ...args, reuse: 'terminate' });
const heredocBody = extractBetween(ud, "sudo -u runner -H bash <<'RUNNER_BOOTSTRAP'\n", '\nRUNNER_BOOTSTRAP');

const marker = './config.sh --url "https://github.com/o/r" --token "TOK" --labels "l" --ephemeral --unattended --disableupdate';
const marker = './config.sh --url "https://github.com/o/r" --token "TOK" --labels "l" --no-default-labels --ephemeral --unattended --disableupdate';
const value = runAndCapturePhoneHome(heredocBody, {
marker,
fakeCommand: LONG_MULTILINE_FAKE_CMD.call,
Expand Down
15 changes: 15 additions & 0 deletions tests/userdata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ describe('buildUserData', () => {
expect(ud).toContain('--ephemeral --unattended --disableupdate');
});

// Without --no-default-labels the runner also gets self-hosted/Linux/X64,
// which lets any job in the repo (e.g. a fork PR's own job) target
// `runs-on: [self-hosted, linux, x64]` and take the runner from the
// trusted run that started it.
test('registers with --no-default-labels so only the unique label matches (cold launch)', () => {
const line = buildUserData(args).split('\n').find(l => l.includes('./config.sh '));
expect(line).toContain('--labels "runner-abc12" --no-default-labels');
});

test('registers with --no-default-labels on every warm-pool boot (reuse: stop)', () => {
const lines = buildUserData({ ...args, reuse: 'stop' }).split('\n').filter(l => l.includes('./config.sh '));
expect(lines).toHaveLength(1);
expect(lines[0]).toContain('--labels "$GH_LABEL" --no-default-labels');
});

test('arms a TTL self-destruct shutdown when max-lifetime-minutes > 0', () => {
const ud = buildUserData({ ...args, maxLifetimeMinutes: '360' });
expect(ud).toContain('shutdown -h +360 || true');
Expand Down
Loading