Skip to content

fix: register runners with --no-default-labels - #83

Merged
kurok merged 1 commit into
mainfrom
fix/no-default-labels
Oct 6, 2026
Merged

kurok merged 1 commit into
mainfrom
fix/no-default-labels

Conversation

@kurok

@kurok kurok commented Oct 5, 2026 •

Copy link
Copy Markdown

Problem

The runner is registered with config.sh --labels <unique> but without --no-default-labels, so GitHub also attaches self-hosted, Linux and X64. Consumer workflows gate their own jobs on needs.start-runner.outputs.label, but nothing stops another job in the same repository from using runs-on: [self-hosted, linux, x64]. On a public repo a fork PR can add such a job: it waits in the queue (up to 24h) and takes the runner the next time a trusted run starts one, which exposes the instance role credentials, VPC/subnet and whitelisted EIP access, and the registration token in IMDS user-data (reusable within its 1h validity to register a rogue runner under the same label and receive the stalled trusted job).

Found while reviewing namecheap/terraform-provider-namecheap CI (ci.yml start-runner / acceptance_test).

Fix

  • --no-default-labels on both config.sh invocations: the cold-launch bootstrap and the warm-pool (reuse: stop) per-boot register script.
  • Tests for both paths in tests/userdata.test.js; tests/phone-home-detail.test.js markers updated.
  • README security section documents the behaviour and that runs-on: self-hosted no longer matches these runners.
  • dist/ rebuilt (npm ci && npm run package).

Requires actions/runner >= 2.299.0; the pinned default is 2.337.0.

Why not just-in-time runner config

Switching to generate-jitconfig was considered for the token-in-user-data part. The JIT API has no --no-default-labels equivalent, and whether GitHub attaches the read-only default labels to JIT runners server-side is not documented (field reports conflict), so it cannot be relied on to close the label hole this PR closes. Left as a follow-up to verify empirically; the registration token stays scoped to IMDS on the instance (hop limit 1), which only trusted-label jobs can now reach.

Verification

  • npm test: 247 passed
  • npm run lint: clean
  • verify-dist should pass (dist rebuilt from a clean npm ci)

Every runner now carries only its unique per-run label, never the
implicit self-hosted/Linux/X64 set. Without this any job in the repo
(e.g. a fork PR's own job once approved, or from a returning
contributor) could target runs-on: [self-hosted, linux, x64], wait in
the queue for up to 24h and take the next runner a trusted run starts,
together with its instance role, VPC/EIP access and the registration
token held in IMDS user-data.

Applied to both registration paths: the cold-launch bootstrap and the
warm-pool (reuse: stop) per-boot register script. Requires actions/runner
>= 2.299.0 (the pinned default is 2.337.0).

Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
@kurok
kurok merged commit 9d745d6 into main Oct 6, 2026
13 of 20 checks passed
kurok added a commit to namecheap/terraform-provider-namecheap that referenced this pull request Oct 6, 2026
## Problem

`start-runner` registers the EC2 runner with its unique label but the
pinned action does not pass `--no-default-labels`, so the runner also
gets `self-hosted`, `Linux` and `X64`. The `if:` gates on `start-runner`
/ `acceptance_test` only cover this workflow's own jobs. A fork PR can
add a job with `runs-on: [self-hosted, linux, x64]`; it waits in the
queue (up to 24h) and takes the runner the next time a trusted run
starts one, gaining the instance role credentials, VPC subnet access,
the whitelisted sandbox EIP and the registration token in user-data
(which can register a rogue runner under the same label and receive the
stalled `acceptance_test` job carrying `NAMECHEAP_API_KEY`).

Precondition today: the repo already requires approval for workflows
from **all** outside collaborators (`fork-pr-contributor-approval` =
`all_external_contributors`), so this is not currently exploitable
without a maintainer approving the fork's workflow. This PR closes the
hole regardless of that setting.

## Fix

- Pin `namecheap/ec2-github-runner` to `9d745d6` (main, merge of
namecheap/ec2-github-runner#83), which registers with
`--no-default-labels` on both the cold-launch and warm-pool paths.
`acceptance_test` already uses `runs-on: ${{
needs.start-runner.outputs.label }}`, so nothing else changes.
- `SECURITY_COMPLIANCE.md`: document the label guarantee and the fork
approval policy under "Fork-safe pull-request CI".

## Verification

Full CI on this branch with the new action: `Start self-hosted EC2
runner`, `Acceptance test` and `Stop self-hosted EC2 runner` all passed,
so the runner is still scheduled via its unique label.

## Not done here

- Just-in-time runner config instead of a registration token in
user-data: the JIT API has no `--no-default-labels` equivalent and it is
undocumented whether default labels are attached server-side, so it
would risk reopening this hole. Follow-up on the action.
- Keep the fork approval policy at `all_external_contributors`; it is a
repo setting, not code.

Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant