Jwt runner auth 2#3978
Conversation
This commit includes the bare minimum implementation to issue a JWT per task execution. There is room for improvement like the bound claims should be reviewed or the subject in the token. (does it make sense to use "task:%d"? Is that a unique value over all projects?)
There was a problem hiding this comment.
Stale comment
Security review — PR #3978 (JWT runner auth)
Outcome: No medium, high, or critical vulnerabilities identified in the added/modified code.
Reviewed the JWT issuance flow end-to-end: template
jwt_paramsvalidation, signer initialization/key storage, runner job delivery, local task execution (SEMAPHORE_JWT), and the public JWKS endpoint. Prior automation threads were not present on this PR.Areas reviewed
- Authn/authz: JWTs are minted only when the global signer is initialized (
jwt_enabled) and the template hasjwt_params.enabled. Runners still requireX-Runner-Token; template changes requireCanManageProjectResources. No unauthenticated minting path found.- Input validation: TTL is bounded at save time (
TemplateJWTParams.Validate) and again at sign time (Signer.Signclamps toMaxTTL). Audience size is capped (32 entries).- Key handling: Signing key is persisted via existing
AccessKeyEncryptionmachinery (same pattern as access keys). JWKS exposes only the public key, gated onjwt_enabled.- Dependencies:
go-jose/v4is pinned to v4.1.4 (includes the JWE panic fix); this PR uses JWS signing only, not JWE decryption of untrusted input.- Secret delivery: JWT is delivered to runners/tasks alongside existing job secrets; no new unauthenticated exposure beyond the established runner API contract.
Residual notes (not reported as findings)
These are operational/design considerations rather than exploitable defects in this diff:
- Project managers can configure JWT
audvalues for their templates; downstream services must enforce their own authorization (e.g., validateaud,iss, and scope claims such asproject_id).- When
access_key_encryptionis unset, the signing key is stored with the same base64-only encoding used for other secrets — DB/admin access remains the control, consistent with pre-existing behavior.No inline finding comments were added.
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: 1e4fb26fff
ℹ️ 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".
| encryptionService server.AccessKeyEncryptionService, | ||
| keyInstallationService server.AccessKeyInstallationService, | ||
| logWriteService pro_interfaces.LogWriteService, | ||
| signer jwt.Signer, |
There was a problem hiding this comment.
Keep CreateTaskPool call sites compiling
Adding this required parameter leaves existing call sites unupdated: services/tasks/TaskRunner_test.go and api/cluster_test.go still call CreateTaskPool with seven arguments. Those packages will fail to compile under go test before any tests run, so either update the callers to pass the signer or avoid making the constructor signature incompatible.
Useful? React with 👍 / 👎.
| if err := store.SetOption(jwtSigningKeyOption, encrypted); err != nil { | ||
| return nil, fmt.Errorf("persist signing key: %w", err) | ||
| } |
There was a problem hiding this comment.
Widen option storage before saving signing keys
When JWT is enabled on SQL-backed installs, this persists the generated PEM through the existing option.value column, which is defined as varchar(255) in the SQL migrations. Even an unencrypted base64-encoded P-256 private-key PEM is over 300 bytes, and AES-GCM adds more, so first startup with JWT enabled will fail or store a truncated key on MySQL/Postgres; the key needs a larger column or a different storage path before calling SetOption here.
Useful? React with 👍 / 👎.
| jwt_enabled: | ||
| type: boolean | ||
| description: > | ||
| When true, Semaphore mints a short-lived JWT for each task run and exposes | ||
| its public key via /.well-known/jwks.json. | ||
| jwt_issuer: | ||
| type: string | ||
| description: Value emitted in the `iss` claim of issued JWTs. | ||
| jwt_default_ttl: |
There was a problem hiding this comment.
Document JWT config under the actual
jwt object
These schema properties are top-level jwt_enabled/jwt_issuer keys, but the new config field is JWT *JWTConfig with JSON name jwt, so the runtime expects jwt: { enabled, issuer, default_ttl, max_ttl }. Users following this schema can create a validating config that is silently ignored by ConfigInit, leaving JWT disabled and the UI hidden.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Stale comment
Security review — PR #3978 (JWT runner auth)
Outcome: No medium, high, or critical vulnerabilities identified in the added/modified code.
Re-reviewed the synchronized PR diff end-to-end and validated prior automation findings; no unresolved security threads remain.
Areas reviewed
- Authn/authz: JWTs are minted only when the global signer is initialized (
jwt.enabled) and a template hasjwt_params.enabled. Runners still requireX-Runner-Token; template changes requireCanManageProjectResources. No unauthenticated minting path found.- Input validation: TTL is bounded at save time (
TemplateJWTParams.Validate) and again at sign time (Signer.Signclamps toMaxTTL). Audience size is capped (32 entries).- Key handling: Signing key is persisted via existing
AccessKeyEncryptionmachinery. JWKS exposes only the public key, gated onjwt.enabled.- Dependencies:
go-jose/v4v4.1.4 is used for JWS signing only; no untrusted JWE/JWS verification path was added.- Secret delivery: JWT is delivered to runners/tasks alongside existing job secrets under the established runner API contract.
Residual notes (not reported as findings)
- Project managers can configure JWT
audvalues for their templates; downstream services must enforce their own authorization (validateaud,iss, and scope claims such asproject_id).- When
access_key_encryptionis unset, the signing key uses the same base64-only encoding as other secrets — DB/admin access remains the control, consistent with pre-existing behavior.- The
option.valuevarchar(255)column may be too small for the encrypted signing key on SQL backends; this is a reliability/deployment concern, not an attacker-controlled exploit path.No inline finding comments were added.
Slack summary: PR #3978 security review complete — no medium/high/critical vulnerabilities found. JWT issuance, key storage, runner delivery, and JWKS endpoint were reviewed; controls (runner auth, template validation, TTL clamping, encrypted key storage) appear sufficient. Residual risk is operational: downstream services must validate JWT claims, and SQL
optioncolumn sizing may block key persistence.Sent by Cursor Automation: Find vulnerabilities
There was a problem hiding this comment.
Security review — PR #3978 (JWT runner auth)
Outcome: No medium, high, or critical vulnerabilities identified in the added/modified code.
Re-reviewed the synchronized PR diff end-to-end and validated prior automation assessments; no unresolved Cursor security finding threads remain.
Areas reviewed
- Authn/authz: JWTs are minted only when the global signer is initialized (
jwt.enabled) and a template hasjwt_params.enabled. Runners still requireX-Runner-Token; template changes requireCanManageProjectResources. No unauthenticated minting path found. - Input validation: TTL is bounded at save time (
TemplateJWTParams.Validate) and again at sign time (Signer.Signclamps toMaxTTL). Audience size is capped (32 entries). - Key handling: Signing key is persisted via existing
AccessKeyEncryptionmachinery. JWKS exposes only the public key, gated onjwt.enabled. - Dependencies:
go-jose/v4v4.1.4 is used for JWS signing only; no untrusted JWE/JWS verification path was added. - Secret delivery: JWT is delivered to runners/tasks alongside existing job secrets under the established runner API contract.
Residual notes (not reported as findings)
- Project managers can configure JWT
audvalues for their templates; downstream services must enforce their own authorization (validateaud,iss, and scope claims such asproject_id). - When
access_key_encryptionis unset, the signing key uses the same base64-only encoding as other secrets — DB/admin access remains the control, consistent with pre-existing behavior.
No inline finding comments were added.
Slack summary: PR #3978 security review complete — no medium/high/critical vulnerabilities found. JWT issuance, key storage, runner delivery, and JWKS endpoint were reviewed; controls (runner auth, template validation, TTL clamping, encrypted key storage) appear sufficient. Residual risk is operational: downstream services must validate JWT claims.
Sent by Cursor Automation: Find vulnerabilities


No description provided.