feat: OIDC execution tokens for CI workflow authorization (POC)#5700
feat: OIDC execution tokens for CI workflow authorization (POC)#5700shiroyasha wants to merge 7 commits into
Conversation
Issue signed OIDC tokens when triggering Semaphore workflows and expose a verify API plus CLI command so CI jobs can confirm a canvas node is authorized before running production deploys or artifact uploads. Signed-off-by: Igor Šarčević <igor@operately.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
👋 Commands for maintainers:
|
Docs Impact ReviewThis PR introduces a new Suggested docs updates:
Why: The PR ships a new CLI subcommand ( Maintainer commands
Posted automatically by warp-gateway · commit 761864b |
|
Risk: 72/100 (high) SummaryIntroduces RSA-signed OIDC JWT execution tokens injected into Semaphore CI workflow parameters, plus a CLI Concerns
Recommended reviewers: bender-rodriguez-unit1, forestileao |
Abort Semaphore workflow triggers when execution tokens cannot be signed, and return a single generic verification failure message to callers while logging detailed reasons server-side. Signed-off-by: Igor Šarčević <igor@operately.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Move CI token parsing to pkg/ciauth and let semaphore.runWorkflow own signing. Drop SuperPlane-side AuthorizeExecution; the verify API now checks signatures and optional expected claims while Semaphore enforces pipeline policy via CLI flags. Signed-off-by: Igor Šarčević <igor@operately.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Igor Šarčević <igor@operately.com>
Remove the /api/v1/oidc/verify endpoint and validate execution tokens in the CLI by fetching OIDC discovery and JWKS, so CI verifies independently. Signed-off-by: Igor Šarčević <igor@operately.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Igor Šarčević <igor@operately.com>
| semaphoreOIDCTokenDuration = time.Hour | ||
|
|
||
| semaphoreClaimOrgID = "org_id" | ||
| semaphoreClaimCanvasID = "canvas_id" |
| } | ||
|
|
||
| return ctx.OIDC.Sign( | ||
| fmt.Sprintf("execution:%s", ctx.ID), |
There was a problem hiding this comment.
do we need the execution: prefix?
| } | ||
|
|
||
| parameters["SUPERPLANE_EXECUTION_ID"] = ctx.ID | ||
| parameters["SUPERPLANE_EXECUTION_ID"] = ctx.ID.String() |
There was a problem hiding this comment.
why do we suddenly need .String()?
| return parameters | ||
| if ctx.OIDC == nil { | ||
| return nil, fmt.Errorf("OIDC provider is not configured") | ||
| } |
There was a problem hiding this comment.
No need to do this.
|
|
||
| token, err := r.signOIDCToken(ctx, spec, metadata) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to sign OIDC execution token: %w", err) |
There was a problem hiding this comment.
Check if this error is leaking some data to users.
| semaphoreClaimCommitSha = "commit_sha" | ||
| ) | ||
|
|
||
| func (r *RunWorkflow) signOIDCToken(ctx core.ExecutionContext, spec RunWorkflowSpec, metadata RunWorkflowNodeMetadata) (string, error) { |
There was a problem hiding this comment.
Don't break a class into multiple files.
| // The approval component doesn't call Pass() in Execute(), so it should remain in started state. | ||
| // | ||
| executor := NewNodeExecutor(r.Encryptor, r.Registry, r.GitProvider, "http://localhost", "http://localhost", "", r.AuthService) | ||
| executor := NewNodeExecutor(r.Encryptor, r.Registry, r.GitProvider, nil, "http://localhost", "http://localhost", "", r.AuthService) |
There was a problem hiding this comment.
can we use r.oidc provider?
| github.com/cenkalti/backoff/v5 v5.0.3 // indirect | ||
| github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||
| github.com/felixge/httpsnoop v1.0.4 // indirect | ||
| github.com/go-jose/go-jose/v4 v4.1.4 // indirect |
There was a problem hiding this comment.
Why, for the love of god would you add v4 if you already have v5?
| github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 | ||
| github.com/casbin/casbin/v2 v2.134.0 | ||
| github.com/casbin/gorm-adapter/v3 v3.37.0 | ||
| github.com/coreos/go-oidc/v3 v3.19.0 |
There was a problem hiding this comment.
Can we switch the OIDC to use this too?
There was a problem hiding this comment.
Seperate PR of course.
Summary
semaphore.runWorkflowtriggers a pipeline, injected as theSUPERPLANE_OIDC_TOKENworkflow parameter alongside existingSUPERPLANE_*IDs.POST /api/v1/oidc/verifyendpoint that validates the JWT signature and confirms the canvas/node is authorized in the live canvas spec.superplane oidc verifyCLI command for CI pre-job steps (Semaphore, GitHub Actions, GitLab, etc.) to abort unauthorized production deploys or artifact uploads.Test plan
go test ./pkg/oidc/... ./pkg/public/... ./pkg/integrations/semaphore/components/...SUPERPLANE_OIDC_TOKENis present in workflow parameterssuperplane oidc verify --pipeline-file ".semaphore/..."in a Semaphore job pre-step; confirm exit 0 for valid token and non-zero for missing/invalid tokenMade with Cursor