From 96e923543bd970f33bf854417d0da2ff04950d9c Mon Sep 17 00:00:00 2001 From: Krishna Pandian Date: Fri, 3 Apr 2026 11:54:29 -0400 Subject: [PATCH 1/2] [OIDC] - Enable Agent Level Max Token Lifetime --- agent/agent_configuration.go | 2 +- agent/job_runner.go | 4 +++ clicommand/agent_start.go | 20 +++++++++++--- clicommand/oidc_request_token.go | 39 ++++++++++++++++++++++++++- clicommand/oidc_request_token_test.go | 34 +++++++++++++++++++++++ 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 clicommand/oidc_request_token_test.go diff --git a/agent/agent_configuration.go b/agent/agent_configuration.go index a82ecef69b..40ec839a75 100644 --- a/agent/agent_configuration.go +++ b/agent/agent_configuration.go @@ -72,6 +72,6 @@ type AgentConfiguration struct { TraceContextEncoding string DisableWarningsFor []string AllowMultipartArtifactUpload bool - + OIDCTokenMaxLifetimeSeconds int PingMode string } diff --git a/agent/job_runner.go b/agent/job_runner.go index ae0c3a9d57..342c10074f 100644 --- a/agent/job_runner.go +++ b/agent/job_runner.go @@ -658,6 +658,10 @@ BUILDKITE_AGENT_JWKS_KEY_ID` setEnv("BUILDKITE_AGENT_DISABLE_WARNINGS_FOR", strings.Join(r.conf.AgentConfiguration.DisableWarningsFor, ",")) + if r.conf.AgentConfiguration.OIDCTokenMaxLifetimeSeconds > 0 { + setEnv("BUILDKITE_AGENT_OIDC_TOKEN_MAX_LIFETIME_SECONDS", strconv.Itoa(r.conf.AgentConfiguration.OIDCTokenMaxLifetimeSeconds)) + } + // see documentation for BuildkiteMessageMax if err := truncateEnv(r.agentLogger, env, BuildkiteMessageName, BuildkiteMessageMax); err != nil { r.agentLogger.Warn("failed to truncate %s: %v", BuildkiteMessageName, err) diff --git a/clicommand/agent_start.go b/clicommand/agent_start.go index c9a4c8fecd..a4a1dd746d 100644 --- a/clicommand/agent_start.go +++ b/clicommand/agent_start.go @@ -195,10 +195,11 @@ type AgentStartConfig struct { TracingPropagateTraceparent bool `cli:"tracing-propagate-traceparent"` // Other shared flags - StrictSingleHooks bool `cli:"strict-single-hooks"` - KubernetesExec bool `cli:"kubernetes-exec"` - TraceContextEncoding string `cli:"trace-context-encoding"` - NoMultipartArtifactUpload bool `cli:"no-multipart-artifact-upload"` + StrictSingleHooks bool `cli:"strict-single-hooks"` + KubernetesExec bool `cli:"kubernetes-exec"` + TraceContextEncoding string `cli:"trace-context-encoding"` + NoMultipartArtifactUpload bool `cli:"no-multipart-artifact-upload"` + OIDCTokenMaxLifetimeSeconds int `cli:"oidc-token-max-lifetime-seconds"` // API + agent behaviour PingMode string `cli:"ping-mode"` @@ -754,6 +755,12 @@ var AgentStartCommand = cli.Command{ StrictSingleHooksFlag, TraceContextEncodingFlag, NoMultipartArtifactUploadFlag, + cli.IntFlag{ + Name: "oidc-token-max-lifetime-seconds", + Value: 0, + Usage: "Maximum lifetime (seconds) for OIDC tokens requested via `buildkite-agent oidc request-token` in jobs. When greater than 0, `--lifetime` cannot exceed this value. 0 means no agent-side limit.", + EnvVar: "BUILDKITE_AGENT_OIDC_TOKEN_MAX_LIFETIME_SECONDS", + }, // Deprecated flags which will be removed in v4 KubernetesLogCollectionGracePeriodFlag, @@ -936,6 +943,10 @@ var AgentStartCommand = cli.Command{ return fmt.Errorf("while parsing trace context encoding: %v", err) } + if cfg.OIDCTokenMaxLifetimeSeconds < 0 { + return fmt.Errorf("oidc-token-max-lifetime-seconds must be a non-negative integer") + } + mc := metrics.NewCollector(l, metrics.CollectorConfig{ Datadog: cfg.MetricsDatadog, DatadogHost: cfg.MetricsDatadogHost, @@ -1078,6 +1089,7 @@ var AgentStartCommand = cli.Command{ TracingPropagateTraceparent: cfg.TracingPropagateTraceparent, TraceContextEncoding: cfg.TraceContextEncoding, AllowMultipartArtifactUpload: !cfg.NoMultipartArtifactUpload, + OIDCTokenMaxLifetimeSeconds: cfg.OIDCTokenMaxLifetimeSeconds, KubernetesExec: cfg.KubernetesExec, PingMode: cfg.PingMode, diff --git a/clicommand/oidc_request_token.go b/clicommand/oidc_request_token.go index b47875ed21..1a0532a0f9 100644 --- a/clicommand/oidc_request_token.go +++ b/clicommand/oidc_request_token.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "os" "slices" + "strconv" "time" "github.com/buildkite/agent/v3/api" @@ -114,6 +116,16 @@ var OIDCRequestTokenCommand = cli.Command{ return fmt.Errorf("format %q is not valid. Supported values are 'jwt' and 'gcp'", cfg.Format) } + lifetime := cfg.Lifetime + const envOIDCTokenMaxLifetimeSeconds = "BUILDKITE_AGENT_OIDC_TOKEN_MAX_LIFETIME_SECONDS" + + if maxSec := parseNonNegativeIntEnv(envOIDCTokenMaxLifetimeSeconds); maxSec > 0 { + if eff, clamped := clampOIDCTokenLifetimeSeconds(lifetime, maxSec); clamped { + l.Debug("OIDC token lifetime clamped from %ds to %ds (%s=%d)", lifetime, eff, envOIDCTokenMaxLifetimeSeconds, maxSec) + lifetime = eff + } + } + // Create the API client client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken")) @@ -126,7 +138,7 @@ var OIDCRequestTokenCommand = cli.Command{ req := &api.OIDCTokenRequest{ Job: cfg.Job, Audience: cfg.Audience, - Lifetime: cfg.Lifetime, + Lifetime: lifetime, Claims: cfg.Claims, AWSSessionTags: cfg.AWSSessionTags, SubjectClaim: cfg.SubjectClaim, @@ -202,3 +214,28 @@ var OIDCRequestTokenCommand = cli.Command{ return nil }, } + +// parseNonNegativeIntEnv returns 0 if the variable is unset, invalid, or negative. +func parseNonNegativeIntEnv(key string) int { + s := os.Getenv(key) + if s == "" { + return 0 + } + n, err := strconv.Atoi(s) + if err != nil || n < 0 { + return 0 + } + return n +} + +// clampOIDCTokenLifetimeSeconds returns the effective lifetime and whether +// clamping occurred. When requested is 0 (API default), it is not changed. +func clampOIDCTokenLifetimeSeconds(requested, maxLifetime int) (effective int, clamped bool) { + if maxLifetime <= 0 || requested <= 0 { + return requested, false + } + if requested > maxLifetime { + return maxLifetime, true + } + return requested, false +} diff --git a/clicommand/oidc_request_token_test.go b/clicommand/oidc_request_token_test.go new file mode 100644 index 0000000000..5ba93d50fa --- /dev/null +++ b/clicommand/oidc_request_token_test.go @@ -0,0 +1,34 @@ +package clicommand + +import ( + "testing" +) + +func TestClampOIDCTokenLifetimeSeconds(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + requested int + maxLifetime int + wantEff int + wantClamp bool + }{ + {"no cap", 600, 0, 600, false}, + {"no cap negative max", 600, -1, 600, false}, + {"api default unchanged", 0, 300, 0, false}, + {"under cap", 100, 300, 100, false}, + {"at cap", 300, 300, 300, false}, + {"over cap", 600, 300, 300, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, clamped := clampOIDCTokenLifetimeSeconds(tt.requested, tt.maxLifetime) + if got != tt.wantEff || clamped != tt.wantClamp { + t.Errorf("clampOIDCTokenLifetimeSeconds(%d, %d) = (%d, %v), want (%d, %v)", + tt.requested, tt.maxLifetime, got, clamped, tt.wantEff, tt.wantClamp) + } + }) + } +} From 3f916951cc6bd8ddcfaa45e68fc195ca4b295978 Mon Sep 17 00:00:00 2001 From: Krishna Pandian Date: Sun, 5 Apr 2026 15:19:26 -0400 Subject: [PATCH 2/2] lint --- agent/agent_configuration.go | 4 ++-- clicommand/agent_start.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/agent/agent_configuration.go b/agent/agent_configuration.go index 40ec839a75..8bd69b838d 100644 --- a/agent/agent_configuration.go +++ b/agent/agent_configuration.go @@ -72,6 +72,6 @@ type AgentConfiguration struct { TraceContextEncoding string DisableWarningsFor []string AllowMultipartArtifactUpload bool - OIDCTokenMaxLifetimeSeconds int - PingMode string + OIDCTokenMaxLifetimeSeconds int + PingMode string } diff --git a/clicommand/agent_start.go b/clicommand/agent_start.go index a4a1dd746d..a8712e282c 100644 --- a/clicommand/agent_start.go +++ b/clicommand/agent_start.go @@ -195,11 +195,11 @@ type AgentStartConfig struct { TracingPropagateTraceparent bool `cli:"tracing-propagate-traceparent"` // Other shared flags - StrictSingleHooks bool `cli:"strict-single-hooks"` - KubernetesExec bool `cli:"kubernetes-exec"` - TraceContextEncoding string `cli:"trace-context-encoding"` - NoMultipartArtifactUpload bool `cli:"no-multipart-artifact-upload"` - OIDCTokenMaxLifetimeSeconds int `cli:"oidc-token-max-lifetime-seconds"` + StrictSingleHooks bool `cli:"strict-single-hooks"` + KubernetesExec bool `cli:"kubernetes-exec"` + TraceContextEncoding string `cli:"trace-context-encoding"` + NoMultipartArtifactUpload bool `cli:"no-multipart-artifact-upload"` + OIDCTokenMaxLifetimeSeconds int `cli:"oidc-token-max-lifetime-seconds"` // API + agent behaviour PingMode string `cli:"ping-mode"` @@ -1089,7 +1089,7 @@ var AgentStartCommand = cli.Command{ TracingPropagateTraceparent: cfg.TracingPropagateTraceparent, TraceContextEncoding: cfg.TraceContextEncoding, AllowMultipartArtifactUpload: !cfg.NoMultipartArtifactUpload, - OIDCTokenMaxLifetimeSeconds: cfg.OIDCTokenMaxLifetimeSeconds, + OIDCTokenMaxLifetimeSeconds: cfg.OIDCTokenMaxLifetimeSeconds, KubernetesExec: cfg.KubernetesExec, PingMode: cfg.PingMode,