Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/agent_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ type AgentConfiguration struct {
TraceContextEncoding string
DisableWarningsFor []string
AllowMultipartArtifactUpload bool

PingMode string
OIDCTokenMaxLifetimeSeconds int
PingMode string
}
4 changes: 4 additions & 0 deletions agent/job_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 16 additions & 4 deletions clicommand/agent_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,

Expand Down
39 changes: 38 additions & 1 deletion clicommand/oidc_request_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"
"slices"
"strconv"
"time"

"github.com/buildkite/agent/v3/api"
Expand Down Expand Up @@ -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"))

Expand All @@ -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,
Expand Down Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions clicommand/oidc_request_token_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}