feat(runners): allow to create unregistered runners for terraform usage#3904
feat(runners): allow to create unregistered runners for terraform usage#3904fiftin wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: 1 medium-severity finding (runner pre-provisioning hijack).
The unregistered-runner flow is useful for Terraform, but finishing registration with only the global
runner_registration_tokenplus a numericrunner_iddoes not prove the caller owns that pre-created slot. Anyone with the shared registration secret can claim an unregistered runner before the intended host registers and then receive that runner's bearer token (and downstream job secrets).Recommendation: Return a one-time registration secret when creating an unregistered runner (admin API) and require it (or a signed claim) in
POST /api/internal/runnerswhenrunner_idis set.No prior automation threads were present to resolve.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: abdd1bee84
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Unregistered is a transient flag (never persisted) used at creation time to | ||
| // request a runner without a token. Such a runner gets an empty token and must | ||
| // be registered later via `semaphore runner register --runner-id <id>`. | ||
| Unregistered bool `db:"-" json:"unregistered"` |
There was a problem hiding this comment.
Hide the transient runner flag from responses
Because this field is tagged db:"-", it is never loaded from either store after creation, but the json:"unregistered" tag makes every runner GET/list response serialize it as false. For the Terraform flow added here, a runner created with unregistered: true will read back as unregistered: false on the next refresh, causing clients that round-trip this request field to see permanent drift even though the runner is still tokenless; either omit it from response JSON or derive it from Token == "".
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: 1 medium-severity finding remains.
This revision adds one-time
registration_tokengeneration and hashed storage at admin create time (RunnerService), butPOST /api/internal/runnerswithrunner_idstill only checks the fleet-widerunner_registration_token. The per-runner secret is never required, so pre-provisioned runner hijacking is still possible for anyone who knows the shared registration secret and the numeric runner ID.Recommendation: When
runner_idis present, require the per-runnerregistration_token(hash compare + expiry) instead of (or in addition to) the global token; extendRunnerRegistration/semaphore runner registerto pass it.Slack summary: PR #3904 security review — 1 medium finding. Pre-provisioned runner hijack still open: per-runner registration tokens are issued on create but not enforced on
POST /api/internal/runnerswithrunner_id; fleet-wide token + ID is still enough to claim the slot and obtain the runner bearer token.Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: 1 medium-severity finding remains.
Admin create now issues a per-runner
registration_token(hashed + TTL inRunnerService), butPOST /api/internal/runnerswithrunner_idstill only checks the fleet-widerunner_registration_token. The per-runner secret is never validated, so pre-provisioned runner hijacking is still possible for anyone who knows the shared registration secret and the numeric runner ID.Recommendation: When
runner_idis present, require the per-runnerregistration_tokenfrom the create response (constant-time hash compare, enforceregistration_token_expires_at, clear hash on success). ExtendRunnerRegistrationandsemaphore runner registerto send it.Slack summary: PR #3904 security review — 1 medium finding. Pre-provisioned runner hijack still open: per-runner registration tokens are stored at create time but not enforced on
POST /api/internal/runnerswithrunner_id; fleet-wide token + ID is still enough to claim the slot and obtain the runner bearer token.Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: 1 medium-severity finding remains.
Admin create (
RunnerService) now issues a per-runnerregistration_token(SHA-256 hash + 1h TTL persisted), butPOST /api/internal/runnerswithrunner_idstill only checks the fleet-widerunner_registration_token.RegisterRunnernever compares the presented token toregistration_token/registration_token_expires_aton the runner row, so the one-time secret does not bind registration to the admin/Terraform-created slot.Recommendation: When
runner_idis set, require the per-runnerregistration_tokenfrom the create response (constant-time hash compare viaHashRunnerRegistrationToken, enforce expiry, clear hash on success). ExtendRunnerRegistrationandsemaphore runner registerto send it (distinct from the global token).Slack summary: PR #3904 security review — 1 medium finding. Pre-provisioned runner hijack still open: per-runner registration tokens are stored at create time but not enforced on
POST /api/internal/runnerswithrunner_id; fleet-wide token + numeric ID is still enough to claim the slot and obtain the runner bearer token (and downstream job secrets).Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No medium-or-higher findings on the current revision.
The earlier pre-provisioned runner hijack is addressed:
POST /api/internal/runnersnow branches on the presentedregistration_token— the fleet-widerunner_registration_tokenonly creates a new runner; any other value is hashed and passed toRegisterRunner, which binds registration to the one-timesmrs_token issued at admin create time (hash + 1h expiry, cleared on success).runner_idis no longer accepted on registration, so global token + numeric ID cannot claim a slot.Controls verified: admin APIs remain behind
adminMiddleware; runner auth tokens stayjson:"-"in list/get responses; unregistered runners cannot be activated via update/active endpoints.Slack summary: PR #3904 security review — no open medium+ findings. Prior runner-hijack issue is fixed via one-time registration tokens enforced in
RegisterRunner.Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No medium-or-higher findings on the current revision (
4184061c).The earlier pre-provisioned runner hijack is addressed.
POST /api/internal/runnersnow branches on the presentedregistration_token: the fleet-widerunner_registration_tokenonly creates a new runner; any other value is hashed and passed tostore.RegisterRunner, which binds registration to the one-timesmrs_token issued at admin create time (SHA-256 hash, 1h TTL, cleared on success).runner_idis no longer accepted on registration, so global token + numeric ID cannot claim a pre-created slot.Controls verified: admin runner APIs remain behind
adminMiddleware; auth tokens stayjson:"-"in list/get responses; unregistered runners cannot be activated via update/active endpoints.Slack summary: PR #3904 security review — no open medium+ findings. Prior runner-hijack issue is fixed via one-time registration tokens enforced in
RegisterRunner.Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No medium-or-higher findings on the current revision (
b44c431e).Prior findings validated: The earlier pre-provisioned runner hijack is addressed.
POST /api/internal/runnersbranches onregistration_token: the fleet-widerunner_registration_tokenonly creates a new runner; any other value is hashed and passed tostore.RegisterRunner, which binds registration to the one-timesmrs_token issued at admin create time (SHA-256 hash, 1h TTL, cleared on success).runner_idis no longer accepted on registration, so global token + numeric ID cannot claim a pre-created slot.Latest delta (
b44c431e):POST …/registration-token(admin-only, behindadminMiddleware+ runner middleware) rotates the one-time hash viaRunnerService.RegenerateRegistrationToken; this does not weaken registration binding and invalidates the previous hash.Controls verified: runner auth tokens remain
json:"-"in list/get responses; unregistered runners cannot be activated via update/active endpoints; registration plaintext is not persisted (only hash + expiry).Slack summary: PR #3904 security review — no open medium+ findings. Prior runner-hijack issue remains fixed; token regeneration endpoint reviewed with no new issues.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No open medium/high/critical findings.
Prior finding (pre-provisioned runner hijacking) — resolved
Earlier reviews flagged that an attacker with the deployment-wide
runner_registration_tokencould claim a Terraform-preprovisioned slot by supplying a guessablerunner_idonPOST /api/internal/runners.The current head addresses this:
RunnerRegistrationno longer acceptsrunner_id.- Unregistered runners receive a one-time
smrs_…token; only its SHA-256 hash and a 1-hour expiry are stored.- The non-global registration path calls
RegisterRunner(HashRunnerRegistrationToken(token), …), which binds registration to the matching row and clears the hash on success.- The global token path only creates a new runner; it cannot finalize an existing unregistered slot.
Possession of the per-runner registration token remains the capability to register that slot (by design). Token entropy (~256 bits) and TTL are appropriate for an unauthenticated internal endpoint.
Other checks
- Admin-only create/regenerate endpoints remain behind
adminMiddleware/ project runner middleware.- Activation of unregistered runners is blocked in
UpdateRunnerandSetRunnerActive.- Registration token plaintext is not persisted; list/get responses use
json:"-"on stored hash fields.- SQL uses parameterized queries for token hash lookup.
No new inline comments — nothing meets the bar for a current exploitable issue in the added/modified code.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No open medium/high/critical findings on commit
9fc1d9c.Prior findings validated
The earlier pre-provisioned runner hijacking issue is resolved. An intermediate revision allowed finishing a tokenless runner using only the deployment-wide
runner_registration_tokenplus a guessablerunner_id. The current code requires the per-runner one-timesmrs_…token:RegisterRunnerhashes the presented token and callsstore.RegisterRunner(hash, …), which matches the stored hash and expiry. The global config token can only create a new runner, not claim an existing unregistered slot.What was reviewed
- Admin runner create/regenerate paths (
RunnerService,GlobalRunnerController) — admin-gated; registration secrets returned once; only hashes persisted (SQL + Bolt).- Public runner registration (
POST /api/internal/runners) — one-time token flow; empty-token runners cannot authenticate viaRunnerMiddleware.- Credential fields on
db.Runner— authtokenand registration hash arejson:"-"; client cannot bind a token via JSON.No plausible authz bypass, injection, or secret-leak paths were found in the added/modified code.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Stale comment
Security review — PR #3904
Outcome: No open medium/high/critical findings on commit
d858536b.Prior findings validated
The earlier pre-provisioned runner hijacking issue is resolved.
POST /api/internal/runnersnow branches onregistration_token: the fleet-widerunner_registration_tokenonly creates a new runner; any other value is hashed (HashRunnerRegistrationToken) and passed tostore.RegisterRunner, which matches the stored hash, enforces expiry, and clears the one-time secret on success.runner_idis no longer accepted on registration.Controls verified
- Admin create/regenerate paths (
RunnerService,GlobalRunnerController) remain behindadminMiddleware; registration secrets are returned once; only hashes are persisted.- Auth
tokenand registration hash fields usejson:"-"ondb.Runner.- SQL/Bolt registration lookups use parameterized queries / safe filters.
- Unregistered runners cannot authenticate via
RunnerMiddleware(empty auth token).No plausible authz bypass, injection, or secret-leak paths were found in the added/modified code on this revision.
Slack summary: PR #3904 security review — no open medium+ findings. Prior runner-hijack issue remains fixed; token regeneration endpoint reviewed with no new issues.
Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Security review — PR #3904
Outcome: No open medium/high/critical findings on commit 636ced6f.
Prior findings validated
The earlier pre-provisioned runner hijacking issue is resolved. POST /api/internal/runners no longer accepts runner_id. The fleet-wide runner_registration_token only creates a new runner; finishing a Terraform-preprovisioned slot requires the per-runner smrs_… token, which is hashed (HashRunnerRegistrationToken) and validated in store.RegisterRunner (hash match, expiry, single use).
Latest revision (636ced6f)
RegenerateRegistrationToken / ResetRunnerRegistration now reset registered runners (clear auth token and public key, issue a new one-time hash). This endpoint remains behind adminMiddleware on global runners; impact is limited to admins intentionally rotating credentials.
Controls verified
- Registration secrets returned once from admin APIs; only SHA-256 hashes + expiry persisted.
- Auth
tokenand stored registration hash usejson:"-"ondb.Runner. - SQL registration lookup is parameterized; Bolt filters by hash in-process.
- Unauthenticated runner API calls require a non-empty
X-Runner-Tokenmatching a stored auth token (RunnerMiddleware).
No plausible authz bypass, injection, or secret-leak paths were found in the added/modified code on this revision.
Slack summary: PR #3904 security review — no open medium+ findings. Prior runner-hijack issue remains fixed; registered-runner reset/regenerate path reviewed with no new issues.
Sent by Cursor Automation: Find vulnerabilities


No description provided.