Skip to content

fix(ci): move Test job to a GitHub-hosted runner (fork-PR RCE on self-hosted) - #31

Open
mattmattox wants to merge 1 commit into
mainfrom
security/test-job-to-hosted-runner
Open

fix(ci): move Test job to a GitHub-hosted runner (fork-PR RCE on self-hosted)#31
mattmattox wants to merge 1 commit into
mainfrom
security/test-job-to-hosted-runner

Conversation

@mattmattox

Copy link
Copy Markdown
Contributor

What

Moves the Test job from self-hosted-linux to ubuntu-latest. One line changed, plus a comment explaining why it must stay that way.

Why

This is a public repository, and Test was reachable by pull_request from any fork with no job-level guard, running on our self-hosted ARC runner.

make test is go test ./... — so a fork PR executed attacker-authored test code on a runner that has:

  • privileged dind and sudo
  • the shared /cache hostPath holding GOCACHE/GOMODCACHE — poisoning it runs attacker code in later, unrelated builds across two GitHub orgs
  • node-level Harbor pull credentials
  • network reach to Vault, Harbor, Athens and the Kubernetes API

The sibling jobs were already safe — Build and Publish carry if: github.event_name != 'pull_request', Release is tag-gated. Only Test was unguarded, so the file was internally inconsistent.

Why hosted instead of adding the same guard

Adding if: github.event_name != 'pull_request' would close the hole by disabling PR testing entirely — a real loss for a public repo that accepts outside contributions.

Moving to a hosted runner keeps full PR coverage and costs nothing: GitHub-hosted standard runners have unlimited free minutes on public repositories.

Verified safe to run hosted, before making the change

  • go.mod has 0 private-org requires and 0 replace directivesgo mod download resolves entirely from proxy.golang.org, no Athens/LAN needed
  • No Vault, Harbor or cluster access in the job; azure/setup-kubectl only installs the binary
  • Every step runs on a stock ubuntu-latest image

Negative check after the change

No job is both pull_request-reachable and self-hosted without a guard:

Job Runner Guard
Test ubuntu-latest none needed
Build self-hosted-linux github.event_name != 'pull_request'
Publish self-hosted-linux github.event_name != 'pull_request'
Release self-hosted-linux startsWith(github.ref, 'refs/tags/v')

This PR is its own test

It triggers on: pull_request, so the Test run on this PR is the live proof that the job works on a hosted runner. Merge once it's green.

Note for the future

If a private dependency is ever added here, do not move this back to self-hosted — split the private part into a separate, fork-guarded job. There's a comment on the job saying so.

Refs: cluster-services-517

https://claude.ai/code/session_01PYjZywQworVe5gurTn1J5D

…self-hosted)

This is a PUBLIC repository and the Test job was reachable by `pull_request` from
ANY fork with no job-level guard, on `runs-on: self-hosted-linux`.

`make test` is `go test ./...`, so a fork PR executed attacker-authored test code
on our self-hosted ARC runner - a runner with privileged dind, `sudo`, a shared
/cache hostPath holding GOCACHE/GOMODCACHE (poisoning it runs attacker code in
later, unrelated builds across two GitHub orgs), node-level Harbor pull
credentials, and network reach to Vault, Harbor, Athens and the Kubernetes API.
The runner namespace also hosts per-repo ci-deployer ServiceAccounts, and Vault's
Kubernetes token-mint role is bound to it.

The sibling jobs were already safe: Build and Publish carry
`if: github.event_name != 'pull_request'` and Release is tag-gated. Only Test was
unguarded - the file was internally inconsistent.

MOVED TO HOSTED RATHER THAN ADDING THAT SAME GUARD, deliberately. The guard would
close the hole by disabling PR testing entirely, which is a real loss for a public
repo that accepts outside contributions. A hosted runner keeps full PR coverage AND
costs nothing: GitHub-hosted standard runners have unlimited free minutes on public
repositories.

Verified safe to run hosted before making the change:
  - go.mod has ZERO private-org requires and ZERO replace directives, so
    `go mod download` resolves entirely from proxy.golang.org and needs no
    Athens/LAN access.
  - No Vault, Harbor or cluster access in the job. `azure/setup-kubectl` only
    installs the binary.
  - Steps are checkout / apt-get make / setup-go / go mod download / make test /
    make test-crds - all runnable on a stock ubuntu-latest image.

Build, Publish and Release stay on self-hosted: they are guarded to non-PR events
and genuinely need Harbor.

Negative check after the change - no job is both pull_request-reachable AND
self-hosted without a guard:
    Test     ubuntu-latest       (no guard needed)
    Build    self-hosted-linux   if: github.event_name != 'pull_request'
    Publish  self-hosted-linux   if: github.event_name != 'pull_request'
    Release  self-hosted-linux   if: startsWith(github.ref, 'refs/tags/v')

If a private dependency is ever added here, do NOT move this back to self-hosted -
split the private part into a separate, fork-guarded job instead. A comment on the
job records this.

Refs: cluster-services-517

Claude-Session: https://claude.ai/code/session_01PYjZywQworVe5gurTn1J5D
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