From 2f08602ead5a59680876cb4b4ccae3582ba95b6d Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Mon, 9 Mar 2026 18:32:34 +0100 Subject: [PATCH 1/3] feat(cli): add OIDC federated authentication support for CI/CD runners Enable CLI commands to authenticate using OIDC tokens provided by CI/CD runners (GitHub Actions, GitLab, etc.) when no explicit auth token is configured. This allows attestation commands to leverage the runner's federated identity, simplifying authentication setup in CI/CD pipelines. Signed-off-by: Miguel Martinez Signed-off-by: Miguel Martinez Entire-Checkpoint: 619ec71d895d --- app/cli/cmd/attestation_add.go | 5 +- app/cli/cmd/attestation_init.go | 5 +- app/cli/cmd/attestation_push.go | 3 +- app/cli/cmd/attestation_reset.go | 5 +- app/cli/cmd/attestation_status.go | 5 +- app/cli/cmd/root.go | 17 ++- app/cli/internal/token/token.go | 65 ++------- app/cli/internal/token/token_test.go | 136 ++---------------- pkg/attestation/crafter/runner.go | 3 + .../crafter/runners/azurepipeline.go | 8 +- .../crafter/runners/circleci_build.go | 8 +- .../crafter/runners/daggerpipeline.go | 5 +- pkg/attestation/crafter/runners/generic.go | 5 + .../crafter/runners/githubaction.go | 11 +- .../crafter/runners/gitlabpipeline.go | 3 + pkg/attestation/crafter/runners/jenkinsjob.go | 10 +- .../crafter/runners/teamcitypipeline.go | 10 +- .../crafter/runners/tektonpipeline.go | 2 + 18 files changed, 106 insertions(+), 200 deletions(-) diff --git a/app/cli/cmd/attestation_add.go b/app/cli/cmd/attestation_add.go index 84e2f73ab..a06bc2c31 100644 --- a/app/cli/cmd/attestation_add.go +++ b/app/cli/cmd/attestation_add.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -53,7 +53,8 @@ func newAttestationAddCmd() *cobra.Command { Use: "add", Short: "add a material to the attestation", Annotations: map[string]string{ - useAPIToken: "true", + useAPIToken: "true", + supportsFederatedAuthAnnotation: "true", }, Example: ` # Add a material to the attestation that is defined in the contract chainloop attestation add --name --value diff --git a/app/cli/cmd/attestation_init.go b/app/cli/cmd/attestation_init.go index 58ecfe7df..bc8afa43d 100644 --- a/app/cli/cmd/attestation_init.go +++ b/app/cli/cmd/attestation_init.go @@ -42,8 +42,9 @@ func newAttestationInitCmd() *cobra.Command { Use: "init", Short: "start attestation crafting process", Annotations: map[string]string{ - useAPIToken: trueString, - confirmWhenUserToken: trueString, + useAPIToken: trueString, + supportsFederatedAuthAnnotation: trueString, + confirmWhenUserToken: trueString, }, PreRunE: func(_ *cobra.Command, _ []string) error { if workflowName == "" { diff --git a/app/cli/cmd/attestation_push.go b/app/cli/cmd/attestation_push.go index 8b5495ccd..c3872911e 100644 --- a/app/cli/cmd/attestation_push.go +++ b/app/cli/cmd/attestation_push.go @@ -62,7 +62,8 @@ func newAttestationPushCmd() *cobra.Command { # Or alternatively chainloop attestation push --annotation key=value,key2=value2`, Annotations: map[string]string{ - useAPIToken: "true", + useAPIToken: "true", + supportsFederatedAuthAnnotation: "true", }, RunE: func(cmd *cobra.Command, _ []string) error { info, err := executableInfo() diff --git a/app/cli/cmd/attestation_reset.go b/app/cli/cmd/attestation_reset.go index 1b0a338aa..388a8b3af 100644 --- a/app/cli/cmd/attestation_reset.go +++ b/app/cli/cmd/attestation_reset.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,7 +31,8 @@ func newAttestationResetCmd() *cobra.Command { Use: "reset", Short: "mark current attestation process as canceled or failed", Annotations: map[string]string{ - useAPIToken: "true", + useAPIToken: "true", + supportsFederatedAuthAnnotation: "true", }, PreRunE: func(cmd *cobra.Command, args []string) error { if trigger != triggerFailed && trigger != triggerCanceled { diff --git a/app/cli/cmd/attestation_status.go b/app/cli/cmd/attestation_status.go index 382ac3755..69b6ea019 100644 --- a/app/cli/cmd/attestation_status.go +++ b/app/cli/cmd/attestation_status.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,7 +41,8 @@ func newAttestationStatusCmd() *cobra.Command { Use: "status", Short: "check the status of the current attestation process", Annotations: map[string]string{ - useAPIToken: "true", + useAPIToken: "true", + supportsFederatedAuthAnnotation: "true", }, RunE: func(cmd *cobra.Command, args []string) error { a, err := action.NewAttestationStatus( diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 033477f69..8b249834a 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -33,6 +33,7 @@ import ( "github.com/chainloop-dev/chainloop/app/cli/pkg/action" "github.com/chainloop-dev/chainloop/app/cli/pkg/plugins" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter" "github.com/chainloop-dev/chainloop/pkg/grpcconn" "github.com/chainloop-dev/chainloop/pkg/policies/engine/builtins" "github.com/rs/zerolog" @@ -66,7 +67,8 @@ const ( // Follow the convention stated on https://consoledonottrack.com/ doNotTrackEnv = "DO_NOT_TRACK" - trueString = "true" + trueString = "true" + supportsFederatedAuthAnnotation = "supportsFederatedAuth" ) var telemetryWg sync.WaitGroup @@ -119,6 +121,15 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { return err } + // If the auth token is not set and the command supports federated auth, we try to discover the runner and use the federated token for the runner if available + if authToken == "" && cmdSupportsFederatedAuth(cmd) { + r := crafter.DiscoverRunner(authToken, logger) + if r.IsAuthenticated() && r.FederatedToken() != nil { + logger.Debug().Str("runner", r.ID().String()).Msg("using federated auth token") + authToken = r.FederatedToken().RawToken + } + } + var opts = []grpcconn.Option{ grpcconn.WithInsecure(apiInsecure()), } @@ -497,6 +508,10 @@ func isAPITokenPreferred(cmd *cobra.Command) bool { return cmd.Annotations[useAPIToken] == trueString } +func cmdSupportsFederatedAuth(cmd *cobra.Command) bool { + return cmd.Annotations[supportsFederatedAuthAnnotation] == trueString +} + func getConfigDir(appName string) string { return filepath.Join(xdg.ConfigHome, appName) } diff --git a/app/cli/internal/token/token.go b/app/cli/internal/token/token.go index 7ad042223..baccfaec1 100644 --- a/app/cli/internal/token/token.go +++ b/app/cli/internal/token/token.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -111,64 +111,21 @@ func Parse(token string) (*ParsedToken, error) { pToken.ID = userID } case federatedTokenAudience: - if isGitLabFederatedToken(claims) { - pToken.TokenType = v1.Attestation_Auth_AUTH_TYPE_FEDERATED - if issuer, ok := claims["iss"].(string); ok { - pToken.ID = issuer - } + pToken.TokenType = v1.Attestation_Auth_AUTH_TYPE_FEDERATED + if issuer, ok := claims["iss"].(string); ok { + pToken.ID = issuer + } else { + return nil, nil } default: return nil, nil } - return pToken, nil -} - -// Checks if the claims contain at least 10 custom GitLab ID token claims. -// Reference: https://docs.gitlab.com/ci/secrets/id_token_authentication/ -func isGitLabFederatedToken(claims jwt.MapClaims) bool { - gitlabClaims := []string{ - "namespace_id", - "namespace_path", - "project_id", - "project_path", - "user_id", - "user_login", - "user_email", - "user_access_level", - "user_identities", - "pipeline_id", - "pipeline_source", - "job_id", - "ref", - "ref_type", - "ref_path", - "ref_protected", - "groups_direct", - "environment", - "environment_protected", - "deployment_tier", - "deployment_action", - "runner_id", - "runner_environment", - "sha", - "ci_config_ref_uri", - "ci_config_sha", - "project_visibility", - } - - requiredClaims := 10 - - // Count how many GitLab-specific claims are present - found := 0 - for _, claim := range gitlabClaims { - if _, exists := claims[claim]; exists { - found++ - if found >= requiredClaims { - return true - } - } + // Avoid returning partially filled tokens that would create invalid auth metadata + // in crafting state (e.g. type=UNSPECIFIED or empty ID). + if pToken.TokenType == v1.Attestation_Auth_AUTH_TYPE_UNSPECIFIED || pToken.ID == "" { + return nil, nil } - return found >= requiredClaims + return pToken, nil } diff --git a/app/cli/internal/token/token_test.go b/app/cli/internal/token/token_test.go index f0aaa2df2..218cbe526 100644 --- a/app/cli/internal/token/token_test.go +++ b/app/cli/internal/token/token_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ package token import ( "testing" - "github.com/golang-jwt/jwt/v4" - v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/stretchr/testify/assert" ) @@ -55,6 +53,18 @@ func TestParse(t *testing.T) { TokenType: v1.Attestation_Auth_AUTH_TYPE_FEDERATED, }, }, + { + name: "federated github token", + token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiY2hhaW5sb29wIiwicmVwb3NpdG9yeSI6Im1hdGlhc2luc2F1cnJhbGRlL3Byb2plY3QiLCJzdWIiOiJyZXBvOm1hdGlhc2luc2F1cnJhbGRlL3Byb2plY3Q6cmVmOnJlZnMvaGVhZHMvbWFpbiJ9.signature", + want: &ParsedToken{ + ID: "https://token.actions.githubusercontent.com", + TokenType: v1.Attestation_Auth_AUTH_TYPE_FEDERATED, + }, + }, + { + name: "federated token without issuer", + token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjaGFpbmxvb3AifQ.signature", + }, { name: "old api token (without orgID)", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjcC5jaGFpbmxvb3AiLCJhdWQiOlsiYXBpLXRva2VuLWF1dGguY2hhaW5sb29wIl0sImp0aSI6ImQ0ZTBlZTVlLTk3MTMtNDFkMi05ZmVhLTBiZGIxNDAzMzA4MSJ9.IOd3JIHPwfo9ihU20kvRwLIQJcQtTvp-ajlGqlCD4Es", @@ -83,123 +93,3 @@ func TestParse(t *testing.T) { }) } } - -func TestIsGitLabFederatedToken(t *testing.T) { - tests := []struct { - name string - claims jwt.MapClaims - want bool - }{ - { - name: "empty claims", - claims: jwt.MapClaims{}, - want: false, - }, - { - name: "exactly 10 gitlab", - claims: jwt.MapClaims{ - "namespace_id": "4243", - "namespace_path": "chainloop", - "project_id": "4242", - "project_path": "chainloop/project", - "user_id": "123", - "user_login": "gitlab-ci-token", - "user_email": "ci@gitlab.com", - "pipeline_id": "101", - "job_id": "202", - "ref": "main", - }, - want: true, - }, - { - name: "9 gitlab claims", - claims: jwt.MapClaims{ - "namespace_id": "4243", - "namespace_path": "chainloop", - "project_id": "4242", - "project_path": "chainloop/project", - "user_id": "123", - "user_login": "gitlab-ci-token", - "pipeline_id": "101", - "job_id": "202", - "ref": "main", - }, - want: false, - }, - { - name: "all gitlab claims", - claims: jwt.MapClaims{ - "namespace_id": "4243", - "namespace_path": "chainloop", - "project_id": "4242", - "project_path": "chainloop/project", - "user_id": "123", - "user_login": "gitlab-ci-token", - "user_email": "ci@gitlab.com", - "user_access_level": "developer", - "pipeline_id": "101", - "pipeline_source": "push", - "job_id": "202", - "ref": "main", - "ref_type": "branch", - "ref_protected": true, - "groups_direct": []string{"group1"}, - "environment": "production", - "environment_protected": true, - "deployment_tier": "production", - "deployment_action": "deploy", - "runner_id": "runner-1", - "runner_environment": "production", - "sha": "abc123", - "ci_config_ref_uri": "https://gitlab.com", - "ci_config_sha": "config-abc123", - "project_visibility": "public", - }, - want: true, - }, - { - name: "10 gitlab claims mixed with non-gitlab claims", - claims: jwt.MapClaims{ - "namespace_id": "4243", - "namespace_path": "chainloop", - "project_id": "4242", - "project_path": "chainloop/project", - "user_id": "123", - "user_login": "gitlab-ci-token", - "user_email": "ci@gitlab.com", - "pipeline_id": "101", - "job_id": "202", - "ref": "main", - "custom_claim_1": "value1", - "custom_claim_2": "value2", - "custom_claim_3": "value3", - }, - want: true, - }, - { - name: "9 gitlab claims mixed with non-gitlab claims", - claims: jwt.MapClaims{ - "namespace_id": "4243", - "namespace_path": "chainloop", - "project_id": "4242", - "project_path": "chainloop/project", - "user_id": "123", - "user_login": "gitlab-ci-token", - "pipeline_id": "101", - "job_id": "202", - "ref": "main", - "custom_claim_1": "value1", - "custom_claim_2": "value2", - "custom_claim_3": "value3", - }, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isGitLabFederatedToken(tt.claims) - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index 6b58f5132..346ae3ee0 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -24,6 +24,7 @@ import ( schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc" "github.com/rs/zerolog" ) @@ -63,6 +64,8 @@ type SupportedRunner interface { // attestationViewURL is an optional URL to view the attestation details. // Returns nil if platform doesn't support reporting. Report(tableOutput []byte, attestationViewURL string) error + + FederatedToken() *oidc.Token } type RunnerM map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner diff --git a/pkg/attestation/crafter/runners/azurepipeline.go b/pkg/attestation/crafter/runners/azurepipeline.go index ea5724b2f..3b4ecf3df 100644 --- a/pkg/attestation/crafter/runners/azurepipeline.go +++ b/pkg/attestation/crafter/runners/azurepipeline.go @@ -25,10 +25,14 @@ import ( "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" ) -type AzurePipeline struct{} +type AzurePipeline struct { + *Generic +} func NewAzurePipeline() *AzurePipeline { - return &AzurePipeline{} + return &AzurePipeline{ + NewGeneric(), + } } func (r *AzurePipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType { diff --git a/pkg/attestation/crafter/runners/circleci_build.go b/pkg/attestation/crafter/runners/circleci_build.go index 75f6036d8..c750170c9 100644 --- a/pkg/attestation/crafter/runners/circleci_build.go +++ b/pkg/attestation/crafter/runners/circleci_build.go @@ -23,10 +23,14 @@ import ( "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" ) -type CircleCIBuild struct{} +type CircleCIBuild struct { + *Generic +} func NewCircleCIBuild() *CircleCIBuild { - return &CircleCIBuild{} + return &CircleCIBuild{ + Generic: NewGeneric(), + } } func (r *CircleCIBuild) ID() schemaapi.CraftingSchema_Runner_RunnerType { diff --git a/pkg/attestation/crafter/runners/daggerpipeline.go b/pkg/attestation/crafter/runners/daggerpipeline.go index 56188dd79..bb335d9cd 100644 --- a/pkg/attestation/crafter/runners/daggerpipeline.go +++ b/pkg/attestation/crafter/runners/daggerpipeline.go @@ -20,19 +20,20 @@ import ( "os" "strings" - schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" "github.com/rs/zerolog" ) type DaggerPipeline struct { + *Generic logger *zerolog.Logger } func NewDaggerPipeline(_ string, logger *zerolog.Logger) *DaggerPipeline { return &DaggerPipeline{ - logger: logger, + Generic: NewGeneric(), + logger: logger, } } diff --git a/pkg/attestation/crafter/runners/generic.go b/pkg/attestation/crafter/runners/generic.go index 33c0cb5e4..429bf04ef 100644 --- a/pkg/attestation/crafter/runners/generic.go +++ b/pkg/attestation/crafter/runners/generic.go @@ -20,6 +20,7 @@ import ( schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc" ) type Generic struct{} @@ -69,3 +70,7 @@ func (r *Generic) VerifyCommitSignature(_ context.Context, _ string) *commitveri func (r *Generic) Report(_ []byte, _ string) error { return nil } + +func (r *Generic) FederatedToken() *oidc.Token { + return nil +} diff --git a/pkg/attestation/crafter/runners/githubaction.go b/pkg/attestation/crafter/runners/githubaction.go index ae55c26aa..b56ab4869 100644 --- a/pkg/attestation/crafter/runners/githubaction.go +++ b/pkg/attestation/crafter/runners/githubaction.go @@ -28,6 +28,7 @@ import ( ) type GitHubAction struct { + *Generic githubToken *oidc.Token logger *zerolog.Logger } @@ -38,10 +39,11 @@ func NewGithubAction(ctx context.Context, logger *zerolog.Logger) *GitHubAction // be done we fallback to reading the env vars directly. actorPersonal := fmt.Sprintf("https://github.com/%s", os.Getenv("GITHUB_ACTOR")) actorOrganization := fmt.Sprintf("https://github.com/%s", os.Getenv("GITHUB_REPOSITORY_OWNER")) - client, err := oidc.NewGitHubClient(logger, oidc.WithActor(actorPersonal), oidc.WithActor(actorOrganization)) + client, err := oidc.NewGitHubClient(logger, oidc.WithActor(actorPersonal), oidc.WithActor(actorOrganization), oidc.WithAudience([]string{"chainloop"})) if err != nil { logger.Debug().Err(err).Msg("failed creating GitHub OIDC client") return &GitHubAction{ + Generic: NewGeneric(), githubToken: nil, logger: logger, } @@ -51,6 +53,7 @@ func NewGithubAction(ctx context.Context, logger *zerolog.Logger) *GitHubAction if err != nil { logger.Debug().Err(err).Msg("failed to get github token") return &GitHubAction{ + Generic: NewGeneric(), githubToken: nil, logger: logger, } @@ -60,12 +63,14 @@ func NewGithubAction(ctx context.Context, logger *zerolog.Logger) *GitHubAction if !ok { logger.Debug().Err(err).Msg("failed casting to OIDC token") return &GitHubAction{ + Generic: NewGeneric(), githubToken: nil, logger: logger, } } return &GitHubAction{ + Generic: NewGeneric(), githubToken: ghToken, logger: logger, } @@ -195,3 +200,7 @@ func (r *GitHubAction) Report(tableOutput []byte, attestationViewURL string) err return nil } + +func (r *GitHubAction) FederatedToken() *oidc.Token { + return r.githubToken +} diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index 478b48e0b..1b96eef61 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -27,6 +27,7 @@ import ( ) type GitlabPipeline struct { + *Generic gitlabToken *oidc.GitlabToken logger *zerolog.Logger } @@ -37,12 +38,14 @@ func NewGitlabPipeline(ctx context.Context, authToken string, logger *zerolog.Lo if err != nil { logger.Debug().Err(err).Msgf("failed to create Gitlab OIDC client: %v", err) return &GitlabPipeline{ + Generic: NewGeneric(), gitlabToken: nil, logger: logger, } } return &GitlabPipeline{ + Generic: NewGeneric(), gitlabToken: client.Token, logger: logger, } diff --git a/pkg/attestation/crafter/runners/jenkinsjob.go b/pkg/attestation/crafter/runners/jenkinsjob.go index d95231f67..1cad696e9 100644 --- a/pkg/attestation/crafter/runners/jenkinsjob.go +++ b/pkg/attestation/crafter/runners/jenkinsjob.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,10 +23,14 @@ import ( "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" ) -type JenkinsJob struct{} +type JenkinsJob struct { + *Generic +} func NewJenkinsJob() *JenkinsJob { - return &JenkinsJob{} + return &JenkinsJob{ + Generic: NewGeneric(), + } } func (r *JenkinsJob) ID() schemaapi.CraftingSchema_Runner_RunnerType { diff --git a/pkg/attestation/crafter/runners/teamcitypipeline.go b/pkg/attestation/crafter/runners/teamcitypipeline.go index 1765493ff..9e320e3e9 100644 --- a/pkg/attestation/crafter/runners/teamcitypipeline.go +++ b/pkg/attestation/crafter/runners/teamcitypipeline.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2025-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,10 +23,14 @@ import ( "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" ) -type TeamCityPipeline struct{} +type TeamCityPipeline struct { + *Generic +} func NewTeamCityPipeline() *TeamCityPipeline { - return &TeamCityPipeline{} + return &TeamCityPipeline{ + Generic: NewGeneric(), + } } func (r *TeamCityPipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType { diff --git a/pkg/attestation/crafter/runners/tektonpipeline.go b/pkg/attestation/crafter/runners/tektonpipeline.go index 8518ef58a..96ebe8f58 100644 --- a/pkg/attestation/crafter/runners/tektonpipeline.go +++ b/pkg/attestation/crafter/runners/tektonpipeline.go @@ -60,6 +60,7 @@ type podMetadata struct { // - Tier 1: HOSTNAME env var and SA namespace file (always available in K8s pods) // - Tier 2: K8s API pod labels for rich tekton.dev/* metadata (best-effort) type TektonPipeline struct { + *Generic logger *zerolog.Logger podName string // from HOSTNAME env var namespace string // from /var/run/secrets/kubernetes.io/serviceaccount/namespace @@ -113,6 +114,7 @@ func NewTektonPipeline(ctx context.Context, logger *zerolog.Logger, opts ...Tekt } r := &TektonPipeline{ + Generic: NewGeneric(), logger: logger, labels: make(map[string]string), saTokenPath: defaultSATokenPath, From 9006b744b196ba606b6056ea849f6b2058dc65c1 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Mon, 9 Mar 2026 18:51:21 +0100 Subject: [PATCH 2/3] refactor(runners): simplify FederatedToken to return raw string Change the SupportedRunner.FederatedToken() interface from returning *oidc.Token to returning string directly. This simplifies the API since callers only need the raw JWT token for federated authentication. Add FederatedToken implementation for GitLab pipeline runner and store the raw token in GitlabToken during OIDC parsing. Signed-off-by: Miguel Martinez Entire-Checkpoint: 7d0a0c9cde99 --- app/cli/cmd/root.go | 4 ++-- pkg/attestation/crafter/runner.go | 3 +-- pkg/attestation/crafter/runners/generic.go | 5 ++--- pkg/attestation/crafter/runners/githubaction.go | 7 +++++-- pkg/attestation/crafter/runners/gitlabpipeline.go | 7 +++++++ pkg/attestation/crafter/runners/oidc/gitlab.go | 6 +++++- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 8b249834a..508af7f29 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -124,9 +124,9 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { // If the auth token is not set and the command supports federated auth, we try to discover the runner and use the federated token for the runner if available if authToken == "" && cmdSupportsFederatedAuth(cmd) { r := crafter.DiscoverRunner(authToken, logger) - if r.IsAuthenticated() && r.FederatedToken() != nil { + if r.IsAuthenticated() && r.FederatedToken() != "" { logger.Debug().Str("runner", r.ID().String()).Msg("using federated auth token") - authToken = r.FederatedToken().RawToken + authToken = r.FederatedToken() } } diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index 346ae3ee0..338d23e82 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -24,7 +24,6 @@ import ( schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" - "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc" "github.com/rs/zerolog" ) @@ -65,7 +64,7 @@ type SupportedRunner interface { // Returns nil if platform doesn't support reporting. Report(tableOutput []byte, attestationViewURL string) error - FederatedToken() *oidc.Token + FederatedToken() string } type RunnerM map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner diff --git a/pkg/attestation/crafter/runners/generic.go b/pkg/attestation/crafter/runners/generic.go index 429bf04ef..1e75d14d1 100644 --- a/pkg/attestation/crafter/runners/generic.go +++ b/pkg/attestation/crafter/runners/generic.go @@ -20,7 +20,6 @@ import ( schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/commitverification" - "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc" ) type Generic struct{} @@ -71,6 +70,6 @@ func (r *Generic) Report(_ []byte, _ string) error { return nil } -func (r *Generic) FederatedToken() *oidc.Token { - return nil +func (r *Generic) FederatedToken() string { + return "" } diff --git a/pkg/attestation/crafter/runners/githubaction.go b/pkg/attestation/crafter/runners/githubaction.go index b56ab4869..0c34415ce 100644 --- a/pkg/attestation/crafter/runners/githubaction.go +++ b/pkg/attestation/crafter/runners/githubaction.go @@ -201,6 +201,9 @@ func (r *GitHubAction) Report(tableOutput []byte, attestationViewURL string) err return nil } -func (r *GitHubAction) FederatedToken() *oidc.Token { - return r.githubToken +func (r *GitHubAction) FederatedToken() string { + if r.githubToken == nil { + return "" + } + return r.githubToken.RawToken } diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index 1b96eef61..44ac37e87 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -108,6 +108,13 @@ func (r *GitlabPipeline) IsAuthenticated() bool { return r.gitlabToken != nil } +func (r *GitlabPipeline) FederatedToken() string { + if r.gitlabToken == nil { + return "" + } + return r.gitlabToken.RawToken +} + func (r *GitlabPipeline) Environment() RunnerEnvironment { if r.gitlabToken != nil { switch r.gitlabToken.RunnerEnvironment { diff --git a/pkg/attestation/crafter/runners/oidc/gitlab.go b/pkg/attestation/crafter/runners/oidc/gitlab.go index 7b9f5b81e..8b024b27e 100644 --- a/pkg/attestation/crafter/runners/oidc/gitlab.go +++ b/pkg/attestation/crafter/runners/oidc/gitlab.go @@ -43,6 +43,9 @@ type GitlabToken struct { // RunnerEnvironment is the environment the runner is running in. RunnerEnvironment string `json:"runner_environment"` + + // RawToken is the raw JWT token string used for federated authentication. + RawToken string `json:"-"` } type GitlabOIDCClient struct { @@ -86,7 +89,8 @@ func parseToken(ctx context.Context, providerURL string, tokenString string) (*G } token := &GitlabToken{ - IDToken: *idToken, + IDToken: *idToken, + RawToken: tokenString, } // Verify the audience From e1e907cba24629ea2c7bb5c46af3918aee259b7b Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Mon, 9 Mar 2026 18:55:11 +0100 Subject: [PATCH 3/3] fix(runners): use oidc.ExpectedAudience constant for GitHub OIDC audience Signed-off-by: Miguel Martinez Entire-Checkpoint: b007a5873ce3 --- pkg/attestation/crafter/runners/githubaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/attestation/crafter/runners/githubaction.go b/pkg/attestation/crafter/runners/githubaction.go index 0c34415ce..0ef2aba08 100644 --- a/pkg/attestation/crafter/runners/githubaction.go +++ b/pkg/attestation/crafter/runners/githubaction.go @@ -39,7 +39,7 @@ func NewGithubAction(ctx context.Context, logger *zerolog.Logger) *GitHubAction // be done we fallback to reading the env vars directly. actorPersonal := fmt.Sprintf("https://github.com/%s", os.Getenv("GITHUB_ACTOR")) actorOrganization := fmt.Sprintf("https://github.com/%s", os.Getenv("GITHUB_REPOSITORY_OWNER")) - client, err := oidc.NewGitHubClient(logger, oidc.WithActor(actorPersonal), oidc.WithActor(actorOrganization), oidc.WithAudience([]string{"chainloop"})) + client, err := oidc.NewGitHubClient(logger, oidc.WithActor(actorPersonal), oidc.WithActor(actorOrganization), oidc.WithAudience([]string{oidc.ExpectedAudience})) if err != nil { logger.Debug().Err(err).Msg("failed creating GitHub OIDC client") return &GitHubAction{