From 6a40e1a1dcc2f516b4c1d987d15b327d4eb39dc6 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Tue, 6 Oct 2026 14:13:28 -0400 Subject: [PATCH 01/17] controller: add SPIFFE JWT-SVID token provider Add a tokenprovider.TokenProvider backed by the SPIFFE Workload API. It fetches a JWT-SVID for a fixed audience; the socket address is passed explicitly or falls back to SPIFFE_ENDPOINT_SOCKET. First step of #2791. Not wired into any rotator yet. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- go.mod | 2 +- .../tokenprovider/spiffe_token_provider.go | 58 +++++++ .../spiffe_token_provider_test.go | 162 ++++++++++++++++++ 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 internal/controller/tokenprovider/spiffe_token_provider.go create mode 100644 internal/controller/tokenprovider/spiffe_token_provider_test.go diff --git a/go.mod b/go.mod index 0c0c48b9f6..c507c7c8af 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/prometheus/client_golang v1.24.1 github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.70.1 + github.com/spiffe/go-spiffe/v2 v2.8.1 github.com/stretchr/testify v1.12.1 github.com/testcontainers/testcontainers-go v0.43.0 github.com/tetratelabs/func-e v1.6.0 @@ -215,7 +216,6 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect github.com/telepresenceio/watchable v0.0.0-20220726211108-9bb86f92afa7 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/internal/controller/tokenprovider/spiffe_token_provider.go b/internal/controller/tokenprovider/spiffe_token_provider.go new file mode 100644 index 0000000000..7a2ecd8a0c --- /dev/null +++ b/internal/controller/tokenprovider/spiffe_token_provider.go @@ -0,0 +1,58 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package tokenprovider + +import ( + "context" + "errors" + "fmt" + + "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" + "github.com/spiffe/go-spiffe/v2/workloadapi" +) + +// jwtSVIDFetcher fetches a JWT-SVID for the given audience from the SPIFFE Workload API. +type jwtSVIDFetcher func(ctx context.Context, audience string) (TokenExpiry, error) + +// spiffeTokenProvider is a provider implements TokenProvider interface for SPIFFE JWT-SVIDs. +type spiffeTokenProvider struct { + audience string + fetch jwtSVIDFetcher +} + +// NewSPIFFETokenProvider creates a new TokenProvider that fetches JWT-SVIDs with the given audience +// from the SPIFFE Workload API. +// +// socketAddr is the Workload API address, e.g. "unix:///run/spire/sockets/agent.sock". When empty, +// the SPIFFE_ENDPOINT_SOCKET environment variable is used. +func NewSPIFFETokenProvider(socketAddr, audience string) (TokenProvider, error) { + if audience == "" { + return nil, errors.New("SPIFFE JWT-SVID audience must not be empty") + } + var opts []workloadapi.ClientOption + if socketAddr != "" { + opts = append(opts, workloadapi.WithAddr(socketAddr)) + } + return &spiffeTokenProvider{ + audience: audience, + fetch: func(ctx context.Context, audience string) (TokenExpiry, error) { + svid, err := workloadapi.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience}, opts...) + if err != nil { + return TokenExpiry{}, err + } + return TokenExpiry{Token: svid.Marshal(), ExpiresAt: svid.Expiry}, nil + }, + }, nil +} + +// GetToken implements TokenProvider.GetToken method to retrieve a JWT-SVID and its expiration time. +func (s *spiffeTokenProvider) GetToken(ctx context.Context) (TokenExpiry, error) { + token, err := s.fetch(ctx, s.audience) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to fetch JWT-SVID from SPIFFE Workload API: %w", err) + } + return token, nil +} diff --git a/internal/controller/tokenprovider/spiffe_token_provider_test.go b/internal/controller/tokenprovider/spiffe_token_provider_test.go new file mode 100644 index 0000000000..cd7399701d --- /dev/null +++ b/internal/controller/tokenprovider/spiffe_token_provider_test.go @@ -0,0 +1,162 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package tokenprovider + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const testSPIFFEID = "spiffe://example.org/ns/envoy-ai-gateway-system/sa/ai-gateway-controller" + +func TestNewSPIFFETokenProvider(t *testing.T) { + t.Run("empty audience", func(t *testing.T) { + provider, err := NewSPIFFETokenProvider("unix:///tmp/agent.sock", "") + require.ErrorContains(t, err, "audience must not be empty") + require.Nil(t, provider) + }) + + t.Run("valid", func(t *testing.T) { + provider, err := NewSPIFFETokenProvider("unix:///tmp/agent.sock", "openai") + require.NoError(t, err) + require.IsType(t, &spiffeTokenProvider{}, provider) + }) + + t.Run("empty socket address", func(t *testing.T) { + provider, err := NewSPIFFETokenProvider("", "openai") + require.NoError(t, err) + require.IsType(t, &spiffeTokenProvider{}, provider) + }) +} + +func TestSPIFFETokenProvider_GetToken(t *testing.T) { + t.Run("success", func(t *testing.T) { + expiresAt := time.Now().Add(5 * time.Minute) + var gotAudience string + provider := &spiffeTokenProvider{ + audience: "openai", + fetch: func(_ context.Context, audience string) (TokenExpiry, error) { + gotAudience = audience + return TokenExpiry{Token: "svid", ExpiresAt: expiresAt}, nil + }, + } + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, "openai", gotAudience) + require.Equal(t, TokenExpiry{Token: "svid", ExpiresAt: expiresAt}, token) + }) + + t.Run("fetch error", func(t *testing.T) { + provider := &spiffeTokenProvider{ + audience: "openai", + fetch: func(context.Context, string) (TokenExpiry, error) { + return TokenExpiry{}, errors.New("connection refused") + }, + } + _, err := provider.GetToken(t.Context()) + require.ErrorContains(t, err, "failed to fetch JWT-SVID from SPIFFE Workload API: connection refused") + }) +} + +func TestSPIFFETokenProvider_GetToken_WorkloadAPI(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + expiresAt := time.Now().Add(5 * time.Minute).Truncate(time.Second).UTC() + + server := &fakeWorkloadAPIServer{key: key, expiresAt: expiresAt} + addr := startFakeWorkloadAPI(t, server) + + t.Run("explicit socket address", func(t *testing.T) { + provider, err := NewSPIFFETokenProvider(addr, "openai") + require.NoError(t, err) + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, expiresAt, token.ExpiresAt) + require.Equal(t, []string{"openai"}, server.lastAudience) + + claims := jwt.RegisteredClaims{} + _, err = jwt.ParseWithClaims(token.Token, &claims, func(*jwt.Token) (any, error) { return &key.PublicKey, nil }) + require.NoError(t, err) + require.Equal(t, testSPIFFEID, claims.Subject) + require.Equal(t, jwt.ClaimStrings{"openai"}, claims.Audience) + }) + + t.Run("socket address from SPIFFE_ENDPOINT_SOCKET", func(t *testing.T) { + t.Setenv("SPIFFE_ENDPOINT_SOCKET", addr) + provider, err := NewSPIFFETokenProvider("", "other-audience") + require.NoError(t, err) + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.NotEmpty(t, token.Token) + require.Equal(t, []string{"other-audience"}, server.lastAudience) + }) + + t.Run("workload API error", func(t *testing.T) { + provider, err := NewSPIFFETokenProvider(addr, "denied") + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.ErrorContains(t, err, "failed to fetch JWT-SVID from SPIFFE Workload API") + require.ErrorContains(t, err, "no identity issued") + }) +} + +// fakeWorkloadAPIServer issues JWT-SVIDs signed by key for testSPIFFEID. +type fakeWorkloadAPIServer struct { + workload.UnimplementedSpiffeWorkloadAPIServer + key *ecdsa.PrivateKey + expiresAt time.Time + lastAudience []string +} + +func (f *fakeWorkloadAPIServer) FetchJWTSVID(_ context.Context, req *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { + f.lastAudience = req.Audience + if len(req.Audience) == 1 && req.Audience[0] == "denied" { + return nil, status.Error(codes.PermissionDenied, "no identity issued") + } + token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.RegisteredClaims{ + Subject: testSPIFFEID, + Audience: req.Audience, + ExpiresAt: jwt.NewNumericDate(f.expiresAt), + }) + signed, err := token.SignedString(f.key) + if err != nil { + return nil, err + } + return &workload.JWTSVIDResponse{Svids: []*workload.JWTSVID{{SpiffeId: testSPIFFEID, Svid: signed}}}, nil +} + +// startFakeWorkloadAPI serves the given server on a unix socket and returns its address. +func startFakeWorkloadAPI(t *testing.T, server workload.SpiffeWorkloadAPIServer) string { + t.Helper() + // Use a short directory since unix socket paths are limited to ~104 bytes on macOS. + dir, err := os.MkdirTemp("", "spiffe") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(dir) }) + socketPath := filepath.Join(dir, "agent.sock") + + lis, err := net.Listen("unix", socketPath) + require.NoError(t, err) + s := grpc.NewServer() + workload.RegisterSpiffeWorkloadAPIServer(s, server) + go func() { _ = s.Serve(lis) }() + t.Cleanup(s.Stop) + return "unix://" + socketPath +} From b5c1835d671779179e4ada854796666309ea34f0 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Tue, 6 Oct 2026 16:09:55 -0400 Subject: [PATCH 02/17] api: add OpenAICredentials BackendSecurityPolicy Add an OpenAICredentials type to v1beta1 for secretless access to OpenAI via OAuth 2.0 Token Exchange (RFC 8693). It carries optional organization/project IDs and a tokenExchange block (tokenURL, subjectTokenType defaulting to ...:token-type:jwt, audience, scopes). The subject token comes from exactly one of an OIDC client-credentials flow or a SPIFFE JWT-SVID (audience plus optional socketPath). v1beta1 only, following the CredentialOverride precedent. Part of #2791; the controller does not act on the new type yet. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 123 +- api/v1beta1/zz_generated.deepcopy.go | 82 + ...envoyproxy.io_backendsecuritypolicies.yaml | 2048 ++++++++++++++++- site/docs/api/api.mdx | 144 ++ tests/crdcel/main_test.go | 41 + .../apikey_with_openai_credentials.yaml | 21 + .../openai_invalid_socket_path.yaml | 19 + .../openai_invalid_token_url.yaml | 18 + .../openai_missing_subject_token.yaml | 16 + .../openai_multiple_subject_tokens.yaml | 24 + .../backendsecuritypolicies/openai_oidc.yaml | 23 + .../openai_spiffe.yaml | 24 + .../openai_with_apikey.yaml | 21 + 13 files changed, 2585 insertions(+), 19 deletions(-) create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 900bdf3918..7bd89f24dc 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -16,12 +16,13 @@ import ( type BackendSecurityPolicyType string const ( - BackendSecurityPolicyTypeAPIKey BackendSecurityPolicyType = "APIKey" - BackendSecurityPolicyTypeAWSCredentials BackendSecurityPolicyType = "AWSCredentials" - BackendSecurityPolicyTypeAzureAPIKey BackendSecurityPolicyType = "AzureAPIKey" - BackendSecurityPolicyTypeAnthropicAPIKey BackendSecurityPolicyType = "AnthropicAPIKey" // #nosec G101 - BackendSecurityPolicyTypeAzureCredentials BackendSecurityPolicyType = "AzureCredentials" - BackendSecurityPolicyTypeGCPCredentials BackendSecurityPolicyType = "GCPCredentials" + BackendSecurityPolicyTypeAPIKey BackendSecurityPolicyType = "APIKey" + BackendSecurityPolicyTypeAWSCredentials BackendSecurityPolicyType = "AWSCredentials" + BackendSecurityPolicyTypeAzureAPIKey BackendSecurityPolicyType = "AzureAPIKey" + BackendSecurityPolicyTypeAnthropicAPIKey BackendSecurityPolicyType = "AnthropicAPIKey" // #nosec G101 + BackendSecurityPolicyTypeAzureCredentials BackendSecurityPolicyType = "AzureCredentials" + BackendSecurityPolicyTypeGCPCredentials BackendSecurityPolicyType = "GCPCredentials" + BackendSecurityPolicyTypeOpenAICredentials BackendSecurityPolicyType = "OpenAICredentials" // #nosec G101 ) // BackendSecurityPolicy specifies configuration for authentication and authorization rules on the traffic @@ -47,12 +48,13 @@ type BackendSecurityPolicy struct { // // Only one type of BackendSecurityPolicy can be defined. // +kubebuilder:validation:MaxProperties=4 -// +kubebuilder:validation:XValidation:rule="self.type == 'APIKey' ? (has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is APIKey, only apiKey field should be set" -// +kubebuilder:validation:XValidation:rule="self.type == 'AWSCredentials' ? (has(self.awsCredentials) && !has(self.apiKey) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AWSCredentials, only awsCredentials field should be set" -// +kubebuilder:validation:XValidation:rule="self.type == 'AzureAPIKey' ? (has(self.azureAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AzureAPIKey, only azureAPIKey field should be set" -// +kubebuilder:validation:XValidation:rule="self.type == 'AzureCredentials' ? (has(self.azureCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is AzureCredentials, only azureCredentials field should be set" -// +kubebuilder:validation:XValidation:rule="self.type == 'GCPCredentials' ? (has(self.gcpCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is GCPCredentials, only gcpCredentials field should be set" -// +kubebuilder:validation:XValidation:rule="self.type == 'AnthropicAPIKey' ? (has(self.anthropicAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true",message="When type is AnthropicAPIKey, only anthropicAPIKey field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'APIKey' ? (has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true",message="When type is APIKey, only apiKey field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'AWSCredentials' ? (has(self.awsCredentials) && !has(self.apiKey) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true",message="When type is AWSCredentials, only awsCredentials field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'AzureAPIKey' ? (has(self.azureAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true",message="When type is AzureAPIKey, only azureAPIKey field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'AzureCredentials' ? (has(self.azureCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true",message="When type is AzureCredentials, only azureCredentials field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'GCPCredentials' ? (has(self.gcpCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true",message="When type is GCPCredentials, only gcpCredentials field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'AnthropicAPIKey' ? (has(self.anthropicAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.openAICredentials)) : true",message="When type is AnthropicAPIKey, only anthropicAPIKey field should be set" +// +kubebuilder:validation:XValidation:rule="self.type == 'OpenAICredentials' ? (has(self.openAICredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true",message="When type is OpenAICredentials, only openAICredentials field should be set" type BackendSecurityPolicySpec struct { // TargetRefs are the names of the AIServiceBackend or InferencePool resources this BackendSecurityPolicy is being attached to. // Attaching multiple BackendSecurityPolicies to the same resource is invalid and will result in an error @@ -65,7 +67,7 @@ type BackendSecurityPolicySpec struct { // Type specifies the type of the backend security policy. // - // +kubebuilder:validation:Enum=APIKey;AWSCredentials;AzureAPIKey;AzureCredentials;GCPCredentials;AnthropicAPIKey + // +kubebuilder:validation:Enum=APIKey;AWSCredentials;AzureAPIKey;AzureCredentials;GCPCredentials;AnthropicAPIKey;OpenAICredentials Type BackendSecurityPolicyType `json:"type"` // APIKey is a mechanism to access a backend(s). The API key will be injected into the Authorization header. @@ -99,6 +101,12 @@ type BackendSecurityPolicySpec struct { // +optional AnthropicAPIKey *BackendSecurityPolicyAnthropicAPIKey `json:"anthropicAPIKey,omitempty"` + // OpenAICredentials is a mechanism to access OpenAI backend(s) without a static API key. The controller + // exchanges a subject token for a short-lived access token, which is injected into the Authorization header. + // + // +optional + OpenAICredentials *BackendSecurityPolicyOpenAICredentials `json:"openAICredentials,omitempty"` + // CredentialOverride, when set, sources the upstream credential per-request instead of using // the static credential configured above. // @@ -347,6 +355,95 @@ type BackendSecurityPolicyAnthropicAPIKey struct { SecretRef *gwapiv1.SecretObjectReference `json:"secretRef"` } +// BackendSecurityPolicyOpenAICredentials contains the supported authentication mechanisms to access OpenAI. +type BackendSecurityPolicyOpenAICredentials struct { + // Organization is the OpenAI organization ID. When set, it is sent in the "OpenAI-Organization" header. + // + // +optional + // +kubebuilder:validation:MinLength=1 + Organization string `json:"organization,omitempty"` + + // Project is the OpenAI project ID. When set, it is sent in the "OpenAI-Project" header. + // + // +optional + // +kubebuilder:validation:MinLength=1 + Project string `json:"project,omitempty"` + + // TokenExchange specifies how the controller obtains an access token via OAuth 2.0 Token Exchange. + // + // +kubebuilder:validation:Required + TokenExchange BackendSecurityPolicyTokenExchange `json:"tokenExchange"` +} + +// BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. +// The controller obtains a subject token, exchanges it at TokenURL for an access token, +// and stores the access token in a secret, rotating it before it expires. +// +// https://datatracker.ietf.org/doc/html/rfc8693 +type BackendSecurityPolicyTokenExchange struct { + // TokenURL is the token exchange endpoint. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:XValidation:rule="isURL(self)",message="tokenURL must be a valid URL" + TokenURL string `json:"tokenURL"` + + // SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token. + // + // +optional + // +kubebuilder:default="urn:ietf:params:oauth:token-type:jwt" + // +kubebuilder:validation:MinLength=1 + SubjectTokenType string `json:"subjectTokenType,omitempty"` + + // Audience is the RFC 8693 audience parameter: the logical name of the target service. + // + // +optional + Audience string `json:"audience,omitempty"` + + // Scopes is the list of scopes requested for the access token. + // + // +optional + // +kubebuilder:validation:MaxItems=16 + Scopes []string `json:"scopes,omitempty"` + + // SubjectToken specifies where the subject token is obtained from. + // + // +kubebuilder:validation:Required + SubjectToken BackendSecurityPolicySubjectToken `json:"subjectToken"` +} + +// BackendSecurityPolicySubjectToken specifies the source of a subject token. +// Exactly one source must be configured. +// +// +kubebuilder:validation:XValidation:rule="has(self.oidc) != has(self.spiffe)",message="Exactly one of oidc or spiffe must be specified" +type BackendSecurityPolicySubjectToken struct { + // OIDC obtains the subject token via an OIDC client-credentials flow. + // + // +optional + OIDC *egv1a1.OIDC `json:"oidc,omitempty"` + + // SPIFFE obtains the subject token as a JWT-SVID from the SPIFFE Workload API. + // + // +optional + SPIFFE *BackendSecurityPolicySPIFFE `json:"spiffe,omitempty"` +} + +// BackendSecurityPolicySPIFFE specifies how to obtain a JWT-SVID from the SPIFFE Workload API. +type BackendSecurityPolicySPIFFE struct { + // Audience is the audience of the requested JWT-SVID. + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Audience string `json:"audience"` + + // SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". + // When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. + // + // +optional + // +kubebuilder:validation:Pattern=`^(unix|tcp)://.+` + SocketPath string `json:"socketPath,omitempty"` +} + // BackendSecurityPolicyCredentialOverride configures per-request credential sourcing. // A trusted upstream filter (ext_authz or a preceding ext_proc) resolves the caller's // credential and delivers it via one of the two sources below. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 702b47c414..8b3f6073a8 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -666,6 +666,37 @@ func (in *BackendSecurityPolicyOIDC) DeepCopy() *BackendSecurityPolicyOIDC { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSecurityPolicyOpenAICredentials) DeepCopyInto(out *BackendSecurityPolicyOpenAICredentials) { + *out = *in + in.TokenExchange.DeepCopyInto(&out.TokenExchange) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyOpenAICredentials. +func (in *BackendSecurityPolicyOpenAICredentials) DeepCopy() *BackendSecurityPolicyOpenAICredentials { + if in == nil { + return nil + } + out := new(BackendSecurityPolicyOpenAICredentials) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSecurityPolicySPIFFE) DeepCopyInto(out *BackendSecurityPolicySPIFFE) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySPIFFE. +func (in *BackendSecurityPolicySPIFFE) DeepCopy() *BackendSecurityPolicySPIFFE { + if in == nil { + return nil + } + out := new(BackendSecurityPolicySPIFFE) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendSecurityPolicySpec) DeepCopyInto(out *BackendSecurityPolicySpec) { *out = *in @@ -704,6 +735,11 @@ func (in *BackendSecurityPolicySpec) DeepCopyInto(out *BackendSecurityPolicySpec *out = new(BackendSecurityPolicyAnthropicAPIKey) (*in).DeepCopyInto(*out) } + if in.OpenAICredentials != nil { + in, out := &in.OpenAICredentials, &out.OpenAICredentials + *out = new(BackendSecurityPolicyOpenAICredentials) + (*in).DeepCopyInto(*out) + } if in.CredentialOverride != nil { in, out := &in.CredentialOverride, &out.CredentialOverride *out = new(BackendSecurityPolicyCredentialOverride) @@ -743,6 +779,52 @@ func (in *BackendSecurityPolicyStatus) DeepCopy() *BackendSecurityPolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSecurityPolicySubjectToken) DeepCopyInto(out *BackendSecurityPolicySubjectToken) { + *out = *in + if in.OIDC != nil { + in, out := &in.OIDC, &out.OIDC + *out = new(v1alpha1.OIDC) + (*in).DeepCopyInto(*out) + } + if in.SPIFFE != nil { + in, out := &in.SPIFFE, &out.SPIFFE + *out = new(BackendSecurityPolicySPIFFE) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySubjectToken. +func (in *BackendSecurityPolicySubjectToken) DeepCopy() *BackendSecurityPolicySubjectToken { + if in == nil { + return nil + } + out := new(BackendSecurityPolicySubjectToken) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { + *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.SubjectToken.DeepCopyInto(&out.SubjectToken) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyTokenExchange. +func (in *BackendSecurityPolicyTokenExchange) DeepCopy() *BackendSecurityPolicyTokenExchange { + if in == nil { + return nil + } + out := new(BackendSecurityPolicyTokenExchange) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialOverrideFromDynamicMetadata) DeepCopyInto(out *CredentialOverrideFromDynamicMetadata) { *out = *in diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index d4d399754c..4728a907ef 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -12824,6 +12824,2031 @@ spec: - message: At most one of credentialsFile or workloadIdentityFederationConfig may be specified rule: '!(has(self.credentialsFile) && has(self.workloadIdentityFederationConfig))' + openAICredentials: + description: |- + OpenAICredentials is a mechanism to access OpenAI backend(s) without a static API key. The controller + exchanges a subject token for a short-lived access token, which is injected into the Authorization header. + properties: + organization: + description: Organization is the OpenAI organization ID. When + set, it is sent in the "OpenAI-Organization" header. + minLength: 1 + type: string + project: + description: Project is the OpenAI project ID. When set, it is + sent in the "OpenAI-Project" header. + minLength: 1 + type: string + tokenExchange: + description: TokenExchange specifies how the controller obtains + an access token via OAuth 2.0 Token Exchange. + properties: + audience: + description: 'Audience is the RFC 8693 audience parameter: + the logical name of the target service.' + type: string + scopes: + description: Scopes is the list of scopes requested for the + access token. + items: + type: string + maxItems: 16 + type: array + subjectToken: + description: SubjectToken specifies where the subject token + is obtained from. + properties: + oidc: + description: OIDC obtains the subject token via an OIDC + client-credentials flow. + properties: + clientID: + description: |- + The client ID to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + Only one of clientID or clientIDRef must be set. + minLength: 1 + type: string + clientIDRef: + description: |- + The Kubernetes secret which contains the client ID to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Exactly one of clientID or clientIDRef must be set. + This is an Opaque secret. The client ID should be stored in the key "client-id". + + Only one of clientID or clientIDRef must be set. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For + example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + clientSecret: + description: |- + The Kubernetes secret which contains the OIDC client secret to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + This is an Opaque secret. The client secret should be stored in the key + "client-secret". + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For + example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + cookieConfig: + description: |- + CookieConfigs allows setting the SameSite attribute for OIDC cookies. + By default, its unset. + properties: + sameSite: + enum: + - Lax + - Strict + - None + type: string + type: object + cookieDomain: + description: |- + The optional domain to set the access and ID token cookies on. + If not set, the cookies will default to the host of the request, not including the subdomains. + If set, the cookies will be set on the specified domain and all subdomains. + This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + cookieNames: + description: |- + The optional cookie name overrides to be used for Bearer and IdToken cookies in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses a randomly generated suffix + properties: + accessToken: + description: |- + The name of the cookie used to store the AccessToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "AccessToken-(randomly generated uid)" + type: string + idToken: + description: |- + The name of the cookie used to store the IdToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "IdToken-(randomly generated uid)" + type: string + type: object + csrfTokenTTL: + description: |- + CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. + + This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token + in the "state" parameter when the provider redirects back to the callback endpoint. + + If omitted, Envoy Gateway defaults the token expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + defaultRefreshTokenTTL: + description: |- + DefaultRefreshTokenTTL is the default lifetime of the refresh token. + This field is only used when the exp (expiration time) claim is omitted in + the refresh token or the refresh token is not JWT. + + If not specified, defaults to 604800s (one week). + Note: this field is only applicable when the "refreshToken" field is set to true. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + defaultTokenTTL: + description: |- + DefaultTokenTTL is the default lifetime of the id token and access token. + Please note that Envoy will always use the expiry time from the response + of the authorization server if it is provided. This field is only used when + the expiry time is not provided by the authorization. + + If not specified, defaults to 0. In this case, the "expires_in" field in + the authorization response must be set by the authorization server, or the + OAuth flow will fail. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + denyRedirect: + description: |- + Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. + This behavior can be useful for AJAX or machine requests. + properties: + headers: + description: Defines the headers to match against + the request to deny redirect to the OIDC Provider. + items: + description: OIDCDenyRedirectHeader defines + how a header is matched + properties: + name: + description: Specifies the name of the header + in the request. + minLength: 1 + type: string + type: + default: Exact + description: Type specifies how to match + against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string + value that the match must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - headers + type: object + disableTokenEncryption: + description: |- + Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. + This option should only be used in secure environments where token encryption is not required. + Default is false (tokens are encrypted). + type: boolean + forwardAccessToken: + description: |- + ForwardAccessToken indicates whether the Envoy should forward the access token + via the Authorization header Bearer scheme to the upstream. + If not specified, defaults to false. + type: boolean + forwardIDToken: + description: |- + ForwardIDToken configures forwarding of the OIDC ID token to the upstream. + + If the configured header is "Authorization", EG forwards the ID token using + the "Bearer " prefix. For any other header, EG forwards the raw token value. + If not specified, the ID token will not be forwarded. + + Note: when passThroughAuthHeader is enabled, this header must not be the same + as a header a JWT provider extracts from (the "Authorization" header by + default). The forwarded ID token header is owned by Envoy, and Envoy rejects + an OAuth2 configuration whose pass-through matcher keys on it. + properties: + header: + description: |- + Header is the upstream request header that will carry the ID token. + It must be a valid HTTP header name. Pseudo-headers (names starting with ":") + and the "Host" header are not allowed. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + x-kubernetes-validations: + - message: header cannot be the Host header + rule: self.lowerAscii() != 'host' + required: + - header + type: object + logoutPath: + description: |- + The path to log a user out, clearing their credential cookies. + + If not specified, uses a default logout path "/logout" + type: string + passThroughAuthHeader: + description: |- + Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless + explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. + + The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC + redirects and wish to directly supply a token instead. + + If not specified, defaults to false. + type: boolean + provider: + description: The OIDC Provider configuration. + properties: + authorizationEndpoint: + description: |- + The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string + backendRef: + description: |- + BackendRef references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + + Deprecated: Use BackendRefs instead. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == + ''Service'') ? has(self.port) : true' + backendRefs: + description: |- + BackendRefs references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + items: + description: BackendRef defines how an ObjectReference + that is specific to BackendRef. + properties: + fallback: + description: |- + Fallback indicates whether the backend is designated as a fallback. + Multiple fallback backends can be configured. + It is highly recommended to configure active or passive health checks to ensure that failover can be detected + when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + the health of the active backends falls below 72%. + type: boolean + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + maxItems: 16 + type: array + backendSettings: + description: |- + BackendSettings holds configuration for managing the connection + to the backend. + properties: + circuitBreaker: + description: |- + Circuit Breaker settings for the upstream connections and requests. + If not set, circuit breakers will be enabled with the default thresholds + properties: + maxConnections: + default: 1024 + description: The maximum number of connections + that Envoy will establish to the referenced + backend defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRequests: + default: 1024 + description: The maximum number of parallel + requests that Envoy will make to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRetries: + default: 1024 + description: The maximum number of parallel + retries that Envoy will make to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxPendingRequests: + default: 1024 + description: The maximum number of pending + requests that Envoy will queue to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxRequestsPerConnection: + description: |- + The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. + Default: unlimited. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + perEndpoint: + description: PerEndpoint defines Circuit + Breakers that will apply per-endpoint + for an upstream cluster + properties: + maxConnections: + default: 1024 + description: MaxConnections configures + the maximum number of connections + that Envoy will establish per-endpoint + to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + type: object + retryBudget: + description: |- + RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. + RetryBudget take precedence over maxParallelRetries. + properties: + minRetryConcurrency: + description: |- + MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. + For example, a budget of 20% with a minimum retry concurrency of 3 + will allow 5 active retries while there are 25 active requests. + If there are 2 active requests, there are still 3 active retries + allowed because of the minimum retry concurrency. + Defaults to 3. + format: int32 + type: integer + percent: + description: |- + Percent specifies the limit on concurrent retries as a percentage [0, 100] of + the sum of active requests and active pending requests. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less + than or equal to denominator + rule: self.numerator <= self.denominator + required: + - percent + type: object + type: object + connection: + description: Connection includes backend connection + settings. + properties: + bufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + BufferLimit Soft limit on size of the cluster’s connections read and write buffers. + BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. + If unspecified, an implementation defined default is applied (32768 bytes). + For example, 20Mi, 1Gi, 256Ki etc. + Note: that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + preconnect: + description: |- + Preconnect configures proactive upstream connections to reduce latency by establishing + connections before they’re needed and avoiding connection establishment overhead. + + If unset, Envoy will fetch connections as needed to serve in-flight requests. + properties: + perEndpointPercent: + description: |- + PerEndpointPercent configures how many additional connections to maintain per + upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a + percentage of the connections required by active streams + (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). + + Allowed value range is between 100-300. When both PerEndpointPercent and + PredictivePercent are set, Envoy ensures both are satisfied (max of the two). + format: int32 + maximum: 300 + minimum: 100 + type: integer + predictivePercent: + description: |- + PredictivePercent configures how many additional connections to maintain + across the cluster by anticipating which upstream endpoint the load balancer + will select next, useful for low-QPS services. Relies on deterministic + loadbalancing and is only supported with Random or RoundRobin. + Expressed as a percentage of the connections required by active streams + (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). + + Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are + set Envoy ensures both are satisfied per host (max of the two). + format: int32 + minimum: 100 + type: integer + type: object + socketBufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket + to backend. + SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. + For example, 20Mi, 1Gi, 256Ki etc. + Note that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + type: object + dns: + description: DNS includes dns resolution settings. + properties: + dnsRefreshRate: + description: |- + DNSRefreshRate specifies the rate at which DNS records should be refreshed. + Defaults to 30 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + lookupFamily: + description: |- + LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). + If set, this configuration overrides other defaults. + enum: + - IPv4 + - IPv6 + - IPv4Preferred + - IPv6Preferred + - IPv4AndIPv6 + type: string + respectDnsTtl: + description: |- + RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. + If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. + Defaults to true. + type: boolean + type: object + healthCheck: + description: HealthCheck allows gateway to + perform active health checking on backends. + properties: + active: + description: Active health check configuration + properties: + grpc: + description: |- + GRPC defines the configuration of the GRPC health checker. + It's optional, and can only be used if the specified type is GRPC. + properties: + service: + description: |- + Service to send in the health check request. + If this is not specified, then the health check request applies to the entire + server and not to a specific service. + type: string + type: object + healthCheckLog: + description: |- + HealthCheckLog defines health check event logging configuration for this cluster. + When set, HC probe outcomes are logged to the configured sinks. + Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. + properties: + matches: + description: |- + Matches defines which health check probe outcomes produce a log entry. + When omitted or empty, all events are logged. + + Each value must be unique. Multiple values are ORed. If any failure type is + specified then a success type must also be specified, and vice versa. + items: + description: ProxyHealthCheckLogEventType + specifies which health check + probe outcomes produce a log + entry. + enum: + - Failure + - FailureSeriesStart + - Success + - HealthyTransition + type: string + maxItems: 4 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: a failure type and + a success type must both be + specified together + rule: self.exists(e, e == 'Failure' + || e == 'FailureSeriesStart') + == self.exists(e, e == 'Success' + || e == 'HealthyTransition') + sinks: + description: |- + Sinks defines where health check events are written. + When omitted, events are written to /dev/stdout. + items: + description: ProxyHealthCheckLogSink + defines a destination for + health check event logs. + properties: + file: + description: |- + File defines the file sink configuration. + Required when type is File. + properties: + path: + description: |- + Path specifies the file path for health check event output. + Use /dev/stdout to write to standard output. + minLength: 1 + type: string + required: + - path + type: object + type: + description: Type defines + the type of sink. + enum: + - File + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If ProxyHealthCheckLogSink + type is File, file field + needs to be set. + rule: 'self.type == ''File'' + ? has(self.file) : !has(self.file)' + maxItems: 1 + type: array + type: object + healthyThreshold: + default: 1 + description: HealthyThreshold defines + the number of healthy health checks + required before a backend host is + marked healthy. + format: int32 + minimum: 1 + type: integer + http: + description: |- + HTTP defines the configuration of http health checker. + It's required while the health checker type is HTTP. + properties: + expectedResponse: + description: ExpectedResponse + defines a list of HTTP expected + responses to match. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + expectedStatuses: + description: |- + ExpectedStatuses defines a list of HTTP response statuses considered healthy. + Defaults to 200 only + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + hostname: + description: |- + Hostname defines the HTTP Host header used for active HTTP health checks. + Host selection uses this order: this field, the associated Backend endpoint + hostname if available, then the effective Route hostname. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + method: + description: |- + Method defines the HTTP method used for health checking. + Defaults to GET + maxLength: 16 + type: string + path: + description: Path defines the + HTTP path that will be requested + during health checking. + maxLength: 1024 + minLength: 1 + type: string + requestBody: + description: RequestBody defines + the HTTP request body payload + sent during health checking. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + retriableStatuses: + description: |- + RetriableStatuses defines a list of HTTP response statuses considered retriable. + Responses matching these statuses count towards the unhealthy threshold but + do not result in the host being considered immediately unhealthy. + The expected statuses take precedence for any range overlaps with this field. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + required: + - path + type: object + x-kubernetes-validations: + - message: The requestBody field can + only be set when method is POST + or PUT. + rule: '!has(self.requestBody) || + (has(self.method) && self.method.upperAscii() + in [''POST'',''PUT''])' + initialJitter: + description: |- + InitialJitter defines the maximum time Envoy will wait before the first health check. + Envoy will randomly select a value between 0 and the initial jitter value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + default: 3s + description: Interval defines the + time between active health checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + overrides: + description: |- + Overrides defines the configuration of the overriding health check settings for all endpoints + in the backend cluster. This allows customization of port and other settings that may differ + from the main service configuration. + properties: + port: + description: |- + Port overrides the health check port. + If not set, the endpoint's serving port is used for health checks. + This is useful when health checks are served on a different port than + the main service port (e.g., port 443 for service, port 9090 for health checks). + format: int32 + maximum: 65535 + minimum: 1 + type: integer + type: object + tcp: + description: |- + TCP defines the configuration of tcp health checker. + It's required while the health checker type is TCP. + properties: + receive: + description: Receive defines the + expected response payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + send: + description: Send defines the + request payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + type: object + timeout: + default: 1s + description: Timeout defines the time + to wait for a health check response. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: + allOf: + - enum: + - HTTP + - TCP + - GRPC + - enum: + - HTTP + - TCP + - GRPC + description: Type defines the type + of health checker. + type: string + unhealthyThreshold: + default: 3 + description: |- + UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. + Without RetriableStatuses configured, any health check failure results in the host being immediately + considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried + up to this threshold before the host is marked unhealthy. + format: int32 + minimum: 1 + type: integer + required: + - type + type: object + x-kubernetes-validations: + - message: If Health Checker type is HTTP, + http field needs to be set. + rule: 'self.type == ''HTTP'' ? has(self.http) + : !has(self.http)' + - message: If Health Checker type is TCP, + tcp field needs to be set. + rule: 'self.type == ''TCP'' ? has(self.tcp) + : !has(self.tcp)' + - message: The grpc field can only be + set if the Health Checker type is + GRPC. + rule: 'has(self.grpc) ? self.type == + ''GRPC'' : true' + panicThreshold: + description: |- + When number of unhealthy endpoints for a backend reaches this threshold + Envoy will disregard health status and balance across all endpoints. + It's designed to prevent a situation in which host failures cascade throughout the cluster + as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. + format: int32 + maximum: 100 + minimum: 0 + type: integer + passive: + description: Passive passive check configuration + properties: + alwaysEjectOneEndpoint: + default: false + description: |- + AlwaysEjectOneEndpoint defines whether at least one host should be ejected, + regardless of MaxEjectionPercent. + type: boolean + baseEjectionTime: + default: 30s + description: BaseEjectionTime defines + the base duration for which a host + will be ejected on consecutive failures. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors + sets the number of consecutive 5xx + errors triggering ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + description: ConsecutiveGatewayErrors + sets the number of consecutive gateway + errors triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 + description: |- + ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors is set to true. + format: int32 + type: integer + failurePercentageThreshold: + description: |- + FailurePercentageThreshold sets the failure percentage threshold for outlier detection. + If the failure percentage of a given host is greater than or equal to this value, it will be ejected. + Defaults to 85. + format: int32 + maximum: 100 + minimum: 0 + type: integer + interval: + default: 3s + description: Interval defines the + time between passive health checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent sets + the maximum percentage of hosts + in a cluster that can be ejected. + format: int32 + maximum: 100 + minimum: 0 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors + enables splitting of errors between + external and local origin. + type: boolean + type: object + type: object + http2: + description: HTTP2 provides HTTP/2 configuration + for backend connections. + properties: + connectionKeepalive: + description: ConnectionKeepalive configures + HTTP/2 connection keepalive using PING + frames. + properties: + idleInterval: + description: IdleInterval specifies + how long a connection must be idle + before a PING is sent. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: Interval specifies how + often to send HTTP/2 PING frames + to keep the connection alive. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + intervalJitter: + description: |- + IntervalJitter specifies a random jitter percentage added to each interval. + Defaults to 15% if not specified. + format: int32 + maximum: 100 + minimum: 0 + type: integer + timeout: + description: Timeout specifies how + long to wait for a PING response + before considering the connection + dead. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: timeout must be less than interval + rule: '!has(self.timeout) || !has(self.interval) + || duration(self.timeout) < duration(self.interval)' + initialConnectionWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. + If not set, the default value is 1 MiB. + x-kubernetes-int-or-string: true + initialStreamWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialStreamWindowSize sets the initial window size for HTTP/2 streams. + If not set, the default value is 64 KiB(64*1024). + x-kubernetes-int-or-string: true + maxConcurrentStreams: + description: |- + MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. + If not set, the default value is 100. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + onInvalidMessage: + description: |- + OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error + It's recommended for L2 Envoy deployments to set this value to TerminateStream. + https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two + Default: TerminateConnection + type: string + type: object + loadBalancer: + description: |- + LoadBalancer policy to apply when routing traffic from the gateway to + the backend endpoints. Defaults to `LeastRequest`. + properties: + backendUtilization: + description: |- + BackendUtilization defines the configuration when the load balancer type is + set to BackendUtilization. + properties: + blackoutPeriod: + description: |- + A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. + Default is 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + errorUtilizationPenaltyPercent: + description: |- + ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). + This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. + + For example: + - 100 => 1.0x + - 120 => 1.2x + - 200 => 2.0x + + Must be non-negative. + format: int32 + minimum: 0 + type: integer + keepResponseHeaders: + default: false + description: |- + KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. + Defaults to false. + type: boolean + metricNamesForComputingUtilization: + description: |- + Metric names used to compute utilization if application_utilization is not set. + For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". + items: + type: string + type: array + outOfBand: + description: |- + OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a + server-streaming gRPC connection to each endpoint's + xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load + reports periodically, instead of relying on in-band ORCA metrics + carried in response headers/trailers. + + The backend must implement OpenRcaService for this to take effect. + properties: + authority: + description: |- + Authority overrides the :authority header on the OutOfBand gRPC stream. + If unset, Envoy uses the endpoint hostname, then the dialed address, then + the cluster name. + maxLength: 259 + minLength: 1 + pattern: ^[^\x00\n\r]*$ + type: string + port: + description: |- + Port overrides the port used for the OutOfBand reporting connection, e.g. to + reach a separate reporting sidecar. Defaults to the endpoint's port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + reportingPeriod: + description: |- + ReportingPeriod is how often Envoy requests load reports from the server. + Must be greater than 0. Defaults to 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + x-kubernetes-validations: + - message: reportingPeriod must + be greater than 0 + rule: duration(self) > duration('0s') + type: object + weightExpirationPeriod: + description: If a given endpoint has + not reported load metrics in this + long, stop using the reported weight. + Defaults to 3m. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + weightUpdatePeriod: + description: How often endpoint weights + are recalculated. Values less than + 100ms are capped at 100ms. Default + 1s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + consistentHash: + description: |- + ConsistentHash defines the configuration when the load balancer type is + set to ConsistentHash + properties: + cookie: + description: Cookie configures the + cookie hash policy when the consistent + hash type is set to Cookie. + properties: + attributes: + additionalProperties: + type: string + description: Additional Attributes + to set for the generated cookie. + type: object + name: + description: |- + Name of the cookie to hash. + If this cookie does not exist in the request, Envoy will generate a cookie and set + the TTL on the response back to the client based on Layer 4 + attributes of the backend endpoint, to ensure that these future requests + go to the same backend endpoint. Make sure to set the TTL field for this case. + type: string + ttl: + description: |- + TTL of the generated cookie if the cookie is not present. This value sets the + Max-Age attribute value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + required: + - name + type: object + header: + description: |- + Header configures the header hash policy when the consistent hash type is set to Header. + + Deprecated: use Headers instead + properties: + name: + description: Name of the header + to hash. + type: string + required: + - name + type: object + headers: + description: Headers configures the + header hash policy for each header, + when the consistent hash type is + set to Headers. + items: + description: |- + Header defines the header hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the header + to hash. + type: string + required: + - name + type: object + type: array + queryParams: + description: QueryParams configures + the query parameter hash policy + when the consistent hash type is + set to QueryParams. + items: + description: |- + QueryParam defines the query parameter name hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the query + param to hash. + type: string + required: + - name + type: object + type: array + tableSize: + default: 65537 + description: The table size for consistent + hashing, must be prime number limited + to 5000011. + format: int64 + maximum: 5000011 + minimum: 2 + type: integer + type: + description: |- + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Headers", + "Cookie". + "QueryParams". + enum: + - SourceIP + - Header + - Headers + - Cookie + - QueryParams + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If consistent hash type is + header, the header field must be set. + rule: 'self.type == ''Header'' ? has(self.header) + : !has(self.header)' + - message: If consistent hash type is + headers, the headers field must be + set. + rule: 'self.type == ''Headers'' ? has(self.headers) + : !has(self.headers)' + - message: If consistent hash type is + cookie, the cookie field must be set. + rule: 'self.type == ''Cookie'' ? has(self.cookie) + : !has(self.cookie)' + - message: If consistent hash type is + queryParams, the queryParams field + must be set. + rule: 'self.type == ''QueryParams'' + ? has(self.queryParams) : !has(self.queryParams)' + dynamicModule: + description: |- + DynamicModule defines the configuration when the load balancer type is + set to DynamicModule. The referenced module must be registered in the + EnvoyProxy resource's dynamicModules allowlist. + properties: + config: + description: |- + Config is optional configuration for the module's load balancer + implementation. This is serialized and passed to the module's + initialization function. + x-kubernetes-preserve-unknown-fields: true + lbPolicyName: + description: |- + LBPolicyName identifies a specific load balancer implementation within + the dynamic module. A single shared library can contain multiple LB + policy implementations. This value is passed to the module's + initialization function to select the appropriate implementation. + maxLength: 253 + minLength: 1 + type: string + name: + description: |- + Name references a dynamic module registered in the EnvoyProxy resource's + dynamicModules list. The referenced module must exist in the registry; + otherwise, the policy will be rejected. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - lbPolicyName + - name + type: object + endpointOverride: + description: |- + EndpointOverride defines the configuration for endpoint override. + When specified, the load balancer will attempt to route requests to endpoints + based on the override information extracted from request headers or metadata. + If the override endpoints are not available, the configured load balancer policy will be used as fallback. + properties: + extractFrom: + description: ExtractFrom defines the + sources to extract endpoint override + information from. + items: + description: EndpointOverrideExtractFrom + defines a source to extract endpoint + override information from. + properties: + header: + description: |- + Header defines the header to get the override endpoint addresses. + The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. + For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. + The IPv6 address is enclosed in square brackets. + type: string + type: object + maxItems: 10 + minItems: 1 + type: array + required: + - extractFrom + type: object + slowStart: + description: |- + SlowStart defines the configuration related to the slow start load balancer policy. + If set, during slow start window, traffic sent to the newly added hosts will gradually increase. + Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. + properties: + window: + description: |- + Window defines the duration of the warm up period for newly added host. + During slow start window, traffic sent to the newly added hosts will gradually increase. + Currently only supports linear growth of traffic. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + required: + - window + type: object + type: + description: |- + Type decides the type of Load Balancer policy. + Valid LoadBalancerType values are + "ConsistentHash", + "LeastRequest", + "Random", + "RoundRobin", + "BackendUtilization", + "DynamicModule". + enum: + - ConsistentHash + - LeastRequest + - Random + - RoundRobin + - BackendUtilization + - DynamicModule + type: string + zoneAware: + description: ZoneAware defines the configuration + related to the distribution of requests + between locality zones. + properties: + preferLocal: + description: PreferLocalZone configures + zone-aware routing to prefer sending + traffic to the local locality zone. + properties: + force: + description: |- + ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior + which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. + properties: + minEndpointsInZoneThreshold: + description: |- + MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone + override. This is useful for protecting zones with fewer endpoints. + format: int32 + type: integer + type: object + minEndpointsThreshold: + description: MinEndpointsThreshold + is the minimum number of total + upstream endpoints across all + zones required to enable zone-aware + routing. + format: int64 + type: integer + percentageEnabled: + description: Configures percentage + of requests that will be considered + for zone aware routing if zone + aware routing is configured. + If not specified, Envoy defaults + to 100%. + format: int32 + maximum: 100 + minimum: 0 + type: integer + type: object + weightedZones: + description: |- + WeightedZones configures weight-based traffic distribution across locality zones. + Traffic is distributed proportionally based on the sum of all zone weights. + items: + description: WeightedZoneConfig + defines the weight for a specific + locality zone. + properties: + weight: + description: |- + Weight defines the weight for this locality. + Higher values receive more traffic. The actual traffic distribution + is proportional to this value relative to other localities. + format: int32 + type: integer + zone: + description: |- + Zone specifies the topology zone this weight applies to. + The value should match the topology.kubernetes.io/zone label + of the nodes where endpoints are running. + Zones not listed in the configuration receive a default weight of 1. + type: string + required: + - weight + - zone + type: object + type: array + x-kubernetes-list-map-keys: + - zone + x-kubernetes-list-type: map + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: If LoadBalancer type is consistentHash, + consistentHash field needs to be set. + rule: 'self.type == ''ConsistentHash'' ? + has(self.consistentHash) : !has(self.consistentHash)' + - message: If LoadBalancer type is BackendUtilization, + backendUtilization field needs to be set. + rule: 'self.type == ''BackendUtilization'' + ? has(self.backendUtilization) : !has(self.backendUtilization)' + - message: If LoadBalancer type is DynamicModule, + dynamicModule field needs to be set. + rule: 'self.type == ''DynamicModule'' ? + has(self.dynamicModule) : !has(self.dynamicModule)' + - message: Currently SlowStart is only supported + for RoundRobin, LeastRequest, and BackendUtilization + load balancers. + rule: 'self.type in [''Random'', ''ConsistentHash'', + ''DynamicModule''] ? !has(self.slowStart) + : true' + - message: PreferLocal zone-aware routing + is not supported for ConsistentHash load + balancers. Use weightedZones instead. + rule: 'self.type == ''ConsistentHash'' && + has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: PreferLocal zone-aware routing + is not currently supported for BackendUtilization + load balancers. Only WeightedZones can + be used with BackendUtilization. + rule: 'self.type == ''BackendUtilization'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: ZoneAware routing is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' ? + !has(self.zoneAware) : true' + - message: ZoneAware PreferLocal and WeightedZones + cannot be specified together. + rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) + && has(self.zoneAware.weightedZones)) + : true' + - message: EndpointOverride is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' ? + !has(self.endpointOverride) : true' + proxyProtocol: + description: ProxyProtocol enables the Proxy + Protocol when communicating with the backend. + properties: + version: + description: |- + Version of ProxyProtocol + Valid ProxyProtocolVersion values are + "V1" + "V2" + enum: + - V1 + - V2 + type: string + required: + - version + type: object + retry: + description: |- + Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. + If not set, retry will be disabled. + properties: + numAttemptsPerPriority: + description: |- + NumAttemptsPerPriority defines the number of requests (initial attempt + retries) + that should be sent to the same priority before switching to a different one. + If not specified or set to 0, all requests are sent to the highest priority that is healthy. + format: int32 + type: integer + numRetries: + default: 2 + description: NumRetries is the number + of retries to be attempted. Defaults + to 2. + format: int32 + minimum: 0 + type: integer + perRetry: + description: PerRetry is the retry policy + to be applied per retry attempt. + properties: + backOff: + description: |- + Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential + back-off algorithm for retries. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + properties: + baseInterval: + description: BaseInterval is the + base interval between retries. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxInterval: + description: |- + MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. + The default is 10 times the base_interval + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + timeout: + description: Timeout is the timeout + per retry attempt. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + retryOn: + description: |- + RetryOn specifies the retry trigger condition. + + If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). + properties: + httpStatusCodes: + description: |- + HttpStatusCodes specifies the http status codes to be retried. + The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + triggers: + description: Triggers specifies the + retry trigger condition(Http/Grpc). + items: + description: TriggerEnum specifies + the conditions that trigger retries. + enum: + - 5xx + - gateway-error + - reset + - reset-before-request + - connect-failure + - retriable-4xx + - refused-stream + - retriable-status-codes + - cancelled + - deadline-exceeded + - internal + - resource-exhausted + - unavailable + type: string + type: array + type: object + type: object + tcpKeepalive: + description: |- + TcpKeepalive settings associated with the upstream client connection. + Disabled by default. + properties: + idleTime: + description: |- + The duration a connection needs to be idle before keep-alive + probes start being sent. + The duration format is + Defaults to `7200s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: |- + The duration between keep-alive probes. + Defaults to `75s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + probes: + description: |- + The total number of unacknowledged probes to send before deciding + the connection is dead. + Defaults to 9. + format: int32 + type: integer + type: object + timeout: + description: Timeout settings for the backend + connections. + properties: + http: + description: Timeout settings for HTTP. + properties: + connectionIdleTimeout: + description: |- + The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + Default: 1 hour. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxConnectionDuration: + description: |- + The maximum duration of an HTTP connection. + Default: unlimited. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxStreamDuration: + description: |- + MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time + from when the request is sent until the response stream is fully consumed and does not apply to + non-streaming requests. + When set to "0s", no max duration is applied and streams can run indefinitely. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + requestTimeout: + description: RequestTimeout is the + time until which entire response + is received from the upstream. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + streamIdleTimeout: + description: |2- + The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. + If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + tcp: + description: Timeout settings for TCP. + properties: + connectTimeout: + description: |- + The timeout for network connection establishment, including TCP and TLS handshakes. + Default: 10 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + type: object + type: object + x-kubernetes-validations: + - message: predictivePercent in preconnect policy + only works with RoundRobin or Random load + balancers + rule: '!((has(self.connection) && has(self.connection.preconnect) + && has(self.connection.preconnect.predictivePercent)) + && !(has(self.loadBalancer) && has(self.loadBalancer.type) + && self.loadBalancer.type in [''Random'', + ''RoundRobin'']))' + endSessionEndpoint: + description: |- + The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). + + If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. + EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. + type: string + issuer: + description: |- + The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). + Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST + be https, a host component, and optionally, port and path components and + no query or fragment components. + pattern: ^https://[^/?#@]+(/[^?#]*)?$ + type: string + tokenEndpoint: + description: |- + The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string + required: + - issuer + type: object + x-kubernetes-validations: + - message: BackendRefs must be used, backendRef is + not supported. + rule: '!has(self.backendRef)' + - message: Retry timeout is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? + !has(self.backendSettings.retry.perRetry.timeout):true):true):true + - message: HTTPStatusCodes is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? + !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true + redirectURL: + description: |- + The redirect URL to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" + type: string + refreshToken: + default: true + description: |- + RefreshToken indicates whether the Envoy should automatically refresh the + id token and access token when they expire. + When set to true, the Envoy will use the refresh token to get a new id token + and access token when they expire. + + If not specified, defaults to true. + type: boolean + resources: + description: |- + The OIDC resources to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + items: + type: string + type: array + scopes: + description: |- + The OIDC scopes to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + The "openid" scope is always added to the list of scopes if not already + specified. + items: + type: string + type: array + required: + - clientSecret + - provider + type: object + x-kubernetes-validations: + - message: only one of clientID or clientIDRef must be + set + rule: (has(self.clientID) && !has(self.clientIDRef)) + || (!has(self.clientID) && has(self.clientIDRef)) + - message: forwardAccessToken cannot be true when forwardIDToken.header + is Authorization + rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken + && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() + == ''authorization'')' + spiffe: + description: SPIFFE obtains the subject token as a JWT-SVID + from the SPIFFE Workload API. + properties: + audience: + description: Audience is the audience of the requested + JWT-SVID. + minLength: 1 + type: string + socketPath: + description: |- + SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". + When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. + pattern: ^(unix|tcp)://.+ + type: string + required: + - audience + type: object + type: object + x-kubernetes-validations: + - message: Exactly one of oidc or spiffe must be specified + rule: has(self.oidc) != has(self.spiffe) + subjectTokenType: + default: urn:ietf:params:oauth:token-type:jwt + description: SubjectTokenType is the RFC 8693 subject_token_type, + identifying the format of the subject token. + minLength: 1 + type: string + tokenURL: + description: TokenURL is the token exchange endpoint. + minLength: 1 + type: string + x-kubernetes-validations: + - message: tokenURL must be a valid URL + rule: isURL(self) + required: + - subjectToken + - tokenURL + type: object + required: + - tokenExchange + type: object targetRefs: description: |- TargetRefs are the names of the AIServiceBackend or InferencePool resources this BackendSecurityPolicy is being attached to. @@ -12875,6 +14900,7 @@ spec: - AzureCredentials - GCPCredentials - AnthropicAPIKey + - OpenAICredentials type: string required: - type @@ -12883,32 +14909,42 @@ spec: - message: When type is APIKey, only apiKey field should be set rule: 'self.type == ''APIKey'' ? (has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) && !has(self.azureCredentials) && !has(self.gcpCredentials) - && !has(self.anthropicAPIKey)) : true' + && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true' - message: When type is AWSCredentials, only awsCredentials field should be set rule: 'self.type == ''AWSCredentials'' ? (has(self.awsCredentials) && !has(self.apiKey) && !has(self.azureAPIKey) && !has(self.azureCredentials) - && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true' + && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) + : true' - message: When type is AzureAPIKey, only azureAPIKey field should be set rule: 'self.type == ''AzureAPIKey'' ? (has(self.azureAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureCredentials) && !has(self.gcpCredentials) - && !has(self.anthropicAPIKey)) : true' + && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) : true' - message: When type is AzureCredentials, only azureCredentials field should be set rule: 'self.type == ''AzureCredentials'' ? (has(self.azureCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) - && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) : true' + && !has(self.gcpCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) + : true' - message: When type is GCPCredentials, only gcpCredentials field should be set rule: 'self.type == ''GCPCredentials'' ? (has(self.gcpCredentials) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) - && !has(self.azureCredentials) && !has(self.anthropicAPIKey)) : true' + && !has(self.azureCredentials) && !has(self.anthropicAPIKey) && !has(self.openAICredentials)) + : true' - message: When type is AnthropicAPIKey, only anthropicAPIKey field should be set rule: 'self.type == ''AnthropicAPIKey'' ? (has(self.anthropicAPIKey) && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) - && !has(self.azureCredentials) && !has(self.gcpCredentials)) : true' + && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.openAICredentials)) + : true' + - message: When type is OpenAICredentials, only openAICredentials field + should be set + rule: 'self.type == ''OpenAICredentials'' ? (has(self.openAICredentials) + && !has(self.apiKey) && !has(self.awsCredentials) && !has(self.azureAPIKey) + && !has(self.azureCredentials) && !has(self.gcpCredentials) && !has(self.anthropicAPIKey)) + : true' status: description: Status defines the status details of the BackendSecurityPolicy. properties: diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index a918cb811e..7316055ab5 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -3187,8 +3187,12 @@ MCPRouteList contains a list of MCPRoute. - [BackendSecurityPolicyCredentialOverride](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicycredentialoverride) - [BackendSecurityPolicyGCPCredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicygcpcredentials) - [BackendSecurityPolicyOIDC](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyoidc) +- [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) +- [BackendSecurityPolicySPIFFE](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffe) - [BackendSecurityPolicySpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspec) - [BackendSecurityPolicyStatus](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicystatus) +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) +- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - [BackendSecurityPolicyType](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytype) - [CredentialOverrideFromDynamicMetadata](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromdynamicmetadata) - [CredentialOverrideFromRequestHeaders](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromrequestheaders) @@ -3903,6 +3907,63 @@ BackendSecurityPolicyOIDC specifies OIDC related fields. /> +#### BackendSecurityPolicyOpenAICredentials + + + +**Appears in:** +- [BackendSecurityPolicySpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspec) + +BackendSecurityPolicyOpenAICredentials contains the supported authentication mechanisms to access OpenAI. + +##### Fields + + + + + + +#### BackendSecurityPolicySPIFFE + + + +**Appears in:** +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) + +BackendSecurityPolicySPIFFE specifies how to obtain a JWT-SVID from the SPIFFE Workload API. + +##### Fields + + + + + + #### BackendSecurityPolicySpec @@ -3959,6 +4020,11 @@ Only one type of BackendSecurityPolicy can be defined. type="[BackendSecurityPolicyAnthropicAPIKey](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyanthropicapikey)" required="false" description="AnthropicAPIKey is a mechanism to access Anthropic backend(s). The API key will be injected into the `x-api-key` header.
https://docs.claude.com/en/api/overview#authentication" +/> +#### BackendSecurityPolicySubjectToken + + + +**Appears in:** +- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) + +BackendSecurityPolicySubjectToken specifies the source of a subject token. +Exactly one source must be configured. + +##### Fields + + + + + + +#### BackendSecurityPolicyTokenExchange + + + +**Appears in:** +- [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) + +BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. +The controller obtains a subject token, exchanges it at TokenURL for an access token, +and stores the access token in a secret, rotating it before it expires. + +https://datatracker.ietf.org/doc/html/rfc8693 + +##### Fields + + + + + + #### BackendSecurityPolicyType **Underlying type:** string @@ -4031,6 +4170,11 @@ BackendSecurityPolicyType specifies the type of auth mechanism used to access a type="enum" required="false" description="" +/> #### CredentialOverrideFromDynamicMetadata diff --git a/tests/crdcel/main_test.go b/tests/crdcel/main_test.go index 6a3511a104..a346eb839b 100644 --- a/tests/crdcel/main_test.go +++ b/tests/crdcel/main_test.go @@ -239,6 +239,32 @@ func TestBackendSecurityPolicies(t *testing.T) { {name: "aws_credential_override.yaml"}, {name: "gcp_oidc.yaml"}, {name: "anthropic-apikey.yaml"}, + {name: "openai_spiffe.yaml"}, + {name: "openai_oidc.yaml"}, + { + name: "openai_missing_subject_token.yaml", + expErr: "Exactly one of oidc or spiffe must be specified", + }, + { + name: "openai_multiple_subject_tokens.yaml", + expErr: "Exactly one of oidc or spiffe must be specified", + }, + { + name: "openai_invalid_token_url.yaml", + expErr: "tokenURL must be a valid URL", + }, + { + name: "openai_invalid_socket_path.yaml", + expErr: "spec.openAICredentials.tokenExchange.subjectToken.spiffe.socketPath in body should match", + }, + { + name: "openai_with_apikey.yaml", + expErr: "When type is OpenAICredentials, only openAICredentials field should be set", + }, + { + name: "apikey_with_openai_credentials.yaml", + expErr: "When type is APIKey, only apiKey field should be set", + }, {name: "targetrefs_basic.yaml"}, {name: "targetrefs_multiple.yaml"}, {name: "targetrefs_inferencepool.yaml"}, @@ -270,6 +296,21 @@ func TestBackendSecurityPolicies(t *testing.T) { } } +func TestBackendSecurityPolicySubjectTokenTypeDefault(t *testing.T) { + c, _, _ := testsinternal.NewEnvTest(t) + ctx := t.Context() + + data, err := testdata.ReadFile("testdata/backendsecuritypolicies/openai_spiffe.yaml") + require.NoError(t, err) + bsp := &aigv1b1.BackendSecurityPolicy{} + require.NoError(t, yaml.UnmarshalStrict(data, bsp)) + require.NoError(t, c.Create(ctx, bsp)) + t.Cleanup(func() { _ = c.Delete(ctx, bsp) }) + + require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", + bsp.Spec.OpenAICredentials.TokenExchange.SubjectTokenType) +} + func TestMCPRoutes(t *testing.T) { c, _, _ := testsinternal.NewEnvTest(t) ctx := t.Context() diff --git a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml new file mode 100644 index 0000000000..f860f61ebe --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml @@ -0,0 +1,21 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: apikey-with-openai-credentials-policy + namespace: default +spec: + type: APIKey + apiKey: + secretRef: + name: placeholder + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectToken: + spiffe: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml new file mode 100644 index 0000000000..51d6fec8ac --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml @@ -0,0 +1,19 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-invalid-socket-path-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectToken: + spiffe: + audience: https://auth.example.com + socketPath: /run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml new file mode 100644 index 0000000000..d39758ae52 --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml @@ -0,0 +1,18 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-invalid-token-url-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + tokenExchange: + tokenURL: not a url + subjectToken: + spiffe: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml new file mode 100644 index 0000000000..b99786f1de --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml @@ -0,0 +1,16 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-missing-subject-token-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectToken: {} diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml new file mode 100644 index 0000000000..416a631706 --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml @@ -0,0 +1,24 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-multiple-subject-tokens-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectToken: + spiffe: + audience: https://auth.example.com + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml new file mode 100644 index 0000000000..473172d2d9 --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml @@ -0,0 +1,23 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-oidc-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectTokenType: urn:ietf:params:oauth:token-type:access_token + subjectToken: + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml new file mode 100644 index 0000000000..7d362e6ccf --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml @@ -0,0 +1,24 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-spiffe-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + organization: org-placeholder + project: proj_placeholder + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + audience: https://api.openai.com + scopes: + - api.model.read + subjectToken: + spiffe: + audience: https://auth.example.com + socketPath: unix:///run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml new file mode 100644 index 0000000000..bbfe9544f9 --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml @@ -0,0 +1,21 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-with-apikey-policy + namespace: default +spec: + type: OpenAICredentials + apiKey: + secretRef: + name: placeholder + openAICredentials: + tokenExchange: + tokenURL: https://auth.example.com/oauth/token + subjectToken: + spiffe: + audience: https://auth.example.com From 2daa9fb3f528167068bb6393af422b26d9292e5a Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 11:04:25 -0400 Subject: [PATCH 03/17] api: reuse BackendSecurityPolicyOIDC for OpenAI Replace subjectToken.oidc (*egv1a1.OIDC) with oidcExchangeToken (*BackendSecurityPolicyOIDC), matching the field name and nesting of the AWS, Azure and GCP credential types. The controller can then read the OIDC config the same way for all four types. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 6 +- api/v1beta1/zz_generated.deepcopy.go | 6 +- ...envoyproxy.io_backendsecuritypolicies.yaml | 3715 +++++++++-------- site/docs/api/api.mdx | 7 +- tests/crdcel/main_test.go | 4 +- .../openai_multiple_subject_tokens.yaml | 13 +- .../backendsecuritypolicies/openai_oidc.yaml | 13 +- 7 files changed, 1909 insertions(+), 1855 deletions(-) diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 7bd89f24dc..9b785b851d 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -415,12 +415,12 @@ type BackendSecurityPolicyTokenExchange struct { // BackendSecurityPolicySubjectToken specifies the source of a subject token. // Exactly one source must be configured. // -// +kubebuilder:validation:XValidation:rule="has(self.oidc) != has(self.spiffe)",message="Exactly one of oidc or spiffe must be specified" +// +kubebuilder:validation:XValidation:rule="has(self.oidcExchangeToken) != has(self.spiffe)",message="Exactly one of oidcExchangeToken or spiffe must be specified" type BackendSecurityPolicySubjectToken struct { - // OIDC obtains the subject token via an OIDC client-credentials flow. + // OIDCExchangeToken obtains the subject token via an OIDC client-credentials flow. // // +optional - OIDC *egv1a1.OIDC `json:"oidc,omitempty"` + OIDCExchangeToken *BackendSecurityPolicyOIDC `json:"oidcExchangeToken,omitempty"` // SPIFFE obtains the subject token as a JWT-SVID from the SPIFFE Workload API. // diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8b3f6073a8..e3ccc23fba 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -782,9 +782,9 @@ func (in *BackendSecurityPolicyStatus) DeepCopy() *BackendSecurityPolicyStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendSecurityPolicySubjectToken) DeepCopyInto(out *BackendSecurityPolicySubjectToken) { *out = *in - if in.OIDC != nil { - in, out := &in.OIDC, &out.OIDC - *out = new(v1alpha1.OIDC) + if in.OIDCExchangeToken != nil { + in, out := &in.OIDCExchangeToken, &out.OIDCExchangeToken + *out = new(BackendSecurityPolicyOIDC) (*in).DeepCopyInto(*out) } if in.SPIFFE != nil { diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 4728a907ef..447a615786 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -12858,1955 +12858,2005 @@ spec: description: SubjectToken specifies where the subject token is obtained from. properties: - oidc: - description: OIDC obtains the subject token via an OIDC - client-credentials flow. + oidcExchangeToken: + description: OIDCExchangeToken obtains the subject token + via an OIDC client-credentials flow. properties: - clientID: - description: |- - The client ID to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - - Only one of clientID or clientIDRef must be set. - minLength: 1 + aud: + description: Aud defines the audience that this ID + Token is intended for. type: string - clientIDRef: - description: |- - The Kubernetes secret which contains the client ID to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - Exactly one of clientID or clientIDRef must be set. - This is an Opaque secret. The client ID should be stored in the key "client-id". - - Only one of clientID or clientIDRef must be set. + grantType: + description: GrantType is the method application gets + access token. + type: string + oidc: + description: OIDC is used to obtain oidc tokens via + an SSO server which will be used to exchange for + provider credentials. properties: - group: - default: "" + clientID: description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. For - example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 + The client ID to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + Only one of clientID or clientIDRef must be set. minLength: 1 type: string - namespace: + clientIDRef: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. + The Kubernetes secret which contains the client ID to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Exactly one of clientID or clientIDRef must be set. + This is an Opaque secret. The client ID should be stored in the key "client-id". - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + Only one of clientID or clientIDRef must be set. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. + For example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name - type: object - clientSecret: - description: |- - The Kubernetes secret which contains the OIDC client secret to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. - This is an Opaque secret. The client secret should be stored in the key - "client-secret". - properties: - group: - default: "" + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + clientSecret: description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. For - example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 + The Kubernetes secret which contains the OIDC client secret to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + This is an Opaque secret. The client secret should be stored in the key + "client-secret". + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. + For example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + cookieConfig: + description: |- + CookieConfigs allows setting the SameSite attribute for OIDC cookies. + By default, its unset. + properties: + sameSite: + enum: + - Lax + - Strict + - None + type: string + type: object + cookieDomain: + description: |- + The optional domain to set the access and ID token cookies on. + If not set, the cookies will default to the host of the request, not including the subdomains. + If set, the cookies will be set on the specified domain and all subdomains. + This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string - namespace: + cookieNames: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. + The optional cookie name overrides to be used for Bearer and IdToken cookies in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses a randomly generated suffix + properties: + accessToken: + description: |- + The name of the cookie used to store the AccessToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "AccessToken-(randomly generated uid)" + type: string + idToken: + description: |- + The name of the cookie used to store the IdToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "IdToken-(randomly generated uid)" + type: string + type: object + csrfTokenTTL: + description: |- + CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token + in the "state" parameter when the provider redirects back to the callback endpoint. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + If omitted, Envoy Gateway defaults the token expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - required: - - name - type: object - cookieConfig: - description: |- - CookieConfigs allows setting the SameSite attribute for OIDC cookies. - By default, its unset. - properties: - sameSite: - enum: - - Lax - - Strict - - None + defaultRefreshTokenTTL: + description: |- + DefaultRefreshTokenTTL is the default lifetime of the refresh token. + This field is only used when the exp (expiration time) claim is omitted in + the refresh token or the refresh token is not JWT. + + If not specified, defaults to 604800s (one week). + Note: this field is only applicable when the "refreshToken" field is set to true. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - type: object - cookieDomain: - description: |- - The optional domain to set the access and ID token cookies on. - If not set, the cookies will default to the host of the request, not including the subdomains. - If set, the cookies will be set on the specified domain and all subdomains. - This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - cookieNames: - description: |- - The optional cookie name overrides to be used for Bearer and IdToken cookies in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses a randomly generated suffix - properties: - accessToken: + defaultTokenTTL: description: |- - The name of the cookie used to store the AccessToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "AccessToken-(randomly generated uid)" + DefaultTokenTTL is the default lifetime of the id token and access token. + Please note that Envoy will always use the expiry time from the response + of the authorization server if it is provided. This field is only used when + the expiry time is not provided by the authorization. + + If not specified, defaults to 0. In this case, the "expires_in" field in + the authorization response must be set by the authorization server, or the + OAuth flow will fail. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - idToken: + denyRedirect: description: |- - The name of the cookie used to store the IdToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "IdToken-(randomly generated uid)" + Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. + This behavior can be useful for AJAX or machine requests. + properties: + headers: + description: Defines the headers to match + against the request to deny redirect to + the OIDC Provider. + items: + description: OIDCDenyRedirectHeader defines + how a header is matched + properties: + name: + description: Specifies the name of the + header in the request. + minLength: 1 + type: string + type: + default: Exact + description: Type specifies how to match + against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string + value that the match must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - headers + type: object + disableTokenEncryption: + description: |- + Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. + This option should only be used in secure environments where token encryption is not required. + Default is false (tokens are encrypted). + type: boolean + forwardAccessToken: + description: |- + ForwardAccessToken indicates whether the Envoy should forward the access token + via the Authorization header Bearer scheme to the upstream. + If not specified, defaults to false. + type: boolean + forwardIDToken: + description: |- + ForwardIDToken configures forwarding of the OIDC ID token to the upstream. + + If the configured header is "Authorization", EG forwards the ID token using + the "Bearer " prefix. For any other header, EG forwards the raw token value. + If not specified, the ID token will not be forwarded. + + Note: when passThroughAuthHeader is enabled, this header must not be the same + as a header a JWT provider extracts from (the "Authorization" header by + default). The forwarded ID token header is owned by Envoy, and Envoy rejects + an OAuth2 configuration whose pass-through matcher keys on it. + properties: + header: + description: |- + Header is the upstream request header that will carry the ID token. + It must be a valid HTTP header name. Pseudo-headers (names starting with ":") + and the "Host" header are not allowed. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + x-kubernetes-validations: + - message: header cannot be the Host header + rule: self.lowerAscii() != 'host' + required: + - header + type: object + logoutPath: + description: |- + The path to log a user out, clearing their credential cookies. + + If not specified, uses a default logout path "/logout" type: string - type: object - csrfTokenTTL: - description: |- - CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. + passThroughAuthHeader: + description: |- + Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless + explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. - This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token - in the "state" parameter when the provider redirects back to the callback endpoint. + The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC + redirects and wish to directly supply a token instead. - If omitted, Envoy Gateway defaults the token expiration to 10 minutes. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - defaultRefreshTokenTTL: - description: |- - DefaultRefreshTokenTTL is the default lifetime of the refresh token. - This field is only used when the exp (expiration time) claim is omitted in - the refresh token or the refresh token is not JWT. + If not specified, defaults to false. + type: boolean + provider: + description: The OIDC Provider configuration. + properties: + authorizationEndpoint: + description: |- + The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string + backendRef: + description: |- + BackendRef references a Kubernetes object that represents the + backend server to which the authorization request will be sent. - If not specified, defaults to 604800s (one week). - Note: this field is only applicable when the "refreshToken" field is set to true. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - defaultTokenTTL: - description: |- - DefaultTokenTTL is the default lifetime of the id token and access token. - Please note that Envoy will always use the expiry time from the response - of the authorization server if it is provided. This field is only used when - the expiry time is not provided by the authorization. - - If not specified, defaults to 0. In this case, the "expires_in" field in - the authorization response must be set by the authorization server, or the - OAuth flow will fail. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - denyRedirect: - description: |- - Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. - This behavior can be useful for AJAX or machine requests. - properties: - headers: - description: Defines the headers to match against - the request to deny redirect to the OIDC Provider. - items: - description: OIDCDenyRedirectHeader defines - how a header is matched - properties: - name: - description: Specifies the name of the header - in the request. - minLength: 1 - type: string - type: - default: Exact - description: Type specifies how to match - against a string. - enum: - - Exact - - Prefix - - Suffix - - RegularExpression - type: string - value: - description: Value specifies the string - value that the match must have. - maxLength: 1024 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - minItems: 1 - type: array - required: - - headers - type: object - disableTokenEncryption: - description: |- - Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. - This option should only be used in secure environments where token encryption is not required. - Default is false (tokens are encrypted). - type: boolean - forwardAccessToken: - description: |- - ForwardAccessToken indicates whether the Envoy should forward the access token - via the Authorization header Bearer scheme to the upstream. - If not specified, defaults to false. - type: boolean - forwardIDToken: - description: |- - ForwardIDToken configures forwarding of the OIDC ID token to the upstream. - - If the configured header is "Authorization", EG forwards the ID token using - the "Bearer " prefix. For any other header, EG forwards the raw token value. - If not specified, the ID token will not be forwarded. - - Note: when passThroughAuthHeader is enabled, this header must not be the same - as a header a JWT provider extracts from (the "Authorization" header by - default). The forwarded ID token header is owned by Envoy, and Envoy rejects - an OAuth2 configuration whose pass-through matcher keys on it. - properties: - header: - description: |- - Header is the upstream request header that will carry the ID token. - It must be a valid HTTP header name. Pseudo-headers (names starting with ":") - and the "Host" header are not allowed. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - x-kubernetes-validations: - - message: header cannot be the Host header - rule: self.lowerAscii() != 'host' - required: - - header - type: object - logoutPath: - description: |- - The path to log a user out, clearing their credential cookies. - - If not specified, uses a default logout path "/logout" - type: string - passThroughAuthHeader: - description: |- - Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless - explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. - - The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC - redirects and wish to directly supply a token instead. - - If not specified, defaults to false. - type: boolean - provider: - description: The OIDC Provider configuration. - properties: - authorizationEndpoint: - description: |- - The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - type: string - backendRef: - description: |- - BackendRef references a Kubernetes object that represents the - backend server to which the authorization request will be sent. - - Deprecated: Use BackendRefs instead. - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". - - Defaults to "Service" when not specified. - - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. - - Support: Core (Services with a type other than ExternalName) - - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind == - ''Service'') ? has(self.port) : true' - backendRefs: - description: |- - BackendRefs references a Kubernetes object that represents the - backend server to which the authorization request will be sent. - items: - description: BackendRef defines how an ObjectReference - that is specific to BackendRef. - properties: - fallback: - description: |- - Fallback indicates whether the backend is designated as a fallback. - Multiple fallback backends can be configured. - It is highly recommended to configure active or passive health checks to ensure that failover can be detected - when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. - The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when - the health of the active backends falls below 72%. - type: boolean - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". - - Defaults to "Service" when not specified. + Deprecated: Use BackendRefs instead. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. + Defaults to "Service" when not specified. - Support: Core (Services with a type other than ExternalName) + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. + Support: Core (Services with a type other than ExternalName) - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - weight: - default: 1 - description: |- - Weight specifies the proportion of requests forwarded to the referenced - backend. This is computed as weight/(sum of all weights in this - BackendRefs list). For non-zero values, there may be some epsilon from - the exact proportion defined here depending on the precision an - implementation supports. Weight is not a percentage and the sum of - weights does not need to equal 100. - - If only one backend is specified and it has a weight greater than 0, 100% - of the traffic is forwarded to that backend. If weight is set to 0, no - traffic should be forwarded for this entry. If unspecified, weight - defaults to 1. - - Support for this field varies based on the context where used. - format: int32 - maximum: 1000000 - minimum: 0 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind - == ''Service'') ? has(self.port) : true' - maxItems: 16 - type: array - backendSettings: - description: |- - BackendSettings holds configuration for managing the connection - to the backend. - properties: - circuitBreaker: - description: |- - Circuit Breaker settings for the upstream connections and requests. - If not set, circuit breakers will be enabled with the default thresholds - properties: - maxConnections: - default: 1024 - description: The maximum number of connections - that Envoy will establish to the referenced - backend defined within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxParallelRequests: - default: 1024 - description: The maximum number of parallel - requests that Envoy will make to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxParallelRetries: - default: 1024 - description: The maximum number of parallel - retries that Envoy will make to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxPendingRequests: - default: 1024 - description: The maximum number of pending - requests that Envoy will queue to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxRequestsPerConnection: - description: |- - The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. - Default: unlimited. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - perEndpoint: - description: PerEndpoint defines Circuit - Breakers that will apply per-endpoint - for an upstream cluster - properties: - maxConnections: - default: 1024 - description: MaxConnections configures - the maximum number of connections - that Envoy will establish per-endpoint - to the referenced backend defined - within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - type: object - retryBudget: - description: |- - RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. - RetryBudget take precedence over maxParallelRetries. - properties: - minRetryConcurrency: - description: |- - MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. - For example, a budget of 20% with a minimum retry concurrency of 3 - will allow 5 active retries while there are 25 active requests. - If there are 2 active requests, there are still 3 active retries - allowed because of the minimum retry concurrency. - Defaults to 3. - format: int32 - type: integer - percent: - description: |- - Percent specifies the limit on concurrent retries as a percentage [0, 100] of - the sum of active requests and active pending requests. - properties: - denominator: - default: 100 - format: int32 - minimum: 1 - type: integer - numerator: - format: int32 - minimum: 0 - type: integer - required: - - numerator - type: object - x-kubernetes-validations: - - message: numerator must be less - than or equal to denominator - rule: self.numerator <= self.denominator - required: - - percent - type: object - type: object - connection: - description: Connection includes backend connection - settings. - properties: - bufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - BufferLimit Soft limit on size of the cluster’s connections read and write buffers. - BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. - If unspecified, an implementation defined default is applied (32768 bytes). - For example, 20Mi, 1Gi, 256Ki etc. - Note: that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true - preconnect: - description: |- - Preconnect configures proactive upstream connections to reduce latency by establishing - connections before they’re needed and avoiding connection establishment overhead. - - If unset, Envoy will fetch connections as needed to serve in-flight requests. - properties: - perEndpointPercent: - description: |- - PerEndpointPercent configures how many additional connections to maintain per - upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a - percentage of the connections required by active streams - (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). - - Allowed value range is between 100-300. When both PerEndpointPercent and - PredictivePercent are set, Envoy ensures both are satisfied (max of the two). - format: int32 - maximum: 300 - minimum: 100 - type: integer - predictivePercent: - description: |- - PredictivePercent configures how many additional connections to maintain - across the cluster by anticipating which upstream endpoint the load balancer - will select next, useful for low-QPS services. Relies on deterministic - loadbalancing and is only supported with Random or RoundRobin. - Expressed as a percentage of the connections required by active streams - (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). - - Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are - set Envoy ensures both are satisfied per host (max of the two). - format: int32 - minimum: 100 - type: integer - type: object - socketBufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket - to backend. - SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. - For example, 20Mi, 1Gi, 256Ki etc. - Note that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true - type: object - dns: - description: DNS includes dns resolution settings. - properties: - dnsRefreshRate: - description: |- - DNSRefreshRate specifies the rate at which DNS records should be refreshed. - Defaults to 30 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - lookupFamily: - description: |- - LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). - If set, this configuration overrides other defaults. - enum: - - IPv4 - - IPv6 - - IPv4Preferred - - IPv6Preferred - - IPv4AndIPv6 + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string - respectDnsTtl: - description: |- - RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. - If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. - Defaults to true. - type: boolean - type: object - healthCheck: - description: HealthCheck allows gateway to - perform active health checking on backends. - properties: - active: - description: Active health check configuration - properties: - grpc: - description: |- - GRPC defines the configuration of the GRPC health checker. - It's optional, and can only be used if the specified type is GRPC. - properties: - service: - description: |- - Service to send in the health check request. - If this is not specified, then the health check request applies to the entire - server and not to a specific service. - type: string - type: object - healthCheckLog: - description: |- - HealthCheckLog defines health check event logging configuration for this cluster. - When set, HC probe outcomes are logged to the configured sinks. - Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. - properties: - matches: - description: |- - Matches defines which health check probe outcomes produce a log entry. - When omitted or empty, all events are logged. - - Each value must be unique. Multiple values are ORed. If any failure type is - specified then a success type must also be specified, and vice versa. - items: - description: ProxyHealthCheckLogEventType - specifies which health check - probe outcomes produce a log - entry. - enum: - - Failure - - FailureSeriesStart - - Success - - HealthyTransition - type: string - maxItems: 4 - type: array - x-kubernetes-list-type: set - x-kubernetes-validations: - - message: a failure type and - a success type must both be - specified together - rule: self.exists(e, e == 'Failure' - || e == 'FailureSeriesStart') - == self.exists(e, e == 'Success' - || e == 'HealthyTransition') - sinks: - description: |- - Sinks defines where health check events are written. - When omitted, events are written to /dev/stdout. - items: - description: ProxyHealthCheckLogSink - defines a destination for - health check event logs. - properties: - file: - description: |- - File defines the file sink configuration. - Required when type is File. - properties: - path: - description: |- - Path specifies the file path for health check event output. - Use /dev/stdout to write to standard output. - minLength: 1 - type: string - required: - - path - type: object - type: - description: Type defines - the type of sink. - enum: - - File - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If ProxyHealthCheckLogSink - type is File, file field - needs to be set. - rule: 'self.type == ''File'' - ? has(self.file) : !has(self.file)' - maxItems: 1 - type: array - type: object - healthyThreshold: - default: 1 - description: HealthyThreshold defines - the number of healthy health checks - required before a backend host is - marked healthy. - format: int32 - minimum: 1 - type: integer - http: - description: |- - HTTP defines the configuration of http health checker. - It's required while the health checker type is HTTP. - properties: - expectedResponse: - description: ExpectedResponse - defines a list of HTTP expected - responses to match. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - expectedStatuses: - description: |- - ExpectedStatuses defines a list of HTTP response statuses considered healthy. - Defaults to 200 only - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array - hostname: - description: |- - Hostname defines the HTTP Host header used for active HTTP health checks. - Host selection uses this order: this field, the associated Backend endpoint - hostname if available, then the effective Route hostname. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - method: - description: |- - Method defines the HTTP method used for health checking. - Defaults to GET - maxLength: 16 - type: string - path: - description: Path defines the - HTTP path that will be requested - during health checking. - maxLength: 1024 - minLength: 1 - type: string - requestBody: - description: RequestBody defines - the HTTP request body payload - sent during health checking. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - retriableStatuses: - description: |- - RetriableStatuses defines a list of HTTP response statuses considered retriable. - Responses matching these statuses count towards the unhealthy threshold but - do not result in the host being considered immediately unhealthy. - The expected statuses take precedence for any range overlaps with this field. - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array - required: - - path - type: object - x-kubernetes-validations: - - message: The requestBody field can - only be set when method is POST - or PUT. - rule: '!has(self.requestBody) || - (has(self.method) && self.method.upperAscii() - in [''POST'',''PUT''])' - initialJitter: - description: |- - InitialJitter defines the maximum time Envoy will wait before the first health check. - Envoy will randomly select a value between 0 and the initial jitter value. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - default: 3s - description: Interval defines the - time between active health checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - overrides: - description: |- - Overrides defines the configuration of the overriding health check settings for all endpoints - in the backend cluster. This allows customization of port and other settings that may differ - from the main service configuration. - properties: - port: - description: |- - Port overrides the health check port. - If not set, the endpoint's serving port is used for health checks. - This is useful when health checks are served on a different port than - the main service port (e.g., port 443 for service, port 9090 for health checks). - format: int32 - maximum: 65535 - minimum: 1 - type: integer - type: object - tcp: - description: |- - TCP defines the configuration of tcp health checker. - It's required while the health checker type is TCP. - properties: - receive: - description: Receive defines the - expected response payload. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - send: - description: Send defines the - request payload. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - type: object - timeout: - default: 1s - description: Timeout defines the time - to wait for a health check response. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: - allOf: - - enum: - - HTTP - - TCP - - GRPC - - enum: - - HTTP - - TCP - - GRPC - description: Type defines the type - of health checker. - type: string - unhealthyThreshold: - default: 3 - description: |- - UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. - Without RetriableStatuses configured, any health check failure results in the host being immediately - considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried - up to this threshold before the host is marked unhealthy. - format: int32 - minimum: 1 - type: integer - required: - - type - type: object - x-kubernetes-validations: - - message: If Health Checker type is HTTP, - http field needs to be set. - rule: 'self.type == ''HTTP'' ? has(self.http) - : !has(self.http)' - - message: If Health Checker type is TCP, - tcp field needs to be set. - rule: 'self.type == ''TCP'' ? has(self.tcp) - : !has(self.tcp)' - - message: The grpc field can only be - set if the Health Checker type is - GRPC. - rule: 'has(self.grpc) ? self.type == - ''GRPC'' : true' - panicThreshold: - description: |- - When number of unhealthy endpoints for a backend reaches this threshold - Envoy will disregard health status and balance across all endpoints. - It's designed to prevent a situation in which host failures cascade throughout the cluster - as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. - format: int32 - maximum: 100 - minimum: 0 - type: integer - passive: - description: Passive passive check configuration - properties: - alwaysEjectOneEndpoint: - default: false - description: |- - AlwaysEjectOneEndpoint defines whether at least one host should be ejected, - regardless of MaxEjectionPercent. - type: boolean - baseEjectionTime: - default: 30s - description: BaseEjectionTime defines - the base duration for which a host - will be ejected on consecutive failures. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - consecutive5XxErrors: - default: 5 - description: Consecutive5xxErrors - sets the number of consecutive 5xx - errors triggering ejection. - format: int32 - type: integer - consecutiveGatewayErrors: - description: ConsecutiveGatewayErrors - sets the number of consecutive gateway - errors triggering ejection. - format: int32 - type: integer - consecutiveLocalOriginFailures: - default: 5 - description: |- - ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. - Parameter takes effect only when split_external_local_origin_errors is set to true. - format: int32 - type: integer - failurePercentageThreshold: - description: |- - FailurePercentageThreshold sets the failure percentage threshold for outlier detection. - If the failure percentage of a given host is greater than or equal to this value, it will be ejected. - Defaults to 85. - format: int32 - maximum: 100 - minimum: 0 - type: integer - interval: - default: 3s - description: Interval defines the - time between passive health checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxEjectionPercent: - default: 10 - description: MaxEjectionPercent sets - the maximum percentage of hosts - in a cluster that can be ejected. - format: int32 - maximum: 100 - minimum: 0 - type: integer - splitExternalLocalOriginErrors: - default: false - description: SplitExternalLocalOriginErrors - enables splitting of errors between - external and local origin. - type: boolean - type: object - type: object - http2: - description: HTTP2 provides HTTP/2 configuration - for backend connections. - properties: - connectionKeepalive: - description: ConnectionKeepalive configures - HTTP/2 connection keepalive using PING - frames. - properties: - idleInterval: - description: IdleInterval specifies - how long a connection must be idle - before a PING is sent. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - description: Interval specifies how - often to send HTTP/2 PING frames - to keep the connection alive. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - intervalJitter: - description: |- - IntervalJitter specifies a random jitter percentage added to each interval. - Defaults to 15% if not specified. - format: int32 - maximum: 100 - minimum: 0 - type: integer - timeout: - description: Timeout specifies how - long to wait for a PING response - before considering the connection - dead. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - x-kubernetes-validations: - - message: timeout must be less than interval - rule: '!has(self.timeout) || !has(self.interval) - || duration(self.timeout) < duration(self.interval)' - initialConnectionWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. - If not set, the default value is 1 MiB. - x-kubernetes-int-or-string: true - initialStreamWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: description: |- - InitialStreamWindowSize sets the initial window size for HTTP/2 streams. - If not set, the default value is 64 KiB(64*1024). - x-kubernetes-int-or-string: true - maxConcurrentStreams: + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: description: |- - MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. - If not set, the default value is 100. + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. format: int32 - maximum: 2147483647 + maximum: 65535 minimum: 1 type: integer - onInvalidMessage: - description: |- - OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error - It's recommended for L2 Envoy deployments to set this value to TerminateStream. - https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two - Default: TerminateConnection - type: string + required: + - name type: object - loadBalancer: + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + backendRefs: description: |- - LoadBalancer policy to apply when routing traffic from the gateway to - the backend endpoints. Defaults to `LeastRequest`. + BackendRefs references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + items: + description: BackendRef defines how an ObjectReference + that is specific to BackendRef. + properties: + fallback: + description: |- + Fallback indicates whether the backend is designated as a fallback. + Multiple fallback backends can be configured. + It is highly recommended to configure active or passive health checks to ensure that failover can be detected + when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + the health of the active backends falls below 72%. + type: boolean + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the + referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + maxItems: 16 + type: array + backendSettings: + description: |- + BackendSettings holds configuration for managing the connection + to the backend. properties: - backendUtilization: + circuitBreaker: description: |- - BackendUtilization defines the configuration when the load balancer type is - set to BackendUtilization. + Circuit Breaker settings for the upstream connections and requests. + If not set, circuit breakers will be enabled with the default thresholds properties: - blackoutPeriod: - description: |- - A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. - Default is 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - errorUtilizationPenaltyPercent: - description: |- - ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). - This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. - - For example: - - 100 => 1.0x - - 120 => 1.2x - - 200 => 2.0x - - Must be non-negative. - format: int32 + maxConnections: + default: 1024 + description: The maximum number of + connections that Envoy will establish + to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 minimum: 0 type: integer - keepResponseHeaders: - default: false - description: |- - KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. - Defaults to false. - type: boolean - metricNamesForComputingUtilization: + maxParallelRequests: + default: 1024 + description: The maximum number of + parallel requests that Envoy will + make to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRetries: + default: 1024 + description: The maximum number of + parallel retries that Envoy will + make to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxPendingRequests: + default: 1024 + description: The maximum number of + pending requests that Envoy will + queue to the referenced backend + defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxRequestsPerConnection: description: |- - Metric names used to compute utilization if application_utilization is not set. - For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". - items: - type: string - type: array - outOfBand: + The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. + Default: unlimited. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + perEndpoint: + description: PerEndpoint defines Circuit + Breakers that will apply per-endpoint + for an upstream cluster + properties: + maxConnections: + default: 1024 + description: MaxConnections configures + the maximum number of connections + that Envoy will establish per-endpoint + to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + type: object + retryBudget: description: |- - OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a - server-streaming gRPC connection to each endpoint's - xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load - reports periodically, instead of relying on in-band ORCA metrics - carried in response headers/trailers. - - The backend must implement OpenRcaService for this to take effect. + RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. + RetryBudget take precedence over maxParallelRetries. properties: - authority: - description: |- - Authority overrides the :authority header on the OutOfBand gRPC stream. - If unset, Envoy uses the endpoint hostname, then the dialed address, then - the cluster name. - maxLength: 259 - minLength: 1 - pattern: ^[^\x00\n\r]*$ - type: string - port: + minRetryConcurrency: description: |- - Port overrides the port used for the OutOfBand reporting connection, e.g. to - reach a separate reporting sidecar. Defaults to the endpoint's port. + MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. + For example, a budget of 20% with a minimum retry concurrency of 3 + will allow 5 active retries while there are 25 active requests. + If there are 2 active requests, there are still 3 active retries + allowed because of the minimum retry concurrency. + Defaults to 3. format: int32 - maximum: 65535 - minimum: 1 type: integer - reportingPeriod: + percent: description: |- - ReportingPeriod is how often Envoy requests load reports from the server. - Must be greater than 0. Defaults to 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + Percent specifies the limit on concurrent retries as a percentage [0, 100] of + the sum of active requests and active pending requests. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object x-kubernetes-validations: - - message: reportingPeriod must - be greater than 0 - rule: duration(self) > duration('0s') + - message: numerator must be less + than or equal to denominator + rule: self.numerator <= self.denominator + required: + - percent type: object - weightExpirationPeriod: - description: If a given endpoint has - not reported load metrics in this - long, stop using the reported weight. - Defaults to 3m. + type: object + connection: + description: Connection includes backend + connection settings. + properties: + bufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + BufferLimit Soft limit on size of the cluster’s connections read and write buffers. + BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. + If unspecified, an implementation defined default is applied (32768 bytes). + For example, 20Mi, 1Gi, 256Ki etc. + Note: that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + preconnect: + description: |- + Preconnect configures proactive upstream connections to reduce latency by establishing + connections before they’re needed and avoiding connection establishment overhead. + + If unset, Envoy will fetch connections as needed to serve in-flight requests. + properties: + perEndpointPercent: + description: |- + PerEndpointPercent configures how many additional connections to maintain per + upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a + percentage of the connections required by active streams + (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). + + Allowed value range is between 100-300. When both PerEndpointPercent and + PredictivePercent are set, Envoy ensures both are satisfied (max of the two). + format: int32 + maximum: 300 + minimum: 100 + type: integer + predictivePercent: + description: |- + PredictivePercent configures how many additional connections to maintain + across the cluster by anticipating which upstream endpoint the load balancer + will select next, useful for low-QPS services. Relies on deterministic + loadbalancing and is only supported with Random or RoundRobin. + Expressed as a percentage of the connections required by active streams + (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). + + Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are + set Envoy ensures both are satisfied per host (max of the two). + format: int32 + minimum: 100 + type: integer + type: object + socketBufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket + to backend. + SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. + For example, 20Mi, 1Gi, 256Ki etc. + Note that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + type: object + dns: + description: DNS includes dns resolution + settings. + properties: + dnsRefreshRate: + description: |- + DNSRefreshRate specifies the rate at which DNS records should be refreshed. + Defaults to 30 seconds. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - weightUpdatePeriod: - description: How often endpoint weights - are recalculated. Values less than - 100ms are capped at 100ms. Default - 1s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + lookupFamily: + description: |- + LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). + If set, this configuration overrides other defaults. + enum: + - IPv4 + - IPv6 + - IPv4Preferred + - IPv6Preferred + - IPv4AndIPv6 type: string + respectDnsTtl: + description: |- + RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. + If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. + Defaults to true. + type: boolean type: object - consistentHash: - description: |- - ConsistentHash defines the configuration when the load balancer type is - set to ConsistentHash + healthCheck: + description: HealthCheck allows gateway + to perform active health checking on + backends. properties: - cookie: - description: Cookie configures the - cookie hash policy when the consistent - hash type is set to Cookie. + active: + description: Active health check configuration properties: - attributes: - additionalProperties: - type: string - description: Additional Attributes - to set for the generated cookie. + grpc: + description: |- + GRPC defines the configuration of the GRPC health checker. + It's optional, and can only be used if the specified type is GRPC. + properties: + service: + description: |- + Service to send in the health check request. + If this is not specified, then the health check request applies to the entire + server and not to a specific service. + type: string type: object - name: + healthCheckLog: + description: |- + HealthCheckLog defines health check event logging configuration for this cluster. + When set, HC probe outcomes are logged to the configured sinks. + Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. + properties: + matches: + description: |- + Matches defines which health check probe outcomes produce a log entry. + When omitted or empty, all events are logged. + + Each value must be unique. Multiple values are ORed. If any failure type is + specified then a success type must also be specified, and vice versa. + items: + description: ProxyHealthCheckLogEventType + specifies which health + check probe outcomes produce + a log entry. + enum: + - Failure + - FailureSeriesStart + - Success + - HealthyTransition + type: string + maxItems: 4 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: a failure type + and a success type must + both be specified together + rule: self.exists(e, e == + 'Failure' || e == 'FailureSeriesStart') + == self.exists(e, e == + 'Success' || e == 'HealthyTransition') + sinks: + description: |- + Sinks defines where health check events are written. + When omitted, events are written to /dev/stdout. + items: + description: ProxyHealthCheckLogSink + defines a destination + for health check event + logs. + properties: + file: + description: |- + File defines the file sink configuration. + Required when type is File. + properties: + path: + description: |- + Path specifies the file path for health check event output. + Use /dev/stdout to write to standard output. + minLength: 1 + type: string + required: + - path + type: object + type: + description: Type defines + the type of sink. + enum: + - File + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If ProxyHealthCheckLogSink + type is File, file field + needs to be set. + rule: 'self.type == ''File'' + ? has(self.file) : !has(self.file)' + maxItems: 1 + type: array + type: object + healthyThreshold: + default: 1 + description: HealthyThreshold + defines the number of healthy + health checks required before + a backend host is marked healthy. + format: int32 + minimum: 1 + type: integer + http: + description: |- + HTTP defines the configuration of http health checker. + It's required while the health checker type is HTTP. + properties: + expectedResponse: + description: ExpectedResponse + defines a list of HTTP expected + responses to match. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + expectedStatuses: + description: |- + ExpectedStatuses defines a list of HTTP response statuses considered healthy. + Defaults to 200 only + items: + description: HTTPStatus + defines the http status + code. + maximum: 599 + minimum: 100 + type: integer + type: array + hostname: + description: |- + Hostname defines the HTTP Host header used for active HTTP health checks. + Host selection uses this order: this field, the associated Backend endpoint + hostname if available, then the effective Route hostname. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + method: + description: |- + Method defines the HTTP method used for health checking. + Defaults to GET + maxLength: 16 + type: string + path: + description: Path defines + the HTTP path that will + be requested during health + checking. + maxLength: 1024 + minLength: 1 + type: string + requestBody: + description: RequestBody defines + the HTTP request body payload + sent during health checking. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + retriableStatuses: + description: |- + RetriableStatuses defines a list of HTTP response statuses considered retriable. + Responses matching these statuses count towards the unhealthy threshold but + do not result in the host being considered immediately unhealthy. + The expected statuses take precedence for any range overlaps with this field. + items: + description: HTTPStatus + defines the http status + code. + maximum: 599 + minimum: 100 + type: integer + type: array + required: + - path + type: object + x-kubernetes-validations: + - message: The requestBody field + can only be set when method + is POST or PUT. + rule: '!has(self.requestBody) + || (has(self.method) && self.method.upperAscii() + in [''POST'',''PUT''])' + initialJitter: + description: |- + InitialJitter defines the maximum time Envoy will wait before the first health check. + Envoy will randomly select a value between 0 and the initial jitter value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + default: 3s + description: Interval defines + the time between active health + checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + overrides: + description: |- + Overrides defines the configuration of the overriding health check settings for all endpoints + in the backend cluster. This allows customization of port and other settings that may differ + from the main service configuration. + properties: + port: + description: |- + Port overrides the health check port. + If not set, the endpoint's serving port is used for health checks. + This is useful when health checks are served on a different port than + the main service port (e.g., port 443 for service, port 9090 for health checks). + format: int32 + maximum: 65535 + minimum: 1 + type: integer + type: object + tcp: + description: |- + TCP defines the configuration of tcp health checker. + It's required while the health checker type is TCP. + properties: + receive: + description: Receive defines + the expected response payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + send: + description: Send defines + the request payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + type: object + timeout: + default: 1s + description: Timeout defines the + time to wait for a health check + response. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: + allOf: + - enum: + - HTTP + - TCP + - GRPC + - enum: + - HTTP + - TCP + - GRPC + description: Type defines the + type of health checker. + type: string + unhealthyThreshold: + default: 3 + description: |- + UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. + Without RetriableStatuses configured, any health check failure results in the host being immediately + considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried + up to this threshold before the host is marked unhealthy. + format: int32 + minimum: 1 + type: integer + required: + - type + type: object + x-kubernetes-validations: + - message: If Health Checker type + is HTTP, http field needs to be + set. + rule: 'self.type == ''HTTP'' ? has(self.http) + : !has(self.http)' + - message: If Health Checker type + is TCP, tcp field needs to be + set. + rule: 'self.type == ''TCP'' ? has(self.tcp) + : !has(self.tcp)' + - message: The grpc field can only + be set if the Health Checker type + is GRPC. + rule: 'has(self.grpc) ? self.type + == ''GRPC'' : true' + panicThreshold: + description: |- + When number of unhealthy endpoints for a backend reaches this threshold + Envoy will disregard health status and balance across all endpoints. + It's designed to prevent a situation in which host failures cascade throughout the cluster + as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. + format: int32 + maximum: 100 + minimum: 0 + type: integer + passive: + description: Passive passive check + configuration + properties: + alwaysEjectOneEndpoint: + default: false description: |- - Name of the cookie to hash. - If this cookie does not exist in the request, Envoy will generate a cookie and set - the TTL on the response back to the client based on Layer 4 - attributes of the backend endpoint, to ensure that these future requests - go to the same backend endpoint. Make sure to set the TTL field for this case. + AlwaysEjectOneEndpoint defines whether at least one host should be ejected, + regardless of MaxEjectionPercent. + type: boolean + baseEjectionTime: + default: 30s + description: BaseEjectionTime + defines the base duration for + which a host will be ejected + on consecutive failures. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - ttl: + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors + sets the number of consecutive + 5xx errors triggering ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + description: ConsecutiveGatewayErrors + sets the number of consecutive + gateway errors triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 description: |- - TTL of the generated cookie if the cookie is not present. This value sets the - Max-Age attribute value. + ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors is set to true. + format: int32 + type: integer + failurePercentageThreshold: + description: |- + FailurePercentageThreshold sets the failure percentage threshold for outlier detection. + If the failure percentage of a given host is greater than or equal to this value, it will be ejected. + Defaults to 85. + format: int32 + maximum: 100 + minimum: 0 + type: integer + interval: + default: 3s + description: Interval defines + the time between passive health + checks. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - required: - - name + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent + sets the maximum percentage + of hosts in a cluster that can + be ejected. + format: int32 + maximum: 100 + minimum: 0 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors + enables splitting of errors + between external and local origin. + type: boolean type: object - header: - description: |- - Header configures the header hash policy when the consistent hash type is set to Header. - - Deprecated: use Headers instead + type: object + http2: + description: HTTP2 provides HTTP/2 configuration + for backend connections. + properties: + connectionKeepalive: + description: ConnectionKeepalive configures + HTTP/2 connection keepalive using + PING frames. properties: - name: - description: Name of the header - to hash. + idleInterval: + description: IdleInterval specifies + how long a connection must be + idle before a PING is sent. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: Interval specifies + how often to send HTTP/2 PING + frames to keep the connection + alive. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + intervalJitter: + description: |- + IntervalJitter specifies a random jitter percentage added to each interval. + Defaults to 15% if not specified. + format: int32 + maximum: 100 + minimum: 0 + type: integer + timeout: + description: Timeout specifies + how long to wait for a PING + response before considering + the connection dead. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - required: - - name type: object - headers: - description: Headers configures the - header hash policy for each header, - when the consistent hash type is - set to Headers. - items: - description: |- - Header defines the header hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the header - to hash. - type: string - required: - - name - type: object - type: array - queryParams: - description: QueryParams configures - the query parameter hash policy - when the consistent hash type is - set to QueryParams. - items: - description: |- - QueryParam defines the query parameter name hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the query - param to hash. - type: string - required: - - name - type: object - type: array - tableSize: - default: 65537 - description: The table size for consistent - hashing, must be prime number limited - to 5000011. - format: int64 - maximum: 5000011 - minimum: 2 + x-kubernetes-validations: + - message: timeout must be less than + interval + rule: '!has(self.timeout) || !has(self.interval) + || duration(self.timeout) < duration(self.interval)' + initialConnectionWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. + If not set, the default value is 1 MiB. + x-kubernetes-int-or-string: true + initialStreamWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialStreamWindowSize sets the initial window size for HTTP/2 streams. + If not set, the default value is 64 KiB(64*1024). + x-kubernetes-int-or-string: true + maxConcurrentStreams: + description: |- + MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. + If not set, the default value is 100. + format: int32 + maximum: 2147483647 + minimum: 1 type: integer - type: + onInvalidMessage: description: |- - ConsistentHashType defines the type of input to hash on. Valid Type values are - "SourceIP", - "Header", - "Headers", - "Cookie". - "QueryParams". - enum: - - SourceIP - - Header - - Headers - - Cookie - - QueryParams + OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error + It's recommended for L2 Envoy deployments to set this value to TerminateStream. + https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two + Default: TerminateConnection type: string - required: - - type type: object - x-kubernetes-validations: - - message: If consistent hash type is - header, the header field must be set. - rule: 'self.type == ''Header'' ? has(self.header) - : !has(self.header)' - - message: If consistent hash type is - headers, the headers field must be - set. - rule: 'self.type == ''Headers'' ? has(self.headers) - : !has(self.headers)' - - message: If consistent hash type is - cookie, the cookie field must be set. - rule: 'self.type == ''Cookie'' ? has(self.cookie) - : !has(self.cookie)' - - message: If consistent hash type is - queryParams, the queryParams field - must be set. - rule: 'self.type == ''QueryParams'' - ? has(self.queryParams) : !has(self.queryParams)' - dynamicModule: + loadBalancer: description: |- - DynamicModule defines the configuration when the load balancer type is - set to DynamicModule. The referenced module must be registered in the - EnvoyProxy resource's dynamicModules allowlist. + LoadBalancer policy to apply when routing traffic from the gateway to + the backend endpoints. Defaults to `LeastRequest`. properties: - config: - description: |- - Config is optional configuration for the module's load balancer - implementation. This is serialized and passed to the module's - initialization function. - x-kubernetes-preserve-unknown-fields: true - lbPolicyName: + backendUtilization: description: |- - LBPolicyName identifies a specific load balancer implementation within - the dynamic module. A single shared library can contain multiple LB - policy implementations. This value is passed to the module's - initialization function to select the appropriate implementation. - maxLength: 253 - minLength: 1 - type: string - name: + BackendUtilization defines the configuration when the load balancer type is + set to BackendUtilization. + properties: + blackoutPeriod: + description: |- + A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. + Default is 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + errorUtilizationPenaltyPercent: + description: |- + ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). + This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. + + For example: + - 100 => 1.0x + - 120 => 1.2x + - 200 => 2.0x + + Must be non-negative. + format: int32 + minimum: 0 + type: integer + keepResponseHeaders: + default: false + description: |- + KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. + Defaults to false. + type: boolean + metricNamesForComputingUtilization: + description: |- + Metric names used to compute utilization if application_utilization is not set. + For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". + items: + type: string + type: array + outOfBand: + description: |- + OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a + server-streaming gRPC connection to each endpoint's + xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load + reports periodically, instead of relying on in-band ORCA metrics + carried in response headers/trailers. + + The backend must implement OpenRcaService for this to take effect. + properties: + authority: + description: |- + Authority overrides the :authority header on the OutOfBand gRPC stream. + If unset, Envoy uses the endpoint hostname, then the dialed address, then + the cluster name. + maxLength: 259 + minLength: 1 + pattern: ^[^\x00\n\r]*$ + type: string + port: + description: |- + Port overrides the port used for the OutOfBand reporting connection, e.g. to + reach a separate reporting sidecar. Defaults to the endpoint's port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + reportingPeriod: + description: |- + ReportingPeriod is how often Envoy requests load reports from the server. + Must be greater than 0. Defaults to 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + x-kubernetes-validations: + - message: reportingPeriod + must be greater than 0 + rule: duration(self) > duration('0s') + type: object + weightExpirationPeriod: + description: If a given endpoint + has not reported load metrics + in this long, stop using the + reported weight. Defaults to + 3m. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + weightUpdatePeriod: + description: How often endpoint + weights are recalculated. Values + less than 100ms are capped at + 100ms. Default 1s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + consistentHash: description: |- - Name references a dynamic module registered in the EnvoyProxy resource's - dynamicModules list. The referenced module must exist in the registry; - otherwise, the policy will be rejected. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ - type: string - required: - - lbPolicyName - - name - type: object - endpointOverride: - description: |- - EndpointOverride defines the configuration for endpoint override. - When specified, the load balancer will attempt to route requests to endpoints - based on the override information extracted from request headers or metadata. - If the override endpoints are not available, the configured load balancer policy will be used as fallback. - properties: - extractFrom: - description: ExtractFrom defines the - sources to extract endpoint override - information from. - items: - description: EndpointOverrideExtractFrom - defines a source to extract endpoint - override information from. - properties: - header: + ConsistentHash defines the configuration when the load balancer type is + set to ConsistentHash + properties: + cookie: + description: Cookie configures + the cookie hash policy when + the consistent hash type is + set to Cookie. + properties: + attributes: + additionalProperties: + type: string + description: Additional Attributes + to set for the generated + cookie. + type: object + name: + description: |- + Name of the cookie to hash. + If this cookie does not exist in the request, Envoy will generate a cookie and set + the TTL on the response back to the client based on Layer 4 + attributes of the backend endpoint, to ensure that these future requests + go to the same backend endpoint. Make sure to set the TTL field for this case. + type: string + ttl: + description: |- + TTL of the generated cookie if the cookie is not present. This value sets the + Max-Age attribute value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + required: + - name + type: object + header: + description: |- + Header configures the header hash policy when the consistent hash type is set to Header. + + Deprecated: use Headers instead + properties: + name: + description: Name of the header + to hash. + type: string + required: + - name + type: object + headers: + description: Headers configures + the header hash policy for each + header, when the consistent + hash type is set to Headers. + items: description: |- - Header defines the header to get the override endpoint addresses. - The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. - For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. - The IPv6 address is enclosed in square brackets. - type: string - type: object - maxItems: 10 - minItems: 1 - type: array - required: - - extractFrom - type: object - slowStart: - description: |- - SlowStart defines the configuration related to the slow start load balancer policy. - If set, during slow start window, traffic sent to the newly added hosts will gradually increase. - Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. - properties: - window: + Header defines the header hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the + header to hash. + type: string + required: + - name + type: object + type: array + queryParams: + description: QueryParams configures + the query parameter hash policy + when the consistent hash type + is set to QueryParams. + items: + description: |- + QueryParam defines the query parameter name hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the + query param to hash. + type: string + required: + - name + type: object + type: array + tableSize: + default: 65537 + description: The table size for + consistent hashing, must be + prime number limited to 5000011. + format: int64 + maximum: 5000011 + minimum: 2 + type: integer + type: + description: |- + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Headers", + "Cookie". + "QueryParams". + enum: + - SourceIP + - Header + - Headers + - Cookie + - QueryParams + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If consistent hash type + is header, the header field must + be set. + rule: 'self.type == ''Header'' ? + has(self.header) : !has(self.header)' + - message: If consistent hash type + is headers, the headers field + must be set. + rule: 'self.type == ''Headers'' + ? has(self.headers) : !has(self.headers)' + - message: If consistent hash type + is cookie, the cookie field must + be set. + rule: 'self.type == ''Cookie'' ? + has(self.cookie) : !has(self.cookie)' + - message: If consistent hash type + is queryParams, the queryParams + field must be set. + rule: 'self.type == ''QueryParams'' + ? has(self.queryParams) : !has(self.queryParams)' + dynamicModule: + description: |- + DynamicModule defines the configuration when the load balancer type is + set to DynamicModule. The referenced module must be registered in the + EnvoyProxy resource's dynamicModules allowlist. + properties: + config: + description: |- + Config is optional configuration for the module's load balancer + implementation. This is serialized and passed to the module's + initialization function. + x-kubernetes-preserve-unknown-fields: true + lbPolicyName: + description: |- + LBPolicyName identifies a specific load balancer implementation within + the dynamic module. A single shared library can contain multiple LB + policy implementations. This value is passed to the module's + initialization function to select the appropriate implementation. + maxLength: 253 + minLength: 1 + type: string + name: + description: |- + Name references a dynamic module registered in the EnvoyProxy resource's + dynamicModules list. The referenced module must exist in the registry; + otherwise, the policy will be rejected. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - lbPolicyName + - name + type: object + endpointOverride: description: |- - Window defines the duration of the warm up period for newly added host. - During slow start window, traffic sent to the newly added hosts will gradually increase. - Currently only supports linear growth of traffic. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - required: - - window - type: object - type: - description: |- - Type decides the type of Load Balancer policy. - Valid LoadBalancerType values are - "ConsistentHash", - "LeastRequest", - "Random", - "RoundRobin", - "BackendUtilization", - "DynamicModule". - enum: - - ConsistentHash - - LeastRequest - - Random - - RoundRobin - - BackendUtilization - - DynamicModule - type: string - zoneAware: - description: ZoneAware defines the configuration - related to the distribution of requests - between locality zones. - properties: - preferLocal: - description: PreferLocalZone configures - zone-aware routing to prefer sending - traffic to the local locality zone. + EndpointOverride defines the configuration for endpoint override. + When specified, the load balancer will attempt to route requests to endpoints + based on the override information extracted from request headers or metadata. + If the override endpoints are not available, the configured load balancer policy will be used as fallback. properties: - force: + extractFrom: + description: ExtractFrom defines + the sources to extract endpoint + override information from. + items: + description: EndpointOverrideExtractFrom + defines a source to extract + endpoint override information + from. + properties: + header: + description: |- + Header defines the header to get the override endpoint addresses. + The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. + For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. + The IPv6 address is enclosed in square brackets. + type: string + type: object + maxItems: 10 + minItems: 1 + type: array + required: + - extractFrom + type: object + slowStart: + description: |- + SlowStart defines the configuration related to the slow start load balancer policy. + If set, during slow start window, traffic sent to the newly added hosts will gradually increase. + Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. + properties: + window: description: |- - ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior - which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. + Window defines the duration of the warm up period for newly added host. + During slow start window, traffic sent to the newly added hosts will gradually increase. + Currently only supports linear growth of traffic. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + required: + - window + type: object + type: + description: |- + Type decides the type of Load Balancer policy. + Valid LoadBalancerType values are + "ConsistentHash", + "LeastRequest", + "Random", + "RoundRobin", + "BackendUtilization", + "DynamicModule". + enum: + - ConsistentHash + - LeastRequest + - Random + - RoundRobin + - BackendUtilization + - DynamicModule + type: string + zoneAware: + description: ZoneAware defines the + configuration related to the distribution + of requests between locality zones. + properties: + preferLocal: + description: PreferLocalZone configures + zone-aware routing to prefer + sending traffic to the local + locality zone. properties: - minEndpointsInZoneThreshold: + force: description: |- - MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone - override. This is useful for protecting zones with fewer endpoints. + ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior + which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. + properties: + minEndpointsInZoneThreshold: + description: |- + MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone + override. This is useful for protecting zones with fewer endpoints. + format: int32 + type: integer + type: object + minEndpointsThreshold: + description: MinEndpointsThreshold + is the minimum number of + total upstream endpoints + across all zones required + to enable zone-aware routing. + format: int64 + type: integer + percentageEnabled: + description: Configures percentage + of requests that will be + considered for zone aware + routing if zone aware routing + is configured. If not specified, + Envoy defaults to 100%. format: int32 + maximum: 100 + minimum: 0 type: integer type: object - minEndpointsThreshold: - description: MinEndpointsThreshold - is the minimum number of total - upstream endpoints across all - zones required to enable zone-aware - routing. - format: int64 - type: integer - percentageEnabled: - description: Configures percentage - of requests that will be considered - for zone aware routing if zone - aware routing is configured. - If not specified, Envoy defaults - to 100%. - format: int32 - maximum: 100 - minimum: 0 - type: integer + weightedZones: + description: |- + WeightedZones configures weight-based traffic distribution across locality zones. + Traffic is distributed proportionally based on the sum of all zone weights. + items: + description: WeightedZoneConfig + defines the weight for a specific + locality zone. + properties: + weight: + description: |- + Weight defines the weight for this locality. + Higher values receive more traffic. The actual traffic distribution + is proportional to this value relative to other localities. + format: int32 + type: integer + zone: + description: |- + Zone specifies the topology zone this weight applies to. + The value should match the topology.kubernetes.io/zone label + of the nodes where endpoints are running. + Zones not listed in the configuration receive a default weight of 1. + type: string + required: + - weight + - zone + type: object + type: array + x-kubernetes-list-map-keys: + - zone + x-kubernetes-list-type: map type: object - weightedZones: + required: + - type + type: object + x-kubernetes-validations: + - message: If LoadBalancer type is consistentHash, + consistentHash field needs to be set. + rule: 'self.type == ''ConsistentHash'' + ? has(self.consistentHash) : !has(self.consistentHash)' + - message: If LoadBalancer type is BackendUtilization, + backendUtilization field needs to + be set. + rule: 'self.type == ''BackendUtilization'' + ? has(self.backendUtilization) : !has(self.backendUtilization)' + - message: If LoadBalancer type is DynamicModule, + dynamicModule field needs to be set. + rule: 'self.type == ''DynamicModule'' + ? has(self.dynamicModule) : !has(self.dynamicModule)' + - message: Currently SlowStart is only + supported for RoundRobin, LeastRequest, + and BackendUtilization load balancers. + rule: 'self.type in [''Random'', ''ConsistentHash'', + ''DynamicModule''] ? !has(self.slowStart) + : true' + - message: PreferLocal zone-aware routing + is not supported for ConsistentHash + load balancers. Use weightedZones + instead. + rule: 'self.type == ''ConsistentHash'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: PreferLocal zone-aware routing + is not currently supported for BackendUtilization + load balancers. Only WeightedZones + can be used with BackendUtilization. + rule: 'self.type == ''BackendUtilization'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: ZoneAware routing is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' + ? !has(self.zoneAware) : true' + - message: ZoneAware PreferLocal and WeightedZones + cannot be specified together. + rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) + && has(self.zoneAware.weightedZones)) + : true' + - message: EndpointOverride is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' + ? !has(self.endpointOverride) : true' + proxyProtocol: + description: ProxyProtocol enables the + Proxy Protocol when communicating with + the backend. + properties: + version: description: |- - WeightedZones configures weight-based traffic distribution across locality zones. - Traffic is distributed proportionally based on the sum of all zone weights. - items: - description: WeightedZoneConfig - defines the weight for a specific - locality zone. - properties: - weight: - description: |- - Weight defines the weight for this locality. - Higher values receive more traffic. The actual traffic distribution - is proportional to this value relative to other localities. - format: int32 - type: integer - zone: - description: |- - Zone specifies the topology zone this weight applies to. - The value should match the topology.kubernetes.io/zone label - of the nodes where endpoints are running. - Zones not listed in the configuration receive a default weight of 1. - type: string - required: - - weight - - zone - type: object - type: array - x-kubernetes-list-map-keys: - - zone - x-kubernetes-list-type: map + Version of ProxyProtocol + Valid ProxyProtocolVersion values are + "V1" + "V2" + enum: + - V1 + - V2 + type: string + required: + - version type: object - required: - - type - type: object - x-kubernetes-validations: - - message: If LoadBalancer type is consistentHash, - consistentHash field needs to be set. - rule: 'self.type == ''ConsistentHash'' ? - has(self.consistentHash) : !has(self.consistentHash)' - - message: If LoadBalancer type is BackendUtilization, - backendUtilization field needs to be set. - rule: 'self.type == ''BackendUtilization'' - ? has(self.backendUtilization) : !has(self.backendUtilization)' - - message: If LoadBalancer type is DynamicModule, - dynamicModule field needs to be set. - rule: 'self.type == ''DynamicModule'' ? - has(self.dynamicModule) : !has(self.dynamicModule)' - - message: Currently SlowStart is only supported - for RoundRobin, LeastRequest, and BackendUtilization - load balancers. - rule: 'self.type in [''Random'', ''ConsistentHash'', - ''DynamicModule''] ? !has(self.slowStart) - : true' - - message: PreferLocal zone-aware routing - is not supported for ConsistentHash load - balancers. Use weightedZones instead. - rule: 'self.type == ''ConsistentHash'' && - has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: PreferLocal zone-aware routing - is not currently supported for BackendUtilization - load balancers. Only WeightedZones can - be used with BackendUtilization. - rule: 'self.type == ''BackendUtilization'' - && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: ZoneAware routing is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' ? - !has(self.zoneAware) : true' - - message: ZoneAware PreferLocal and WeightedZones - cannot be specified together. - rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) - && has(self.zoneAware.weightedZones)) - : true' - - message: EndpointOverride is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' ? - !has(self.endpointOverride) : true' - proxyProtocol: - description: ProxyProtocol enables the Proxy - Protocol when communicating with the backend. - properties: - version: - description: |- - Version of ProxyProtocol - Valid ProxyProtocolVersion values are - "V1" - "V2" - enum: - - V1 - - V2 - type: string - required: - - version - type: object - retry: - description: |- - Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. - If not set, retry will be disabled. - properties: - numAttemptsPerPriority: + retry: description: |- - NumAttemptsPerPriority defines the number of requests (initial attempt + retries) - that should be sent to the same priority before switching to a different one. - If not specified or set to 0, all requests are sent to the highest priority that is healthy. - format: int32 - type: integer - numRetries: - default: 2 - description: NumRetries is the number - of retries to be attempted. Defaults - to 2. - format: int32 - minimum: 0 - type: integer - perRetry: - description: PerRetry is the retry policy - to be applied per retry attempt. + Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. + If not set, retry will be disabled. properties: - backOff: + numAttemptsPerPriority: description: |- - Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential - back-off algorithm for retries. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries - properties: - baseInterval: - description: BaseInterval is the - base interval between retries. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxInterval: + NumAttemptsPerPriority defines the number of requests (initial attempt + retries) + that should be sent to the same priority before switching to a different one. + If not specified or set to 0, all requests are sent to the highest priority that is healthy. + format: int32 + type: integer + numRetries: + default: 2 + description: NumRetries is the number + of retries to be attempted. Defaults + to 2. + format: int32 + minimum: 0 + type: integer + perRetry: + description: PerRetry is the retry + policy to be applied per retry attempt. + properties: + backOff: description: |- - MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. - The default is 10 times the base_interval + Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential + back-off algorithm for retries. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + properties: + baseInterval: + description: BaseInterval + is the base interval between + retries. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxInterval: + description: |- + MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. + The default is 10 times the base_interval + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + timeout: + description: Timeout is the timeout + per retry attempt. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string type: object - timeout: - description: Timeout is the timeout - per retry attempt. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - retryOn: - description: |- - RetryOn specifies the retry trigger condition. - - If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). - properties: - httpStatusCodes: + retryOn: description: |- - HttpStatusCodes specifies the http status codes to be retried. - The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array - triggers: - description: Triggers specifies the - retry trigger condition(Http/Grpc). - items: - description: TriggerEnum specifies - the conditions that trigger retries. - enum: - - 5xx - - gateway-error - - reset - - reset-before-request - - connect-failure - - retriable-4xx - - refused-stream - - retriable-status-codes - - cancelled - - deadline-exceeded - - internal - - resource-exhausted - - unavailable - type: string - type: array + RetryOn specifies the retry trigger condition. + + If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). + properties: + httpStatusCodes: + description: |- + HttpStatusCodes specifies the http status codes to be retried. + The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + triggers: + description: Triggers specifies + the retry trigger condition(Http/Grpc). + items: + description: TriggerEnum specifies + the conditions that trigger + retries. + enum: + - 5xx + - gateway-error + - reset + - reset-before-request + - connect-failure + - retriable-4xx + - refused-stream + - retriable-status-codes + - cancelled + - deadline-exceeded + - internal + - resource-exhausted + - unavailable + type: string + type: array + type: object type: object - type: object - tcpKeepalive: - description: |- - TcpKeepalive settings associated with the upstream client connection. - Disabled by default. - properties: - idleTime: - description: |- - The duration a connection needs to be idle before keep-alive - probes start being sent. - The duration format is - Defaults to `7200s`. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - description: |- - The duration between keep-alive probes. - Defaults to `75s`. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - probes: + tcpKeepalive: description: |- - The total number of unacknowledged probes to send before deciding - the connection is dead. - Defaults to 9. - format: int32 - type: integer - type: object - timeout: - description: Timeout settings for the backend - connections. - properties: - http: - description: Timeout settings for HTTP. + TcpKeepalive settings associated with the upstream client connection. + Disabled by default. properties: - connectionIdleTimeout: + idleTime: description: |- - The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. - Default: 1 hour. + The duration a connection needs to be idle before keep-alive + probes start being sent. + The duration format is + Defaults to `7200s`. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - maxConnectionDuration: + interval: description: |- - The maximum duration of an HTTP connection. - Default: unlimited. + The duration between keep-alive probes. + Defaults to `75s`. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - maxStreamDuration: + probes: description: |- - MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time - from when the request is sent until the response stream is fully consumed and does not apply to - non-streaming requests. - When set to "0s", no max duration is applied and streams can run indefinitely. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - requestTimeout: - description: RequestTimeout is the - time until which entire response - is received from the upstream. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - streamIdleTimeout: - description: |2- - The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. - If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + The total number of unacknowledged probes to send before deciding + the connection is dead. + Defaults to 9. + format: int32 + type: integer type: object - tcp: - description: Timeout settings for TCP. + timeout: + description: Timeout settings for the + backend connections. properties: - connectTimeout: - description: |- - The timeout for network connection establishment, including TCP and TLS handshakes. - Default: 10 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + http: + description: Timeout settings for + HTTP. + properties: + connectionIdleTimeout: + description: |- + The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + Default: 1 hour. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxConnectionDuration: + description: |- + The maximum duration of an HTTP connection. + Default: unlimited. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxStreamDuration: + description: |- + MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time + from when the request is sent until the response stream is fully consumed and does not apply to + non-streaming requests. + When set to "0s", no max duration is applied and streams can run indefinitely. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + requestTimeout: + description: RequestTimeout is + the time until which entire + response is received from the + upstream. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + streamIdleTimeout: + description: |2- + The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. + If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + tcp: + description: Timeout settings for + TCP. + properties: + connectTimeout: + description: |- + The timeout for network connection establishment, including TCP and TLS handshakes. + Default: 10 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object type: object type: object + x-kubernetes-validations: + - message: predictivePercent in preconnect + policy only works with RoundRobin or Random + load balancers + rule: '!((has(self.connection) && has(self.connection.preconnect) + && has(self.connection.preconnect.predictivePercent)) + && !(has(self.loadBalancer) && has(self.loadBalancer.type) + && self.loadBalancer.type in [''Random'', + ''RoundRobin'']))' + endSessionEndpoint: + description: |- + The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). + + If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. + EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. + type: string + issuer: + description: |- + The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). + Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST + be https, a host component, and optionally, port and path components and + no query or fragment components. + pattern: ^https://[^/?#@]+(/[^?#]*)?$ + type: string + tokenEndpoint: + description: |- + The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string + required: + - issuer type: object x-kubernetes-validations: - - message: predictivePercent in preconnect policy - only works with RoundRobin or Random load - balancers - rule: '!((has(self.connection) && has(self.connection.preconnect) - && has(self.connection.preconnect.predictivePercent)) - && !(has(self.loadBalancer) && has(self.loadBalancer.type) - && self.loadBalancer.type in [''Random'', - ''RoundRobin'']))' - endSessionEndpoint: + - message: BackendRefs must be used, backendRef + is not supported. + rule: '!has(self.backendRef)' + - message: Retry timeout is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? + !has(self.backendSettings.retry.perRetry.timeout):true):true):true + - message: HTTPStatusCodes is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? + !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true + redirectURL: description: |- - The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). - - If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. - EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. + The redirect URL to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" type: string - issuer: + refreshToken: + default: true description: |- - The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). - Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST - be https, a host component, and optionally, port and path components and - no query or fragment components. - pattern: ^https://[^/?#@]+(/[^?#]*)?$ - type: string - tokenEndpoint: + RefreshToken indicates whether the Envoy should automatically refresh the + id token and access token when they expire. + When set to true, the Envoy will use the refresh token to get a new id token + and access token when they expire. + + If not specified, defaults to true. + type: boolean + resources: description: |- - The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - type: string + The OIDC resources to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + items: + type: string + type: array + scopes: + description: |- + The OIDC scopes to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + The "openid" scope is always added to the list of scopes if not already + specified. + items: + type: string + type: array required: - - issuer + - clientSecret + - provider type: object x-kubernetes-validations: - - message: BackendRefs must be used, backendRef is - not supported. - rule: '!has(self.backendRef)' - - message: Retry timeout is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? - !has(self.backendSettings.retry.perRetry.timeout):true):true):true - - message: HTTPStatusCodes is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? - !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true - redirectURL: - description: |- - The redirect URL to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" - type: string - refreshToken: - default: true - description: |- - RefreshToken indicates whether the Envoy should automatically refresh the - id token and access token when they expire. - When set to true, the Envoy will use the refresh token to get a new id token - and access token when they expire. - - If not specified, defaults to true. - type: boolean - resources: - description: |- - The OIDC resources to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - items: - type: string - type: array - scopes: - description: |- - The OIDC scopes to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - The "openid" scope is always added to the list of scopes if not already - specified. - items: - type: string - type: array + - message: only one of clientID or clientIDRef must + be set + rule: (has(self.clientID) && !has(self.clientIDRef)) + || (!has(self.clientID) && has(self.clientIDRef)) + - message: forwardAccessToken cannot be true when + forwardIDToken.header is Authorization + rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken + && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() + == ''authorization'')' required: - - clientSecret - - provider + - oidc type: object - x-kubernetes-validations: - - message: only one of clientID or clientIDRef must be - set - rule: (has(self.clientID) && !has(self.clientIDRef)) - || (!has(self.clientID) && has(self.clientIDRef)) - - message: forwardAccessToken cannot be true when forwardIDToken.header - is Authorization - rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken - && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() - == ''authorization'')' spiffe: description: SPIFFE obtains the subject token as a JWT-SVID from the SPIFFE Workload API. @@ -14827,8 +14877,9 @@ spec: type: object type: object x-kubernetes-validations: - - message: Exactly one of oidc or spiffe must be specified - rule: has(self.oidc) != has(self.spiffe) + - message: Exactly one of oidcExchangeToken or spiffe must + be specified + rule: has(self.oidcExchangeToken) != has(self.spiffe) subjectTokenType: default: urn:ietf:params:oauth:token-type:jwt description: SubjectTokenType is the RFC 8693 subject_token_type, diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 7316055ab5..39df89898f 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -3881,6 +3881,7 @@ default service account credentials when running on GCP. **Appears in:** - [AWSOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-awsoidcexchangetoken) - [AzureOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-azureoidcexchangetoken) +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) - [GCPOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-gcpoidcexchangetoken) BackendSecurityPolicyOIDC specifies OIDC related fields. @@ -4069,10 +4070,10 @@ Exactly one source must be configured. Date: Wed, 7 Oct 2026 12:43:21 -0400 Subject: [PATCH 04/17] api: flatten OpenAI subject token, add spiffeJWTSVID Drop the subjectToken wrapper: oidcExchangeToken and the SPIFFE source now sit directly under tokenExchange, so "OIDC vs SPIFFE" can be spelled the same way if AWS/GCP/Azure gain a SPIFFE option later. Rename spiffe to spiffeJWTSVID (type BackendSecurityPolicySPIFFEJWTSVID) since nothing is exchanged when fetching it, and to leave room for an X.509-SVID source. Clarify which hop each audience/scopes field applies to. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 41 +- api/v1beta1/zz_generated.deepcopy.go | 42 +- ...envoyproxy.io_backendsecuritypolicies.yaml | 3688 ++++++++--------- site/docs/api/api.mdx | 67 +- tests/crdcel/main_test.go | 6 +- .../apikey_with_openai_credentials.yaml | 5 +- .../openai_invalid_socket_path.yaml | 7 +- .../openai_invalid_token_url.yaml | 5 +- .../openai_missing_subject_token.yaml | 1 - .../openai_multiple_subject_tokens.yaml | 19 +- .../backendsecuritypolicies/openai_oidc.yaml | 15 +- .../openai_spiffe.yaml | 7 +- .../openai_with_apikey.yaml | 5 +- 13 files changed, 1914 insertions(+), 1994 deletions(-) diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 9b785b851d..03f8a60b6b 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -376,10 +376,16 @@ type BackendSecurityPolicyOpenAICredentials struct { } // BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -// The controller obtains a subject token, exchanges it at TokenURL for an access token, -// and stores the access token in a secret, rotating it before it expires. +// The controller obtains a subject token from exactly one source (oidcExchangeToken or spiffeJWTSVID), +// exchanges it at TokenURL for an access token, and stores the access token in a secret, +// rotating it before it expires. +// +// TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply +// to either subject token source. // // https://datatracker.ietf.org/doc/html/rfc8693 +// +// +kubebuilder:validation:XValidation:rule="has(self.oidcExchangeToken) != has(self.spiffeJWTSVID)",message="Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified" type BackendSecurityPolicyTokenExchange struct { // TokenURL is the token exchange endpoint. // @@ -389,48 +395,41 @@ type BackendSecurityPolicyTokenExchange struct { TokenURL string `json:"tokenURL"` // SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token. + // Use "urn:ietf:params:oauth:token-type:access_token" when the OIDC provider returns an opaque access token. // // +optional // +kubebuilder:default="urn:ietf:params:oauth:token-type:jwt" // +kubebuilder:validation:MinLength=1 SubjectTokenType string `json:"subjectTokenType,omitempty"` - // Audience is the RFC 8693 audience parameter: the logical name of the target service. + // Audience is the RFC 8693 audience parameter: the logical name of the service the + // exchanged access token is intended for. It is unrelated to the audience of the subject token. // // +optional Audience string `json:"audience,omitempty"` - // Scopes is the list of scopes requested for the access token. + // Scopes is the list of scopes requested for the exchanged access token. They are unrelated to + // the scopes requested from the OIDC provider for the subject token. // // +optional // +kubebuilder:validation:MaxItems=16 Scopes []string `json:"scopes,omitempty"` - // SubjectToken specifies where the subject token is obtained from. - // - // +kubebuilder:validation:Required - SubjectToken BackendSecurityPolicySubjectToken `json:"subjectToken"` -} - -// BackendSecurityPolicySubjectToken specifies the source of a subject token. -// Exactly one source must be configured. -// -// +kubebuilder:validation:XValidation:rule="has(self.oidcExchangeToken) != has(self.spiffe)",message="Exactly one of oidcExchangeToken or spiffe must be specified" -type BackendSecurityPolicySubjectToken struct { - // OIDCExchangeToken obtains the subject token via an OIDC client-credentials flow. + // OIDCExchangeToken obtains the subject token from an OIDC provider via the client-credentials flow. // // +optional OIDCExchangeToken *BackendSecurityPolicyOIDC `json:"oidcExchangeToken,omitempty"` - // SPIFFE obtains the subject token as a JWT-SVID from the SPIFFE Workload API. + // SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API. // // +optional - SPIFFE *BackendSecurityPolicySPIFFE `json:"spiffe,omitempty"` + SPIFFEJWTSVID *BackendSecurityPolicySPIFFEJWTSVID `json:"spiffeJWTSVID,omitempty"` } -// BackendSecurityPolicySPIFFE specifies how to obtain a JWT-SVID from the SPIFFE Workload API. -type BackendSecurityPolicySPIFFE struct { - // Audience is the audience of the requested JWT-SVID. +// BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the SPIFFE Workload API. +type BackendSecurityPolicySPIFFEJWTSVID struct { + // Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint + // validates it, so it must match the audience that endpoint expects. // // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index e3ccc23fba..1a5fef3573 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -683,16 +683,16 @@ func (in *BackendSecurityPolicyOpenAICredentials) DeepCopy() *BackendSecurityPol } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicySPIFFE) DeepCopyInto(out *BackendSecurityPolicySPIFFE) { +func (in *BackendSecurityPolicySPIFFEJWTSVID) DeepCopyInto(out *BackendSecurityPolicySPIFFEJWTSVID) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySPIFFE. -func (in *BackendSecurityPolicySPIFFE) DeepCopy() *BackendSecurityPolicySPIFFE { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySPIFFEJWTSVID. +func (in *BackendSecurityPolicySPIFFEJWTSVID) DeepCopy() *BackendSecurityPolicySPIFFEJWTSVID { if in == nil { return nil } - out := new(BackendSecurityPolicySPIFFE) + out := new(BackendSecurityPolicySPIFFEJWTSVID) in.DeepCopyInto(out) return out } @@ -780,41 +780,25 @@ func (in *BackendSecurityPolicyStatus) DeepCopy() *BackendSecurityPolicyStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicySubjectToken) DeepCopyInto(out *BackendSecurityPolicySubjectToken) { +func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.OIDCExchangeToken != nil { in, out := &in.OIDCExchangeToken, &out.OIDCExchangeToken *out = new(BackendSecurityPolicyOIDC) (*in).DeepCopyInto(*out) } - if in.SPIFFE != nil { - in, out := &in.SPIFFE, &out.SPIFFE - *out = new(BackendSecurityPolicySPIFFE) + if in.SPIFFEJWTSVID != nil { + in, out := &in.SPIFFEJWTSVID, &out.SPIFFEJWTSVID + *out = new(BackendSecurityPolicySPIFFEJWTSVID) **out = **in } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySubjectToken. -func (in *BackendSecurityPolicySubjectToken) DeepCopy() *BackendSecurityPolicySubjectToken { - if in == nil { - return nil - } - out := new(BackendSecurityPolicySubjectToken) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { - *out = *in - if in.Scopes != nil { - in, out := &in.Scopes, &out.Scopes - *out = make([]string, len(*in)) - copy(*out, *in) - } - in.SubjectToken.DeepCopyInto(&out.SubjectToken) -} - // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyTokenExchange. func (in *BackendSecurityPolicyTokenExchange) DeepCopy() *BackendSecurityPolicyTokenExchange { if in == nil { diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 447a615786..7a09b59c1d 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -12844,2046 +12844,2007 @@ spec: an access token via OAuth 2.0 Token Exchange. properties: audience: - description: 'Audience is the RFC 8693 audience parameter: - the logical name of the target service.' + description: |- + Audience is the RFC 8693 audience parameter: the logical name of the service the + exchanged access token is intended for. It is unrelated to the audience of the subject token. type: string - scopes: - description: Scopes is the list of scopes requested for the - access token. - items: - type: string - maxItems: 16 - type: array - subjectToken: - description: SubjectToken specifies where the subject token - is obtained from. + oidcExchangeToken: + description: OIDCExchangeToken obtains the subject token from + an OIDC provider via the client-credentials flow. properties: - oidcExchangeToken: - description: OIDCExchangeToken obtains the subject token - via an OIDC client-credentials flow. + aud: + description: Aud defines the audience that this ID Token + is intended for. + type: string + grantType: + description: GrantType is the method application gets + access token. + type: string + oidc: + description: OIDC is used to obtain oidc tokens via an + SSO server which will be used to exchange for provider + credentials. properties: - aud: - description: Aud defines the audience that this ID - Token is intended for. - type: string - grantType: - description: GrantType is the method application gets - access token. + clientID: + description: |- + The client ID to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + Only one of clientID or clientIDRef must be set. + minLength: 1 type: string - oidc: - description: OIDC is used to obtain oidc tokens via - an SSO server which will be used to exchange for - provider credentials. + clientIDRef: + description: |- + The Kubernetes secret which contains the client ID to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Exactly one of clientID or clientIDRef must be set. + This is an Opaque secret. The client ID should be stored in the key "client-id". + + Only one of clientID or clientIDRef must be set. properties: - clientID: + group: + default: "" description: |- - The client ID to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - - Only one of clientID or clientIDRef must be set. + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For + example "Secret". + maxLength: 63 minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string - clientIDRef: - description: |- - The Kubernetes secret which contains the client ID to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - Exactly one of clientID or clientIDRef must be set. - This is an Opaque secret. The client ID should be stored in the key "client-id". - - Only one of clientID or clientIDRef must be set. - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. - For example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name - type: object - clientSecret: + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: description: |- - The Kubernetes secret which contains the OIDC client secret to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. - This is an Opaque secret. The client secret should be stored in the key - "client-secret". - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. - For example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + clientSecret: + description: |- + The Kubernetes secret which contains the OIDC client secret to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name - type: object - cookieConfig: - description: |- - CookieConfigs allows setting the SameSite attribute for OIDC cookies. - By default, its unset. - properties: - sameSite: - enum: - - Lax - - Strict - - None - type: string - type: object - cookieDomain: + This is an Opaque secret. The client secret should be stored in the key + "client-secret". + properties: + group: + default: "" description: |- - The optional domain to set the access and ID token cookies on. - If not set, the cookies will default to the host of the request, not including the subdomains. - If set, the cookies will be set on the specified domain and all subdomains. - This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string - cookieNames: - description: |- - The optional cookie name overrides to be used for Bearer and IdToken cookies in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses a randomly generated suffix - properties: - accessToken: - description: |- - The name of the cookie used to store the AccessToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "AccessToken-(randomly generated uid)" - type: string - idToken: - description: |- - The name of the cookie used to store the IdToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "IdToken-(randomly generated uid)" - type: string - type: object - csrfTokenTTL: + kind: + default: Secret + description: Kind is kind of the referent. For + example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: description: |- - CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. - This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token - in the "state" parameter when the provider redirects back to the callback endpoint. + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. - If omitted, Envoy Gateway defaults the token expiration to 10 minutes. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string - defaultRefreshTokenTTL: - description: |- - DefaultRefreshTokenTTL is the default lifetime of the refresh token. - This field is only used when the exp (expiration time) claim is omitted in - the refresh token or the refresh token is not JWT. - - If not specified, defaults to 604800s (one week). - Note: this field is only applicable when the "refreshToken" field is set to true. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + required: + - name + type: object + cookieConfig: + description: |- + CookieConfigs allows setting the SameSite attribute for OIDC cookies. + By default, its unset. + properties: + sameSite: + enum: + - Lax + - Strict + - None type: string - defaultTokenTTL: + type: object + cookieDomain: + description: |- + The optional domain to set the access and ID token cookies on. + If not set, the cookies will default to the host of the request, not including the subdomains. + If set, the cookies will be set on the specified domain and all subdomains. + This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + cookieNames: + description: |- + The optional cookie name overrides to be used for Bearer and IdToken cookies in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses a randomly generated suffix + properties: + accessToken: description: |- - DefaultTokenTTL is the default lifetime of the id token and access token. - Please note that Envoy will always use the expiry time from the response - of the authorization server if it is provided. This field is only used when - the expiry time is not provided by the authorization. - - If not specified, defaults to 0. In this case, the "expires_in" field in - the authorization response must be set by the authorization server, or the - OAuth flow will fail. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + The name of the cookie used to store the AccessToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "AccessToken-(randomly generated uid)" type: string - denyRedirect: - description: |- - Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. - This behavior can be useful for AJAX or machine requests. - properties: - headers: - description: Defines the headers to match - against the request to deny redirect to - the OIDC Provider. - items: - description: OIDCDenyRedirectHeader defines - how a header is matched - properties: - name: - description: Specifies the name of the - header in the request. - minLength: 1 - type: string - type: - default: Exact - description: Type specifies how to match - against a string. - enum: - - Exact - - Prefix - - Suffix - - RegularExpression - type: string - value: - description: Value specifies the string - value that the match must have. - maxLength: 1024 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - minItems: 1 - type: array - required: - - headers - type: object - disableTokenEncryption: - description: |- - Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. - This option should only be used in secure environments where token encryption is not required. - Default is false (tokens are encrypted). - type: boolean - forwardAccessToken: - description: |- - ForwardAccessToken indicates whether the Envoy should forward the access token - via the Authorization header Bearer scheme to the upstream. - If not specified, defaults to false. - type: boolean - forwardIDToken: + idToken: description: |- - ForwardIDToken configures forwarding of the OIDC ID token to the upstream. + The name of the cookie used to store the IdToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "IdToken-(randomly generated uid)" + type: string + type: object + csrfTokenTTL: + description: |- + CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. - If the configured header is "Authorization", EG forwards the ID token using - the "Bearer " prefix. For any other header, EG forwards the raw token value. - If not specified, the ID token will not be forwarded. + This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token + in the "state" parameter when the provider redirects back to the callback endpoint. - Note: when passThroughAuthHeader is enabled, this header must not be the same - as a header a JWT provider extracts from (the "Authorization" header by - default). The forwarded ID token header is owned by Envoy, and Envoy rejects - an OAuth2 configuration whose pass-through matcher keys on it. - properties: - header: - description: |- - Header is the upstream request header that will carry the ID token. - It must be a valid HTTP header name. Pseudo-headers (names starting with ":") - and the "Host" header are not allowed. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - x-kubernetes-validations: - - message: header cannot be the Host header - rule: self.lowerAscii() != 'host' - required: - - header - type: object - logoutPath: + If omitted, Envoy Gateway defaults the token expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + defaultRefreshTokenTTL: + description: |- + DefaultRefreshTokenTTL is the default lifetime of the refresh token. + This field is only used when the exp (expiration time) claim is omitted in + the refresh token or the refresh token is not JWT. + + If not specified, defaults to 604800s (one week). + Note: this field is only applicable when the "refreshToken" field is set to true. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + defaultTokenTTL: + description: |- + DefaultTokenTTL is the default lifetime of the id token and access token. + Please note that Envoy will always use the expiry time from the response + of the authorization server if it is provided. This field is only used when + the expiry time is not provided by the authorization. + + If not specified, defaults to 0. In this case, the "expires_in" field in + the authorization response must be set by the authorization server, or the + OAuth flow will fail. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + denyRedirect: + description: |- + Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. + This behavior can be useful for AJAX or machine requests. + properties: + headers: + description: Defines the headers to match against + the request to deny redirect to the OIDC Provider. + items: + description: OIDCDenyRedirectHeader defines + how a header is matched + properties: + name: + description: Specifies the name of the header + in the request. + minLength: 1 + type: string + type: + default: Exact + description: Type specifies how to match + against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string + value that the match must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - headers + type: object + disableTokenEncryption: + description: |- + Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. + This option should only be used in secure environments where token encryption is not required. + Default is false (tokens are encrypted). + type: boolean + forwardAccessToken: + description: |- + ForwardAccessToken indicates whether the Envoy should forward the access token + via the Authorization header Bearer scheme to the upstream. + If not specified, defaults to false. + type: boolean + forwardIDToken: + description: |- + ForwardIDToken configures forwarding of the OIDC ID token to the upstream. + + If the configured header is "Authorization", EG forwards the ID token using + the "Bearer " prefix. For any other header, EG forwards the raw token value. + If not specified, the ID token will not be forwarded. + + Note: when passThroughAuthHeader is enabled, this header must not be the same + as a header a JWT provider extracts from (the "Authorization" header by + default). The forwarded ID token header is owned by Envoy, and Envoy rejects + an OAuth2 configuration whose pass-through matcher keys on it. + properties: + header: description: |- - The path to log a user out, clearing their credential cookies. + Header is the upstream request header that will carry the ID token. + It must be a valid HTTP header name. Pseudo-headers (names starting with ":") + and the "Host" header are not allowed. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + x-kubernetes-validations: + - message: header cannot be the Host header + rule: self.lowerAscii() != 'host' + required: + - header + type: object + logoutPath: + description: |- + The path to log a user out, clearing their credential cookies. + + If not specified, uses a default logout path "/logout" + type: string + passThroughAuthHeader: + description: |- + Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless + explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. + + The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC + redirects and wish to directly supply a token instead. - If not specified, uses a default logout path "/logout" + If not specified, defaults to false. + type: boolean + provider: + description: The OIDC Provider configuration. + properties: + authorizationEndpoint: + description: |- + The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). type: string - passThroughAuthHeader: + backendRef: description: |- - Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless - explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. - - The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC - redirects and wish to directly supply a token instead. + BackendRef references a Kubernetes object that represents the + backend server to which the authorization request will be sent. - If not specified, defaults to false. - type: boolean - provider: - description: The OIDC Provider configuration. + Deprecated: Use BackendRefs instead. properties: - authorizationEndpoint: + group: + default: "" description: |- - The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string - backendRef: + kind: + default: Service description: |- - BackendRef references a Kubernetes object that represents the - backend server to which the authorization request will be sent. - - Deprecated: Use BackendRefs instead. - properties: - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". + Kind is the Kubernetes resource kind of the referent. For example + "Service". - Defaults to "Service" when not specified. + Defaults to "Service" when not specified. - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. - Support: Core (Services with a type other than ExternalName) + Support: Core (Services with a type other than ExternalName) - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind - == ''Service'') ? has(self.port) : true' - backendRefs: + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: description: |- - BackendRefs references a Kubernetes object that represents the - backend server to which the authorization request will be sent. - items: - description: BackendRef defines how an ObjectReference - that is specific to BackendRef. - properties: - fallback: - description: |- - Fallback indicates whether the backend is designated as a fallback. - Multiple fallback backends can be configured. - It is highly recommended to configure active or passive health checks to ensure that failover can be detected - when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. - The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when - the health of the active backends falls below 72%. - type: boolean - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == + ''Service'') ? has(self.port) : true' + backendRefs: + description: |- + BackendRefs references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + items: + description: BackendRef defines how an ObjectReference + that is specific to BackendRef. + properties: + fallback: + description: |- + Fallback indicates whether the backend is designated as a fallback. + Multiple fallback backends can be configured. + It is highly recommended to configure active or passive health checks to ensure that failover can be detected + when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + the health of the active backends falls below 72%. + type: boolean + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". - Defaults to "Service" when not specified. + Defaults to "Service" when not specified. - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. - Support: Core (Services with a type other than ExternalName) + Support: Core (Services with a type other than ExternalName) - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the - referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - weight: - default: 1 - description: |- - Weight specifies the proportion of requests forwarded to the referenced - backend. This is computed as weight/(sum of all weights in this - BackendRefs list). For non-zero values, there may be some epsilon from - the exact proportion defined here depending on the precision an - implementation supports. Weight is not a percentage and the sum of - weights does not need to equal 100. - - If only one backend is specified and it has a weight greater than 0, 100% - of the traffic is forwarded to that backend. If weight is set to 0, no - traffic should be forwarded for this entry. If unspecified, weight - defaults to 1. - - Support for this field varies based on the context where used. - format: int32 - maximum: 1000000 - minimum: 0 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind - == ''Service'') ? has(self.port) : true' - maxItems: 16 - type: array - backendSettings: + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + maxItems: 16 + type: array + backendSettings: + description: |- + BackendSettings holds configuration for managing the connection + to the backend. + properties: + circuitBreaker: description: |- - BackendSettings holds configuration for managing the connection - to the backend. + Circuit Breaker settings for the upstream connections and requests. + If not set, circuit breakers will be enabled with the default thresholds properties: - circuitBreaker: + maxConnections: + default: 1024 + description: The maximum number of connections + that Envoy will establish to the referenced + backend defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRequests: + default: 1024 + description: The maximum number of parallel + requests that Envoy will make to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRetries: + default: 1024 + description: The maximum number of parallel + retries that Envoy will make to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxPendingRequests: + default: 1024 + description: The maximum number of pending + requests that Envoy will queue to the + referenced backend defined within a + xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxRequestsPerConnection: description: |- - Circuit Breaker settings for the upstream connections and requests. - If not set, circuit breakers will be enabled with the default thresholds + The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. + Default: unlimited. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + perEndpoint: + description: PerEndpoint defines Circuit + Breakers that will apply per-endpoint + for an upstream cluster properties: maxConnections: default: 1024 - description: The maximum number of - connections that Envoy will establish + description: MaxConnections configures + the maximum number of connections + that Envoy will establish per-endpoint to the referenced backend defined within a xRoute rule. format: int64 maximum: 4294967295 minimum: 0 type: integer - maxParallelRequests: - default: 1024 - description: The maximum number of - parallel requests that Envoy will - make to the referenced backend defined - within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxParallelRetries: - default: 1024 - description: The maximum number of - parallel retries that Envoy will - make to the referenced backend defined - within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxPendingRequests: - default: 1024 - description: The maximum number of - pending requests that Envoy will - queue to the referenced backend - defined within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxRequestsPerConnection: + type: object + retryBudget: + description: |- + RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. + RetryBudget take precedence over maxParallelRetries. + properties: + minRetryConcurrency: description: |- - The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. - Default: unlimited. - format: int64 - maximum: 4294967295 - minimum: 0 + MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. + For example, a budget of 20% with a minimum retry concurrency of 3 + will allow 5 active retries while there are 25 active requests. + If there are 2 active requests, there are still 3 active retries + allowed because of the minimum retry concurrency. + Defaults to 3. + format: int32 type: integer - perEndpoint: - description: PerEndpoint defines Circuit - Breakers that will apply per-endpoint - for an upstream cluster - properties: - maxConnections: - default: 1024 - description: MaxConnections configures - the maximum number of connections - that Envoy will establish per-endpoint - to the referenced backend defined - within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - type: object - retryBudget: + percent: description: |- - RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. - RetryBudget take precedence over maxParallelRetries. - properties: - minRetryConcurrency: - description: |- - MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. - For example, a budget of 20% with a minimum retry concurrency of 3 - will allow 5 active retries while there are 25 active requests. - If there are 2 active requests, there are still 3 active retries - allowed because of the minimum retry concurrency. - Defaults to 3. - format: int32 - type: integer - percent: - description: |- - Percent specifies the limit on concurrent retries as a percentage [0, 100] of - the sum of active requests and active pending requests. - properties: - denominator: - default: 100 - format: int32 - minimum: 1 - type: integer - numerator: - format: int32 - minimum: 0 - type: integer - required: - - numerator - type: object - x-kubernetes-validations: - - message: numerator must be less - than or equal to denominator - rule: self.numerator <= self.denominator - required: - - percent - type: object - type: object - connection: - description: Connection includes backend - connection settings. - properties: - bufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - BufferLimit Soft limit on size of the cluster’s connections read and write buffers. - BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. - If unspecified, an implementation defined default is applied (32768 bytes). - For example, 20Mi, 1Gi, 256Ki etc. - Note: that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true - preconnect: - description: |- - Preconnect configures proactive upstream connections to reduce latency by establishing - connections before they’re needed and avoiding connection establishment overhead. - - If unset, Envoy will fetch connections as needed to serve in-flight requests. + Percent specifies the limit on concurrent retries as a percentage [0, 100] of + the sum of active requests and active pending requests. properties: - perEndpointPercent: - description: |- - PerEndpointPercent configures how many additional connections to maintain per - upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a - percentage of the connections required by active streams - (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). - - Allowed value range is between 100-300. When both PerEndpointPercent and - PredictivePercent are set, Envoy ensures both are satisfied (max of the two). + denominator: + default: 100 format: int32 - maximum: 300 - minimum: 100 + minimum: 1 type: integer - predictivePercent: - description: |- - PredictivePercent configures how many additional connections to maintain - across the cluster by anticipating which upstream endpoint the load balancer - will select next, useful for low-QPS services. Relies on deterministic - loadbalancing and is only supported with Random or RoundRobin. - Expressed as a percentage of the connections required by active streams - (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). - - Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are - set Envoy ensures both are satisfied per host (max of the two). + numerator: format: int32 - minimum: 100 + minimum: 0 type: integer + required: + - numerator type: object - socketBufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket - to backend. - SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. - For example, 20Mi, 1Gi, 256Ki etc. - Note that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: numerator must be less + than or equal to denominator + rule: self.numerator <= self.denominator + required: + - percent type: object - dns: - description: DNS includes dns resolution - settings. + type: object + connection: + description: Connection includes backend connection + settings. + properties: + bufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + BufferLimit Soft limit on size of the cluster’s connections read and write buffers. + BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. + If unspecified, an implementation defined default is applied (32768 bytes). + For example, 20Mi, 1Gi, 256Ki etc. + Note: that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + preconnect: + description: |- + Preconnect configures proactive upstream connections to reduce latency by establishing + connections before they’re needed and avoiding connection establishment overhead. + + If unset, Envoy will fetch connections as needed to serve in-flight requests. properties: - dnsRefreshRate: - description: |- - DNSRefreshRate specifies the rate at which DNS records should be refreshed. - Defaults to 30 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - lookupFamily: + perEndpointPercent: description: |- - LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). - If set, this configuration overrides other defaults. - enum: - - IPv4 - - IPv6 - - IPv4Preferred - - IPv6Preferred - - IPv4AndIPv6 - type: string - respectDnsTtl: + PerEndpointPercent configures how many additional connections to maintain per + upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a + percentage of the connections required by active streams + (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). + + Allowed value range is between 100-300. When both PerEndpointPercent and + PredictivePercent are set, Envoy ensures both are satisfied (max of the two). + format: int32 + maximum: 300 + minimum: 100 + type: integer + predictivePercent: description: |- - RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. - If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. - Defaults to true. - type: boolean - type: object - healthCheck: - description: HealthCheck allows gateway - to perform active health checking on - backends. + PredictivePercent configures how many additional connections to maintain + across the cluster by anticipating which upstream endpoint the load balancer + will select next, useful for low-QPS services. Relies on deterministic + loadbalancing and is only supported with Random or RoundRobin. + Expressed as a percentage of the connections required by active streams + (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). + + Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are + set Envoy ensures both are satisfied per host (max of the two). + format: int32 + minimum: 100 + type: integer + type: object + socketBufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket + to backend. + SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. + For example, 20Mi, 1Gi, 256Ki etc. + Note that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + type: object + dns: + description: DNS includes dns resolution settings. + properties: + dnsRefreshRate: + description: |- + DNSRefreshRate specifies the rate at which DNS records should be refreshed. + Defaults to 30 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + lookupFamily: + description: |- + LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). + If set, this configuration overrides other defaults. + enum: + - IPv4 + - IPv6 + - IPv4Preferred + - IPv6Preferred + - IPv4AndIPv6 + type: string + respectDnsTtl: + description: |- + RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. + If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. + Defaults to true. + type: boolean + type: object + healthCheck: + description: HealthCheck allows gateway to + perform active health checking on backends. + properties: + active: + description: Active health check configuration properties: - active: - description: Active health check configuration + grpc: + description: |- + GRPC defines the configuration of the GRPC health checker. + It's optional, and can only be used if the specified type is GRPC. properties: - grpc: + service: description: |- - GRPC defines the configuration of the GRPC health checker. - It's optional, and can only be used if the specified type is GRPC. - properties: - service: - description: |- - Service to send in the health check request. - If this is not specified, then the health check request applies to the entire - server and not to a specific service. - type: string - type: object - healthCheckLog: + Service to send in the health check request. + If this is not specified, then the health check request applies to the entire + server and not to a specific service. + type: string + type: object + healthCheckLog: + description: |- + HealthCheckLog defines health check event logging configuration for this cluster. + When set, HC probe outcomes are logged to the configured sinks. + Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. + properties: + matches: description: |- - HealthCheckLog defines health check event logging configuration for this cluster. - When set, HC probe outcomes are logged to the configured sinks. - Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. - properties: - matches: - description: |- - Matches defines which health check probe outcomes produce a log entry. - When omitted or empty, all events are logged. - - Each value must be unique. Multiple values are ORed. If any failure type is - specified then a success type must also be specified, and vice versa. - items: - description: ProxyHealthCheckLogEventType - specifies which health - check probe outcomes produce - a log entry. - enum: - - Failure - - FailureSeriesStart - - Success - - HealthyTransition - type: string - maxItems: 4 - type: array - x-kubernetes-list-type: set - x-kubernetes-validations: - - message: a failure type - and a success type must - both be specified together - rule: self.exists(e, e == - 'Failure' || e == 'FailureSeriesStart') - == self.exists(e, e == - 'Success' || e == 'HealthyTransition') - sinks: - description: |- - Sinks defines where health check events are written. - When omitted, events are written to /dev/stdout. - items: - description: ProxyHealthCheckLogSink - defines a destination - for health check event - logs. + Matches defines which health check probe outcomes produce a log entry. + When omitted or empty, all events are logged. + + Each value must be unique. Multiple values are ORed. If any failure type is + specified then a success type must also be specified, and vice versa. + items: + description: ProxyHealthCheckLogEventType + specifies which health check + probe outcomes produce a log + entry. + enum: + - Failure + - FailureSeriesStart + - Success + - HealthyTransition + type: string + maxItems: 4 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: a failure type and + a success type must both be + specified together + rule: self.exists(e, e == 'Failure' + || e == 'FailureSeriesStart') + == self.exists(e, e == 'Success' + || e == 'HealthyTransition') + sinks: + description: |- + Sinks defines where health check events are written. + When omitted, events are written to /dev/stdout. + items: + description: ProxyHealthCheckLogSink + defines a destination for + health check event logs. + properties: + file: + description: |- + File defines the file sink configuration. + Required when type is File. properties: - file: + path: description: |- - File defines the file sink configuration. - Required when type is File. - properties: - path: - description: |- - Path specifies the file path for health check event output. - Use /dev/stdout to write to standard output. - minLength: 1 - type: string - required: - - path - type: object - type: - description: Type defines - the type of sink. - enum: - - File + Path specifies the file path for health check event output. + Use /dev/stdout to write to standard output. + minLength: 1 type: string required: - - type + - path type: object - x-kubernetes-validations: - - message: If ProxyHealthCheckLogSink - type is File, file field - needs to be set. - rule: 'self.type == ''File'' - ? has(self.file) : !has(self.file)' - maxItems: 1 - type: array - type: object - healthyThreshold: - default: 1 - description: HealthyThreshold - defines the number of healthy - health checks required before - a backend host is marked healthy. - format: int32 - minimum: 1 - type: integer - http: - description: |- - HTTP defines the configuration of http health checker. - It's required while the health checker type is HTTP. + type: + description: Type defines + the type of sink. + enum: + - File + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If ProxyHealthCheckLogSink + type is File, file field + needs to be set. + rule: 'self.type == ''File'' + ? has(self.file) : !has(self.file)' + maxItems: 1 + type: array + type: object + healthyThreshold: + default: 1 + description: HealthyThreshold defines + the number of healthy health checks + required before a backend host is + marked healthy. + format: int32 + minimum: 1 + type: integer + http: + description: |- + HTTP defines the configuration of http health checker. + It's required while the health checker type is HTTP. + properties: + expectedResponse: + description: ExpectedResponse + defines a list of HTTP expected + responses to match. properties: - expectedResponse: - description: ExpectedResponse - defines a list of HTTP expected - responses to match. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type - is Text, text field needs - to be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type - is Binary, binary field - needs to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - expectedStatuses: - description: |- - ExpectedStatuses defines a list of HTTP response statuses considered healthy. - Defaults to 200 only - items: - description: HTTPStatus - defines the http status - code. - maximum: 599 - minimum: 100 - type: integer - type: array - hostname: - description: |- - Hostname defines the HTTP Host header used for active HTTP health checks. - Host selection uses this order: this field, the associated Backend endpoint - hostname if available, then the effective Route hostname. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + binary: + description: Binary payload + base64 encoded. + format: byte type: string - method: - description: |- - Method defines the HTTP method used for health checking. - Defaults to GET - maxLength: 16 + text: + description: Text payload + in plain text. type: string - path: - description: Path defines - the HTTP path that will - be requested during health - checking. - maxLength: 1024 - minLength: 1 + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. type: string - requestBody: - description: RequestBody defines - the HTTP request body payload - sent during health checking. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type - is Text, text field needs - to be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type - is Binary, binary field - needs to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - retriableStatuses: - description: |- - RetriableStatuses defines a list of HTTP response statuses considered retriable. - Responses matching these statuses count towards the unhealthy threshold but - do not result in the host being considered immediately unhealthy. - The expected statuses take precedence for any range overlaps with this field. - items: - description: HTTPStatus - defines the http status - code. - maximum: 599 - minimum: 100 - type: integer - type: array required: - - path + - type type: object x-kubernetes-validations: - - message: The requestBody field - can only be set when method - is POST or PUT. - rule: '!has(self.requestBody) - || (has(self.method) && self.method.upperAscii() - in [''POST'',''PUT''])' - initialJitter: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + expectedStatuses: description: |- - InitialJitter defines the maximum time Envoy will wait before the first health check. - Envoy will randomly select a value between 0 and the initial jitter value. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - default: 3s - description: Interval defines - the time between active health - checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - overrides: + ExpectedStatuses defines a list of HTTP response statuses considered healthy. + Defaults to 200 only + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + hostname: description: |- - Overrides defines the configuration of the overriding health check settings for all endpoints - in the backend cluster. This allows customization of port and other settings that may differ - from the main service configuration. - properties: - port: - description: |- - Port overrides the health check port. - If not set, the endpoint's serving port is used for health checks. - This is useful when health checks are served on a different port than - the main service port (e.g., port 443 for service, port 9090 for health checks). - format: int32 - maximum: 65535 - minimum: 1 - type: integer - type: object - tcp: + Hostname defines the HTTP Host header used for active HTTP health checks. + Host selection uses this order: this field, the associated Backend endpoint + hostname if available, then the effective Route hostname. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + method: description: |- - TCP defines the configuration of tcp health checker. - It's required while the health checker type is TCP. - properties: - receive: - description: Receive defines - the expected response payload. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type - is Text, text field needs - to be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type - is Binary, binary field - needs to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - send: - description: Send defines - the request payload. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type - is Text, text field needs - to be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type - is Binary, binary field - needs to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - type: object - timeout: - default: 1s - description: Timeout defines the - time to wait for a health check - response. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + Method defines the HTTP method used for health checking. + Defaults to GET + maxLength: 16 type: string - type: - allOf: - - enum: - - HTTP - - TCP - - GRPC - - enum: - - HTTP - - TCP - - GRPC - description: Type defines the - type of health checker. + path: + description: Path defines the + HTTP path that will be requested + during health checking. + maxLength: 1024 + minLength: 1 type: string - unhealthyThreshold: - default: 3 - description: |- - UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. - Without RetriableStatuses configured, any health check failure results in the host being immediately - considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried - up to this threshold before the host is marked unhealthy. - format: int32 - minimum: 1 - type: integer - required: - - type - type: object - x-kubernetes-validations: - - message: If Health Checker type - is HTTP, http field needs to be - set. - rule: 'self.type == ''HTTP'' ? has(self.http) - : !has(self.http)' - - message: If Health Checker type - is TCP, tcp field needs to be - set. - rule: 'self.type == ''TCP'' ? has(self.tcp) - : !has(self.tcp)' - - message: The grpc field can only - be set if the Health Checker type - is GRPC. - rule: 'has(self.grpc) ? self.type - == ''GRPC'' : true' - panicThreshold: - description: |- - When number of unhealthy endpoints for a backend reaches this threshold - Envoy will disregard health status and balance across all endpoints. - It's designed to prevent a situation in which host failures cascade throughout the cluster - as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. - format: int32 - maximum: 100 - minimum: 0 - type: integer - passive: - description: Passive passive check - configuration - properties: - alwaysEjectOneEndpoint: - default: false - description: |- - AlwaysEjectOneEndpoint defines whether at least one host should be ejected, - regardless of MaxEjectionPercent. - type: boolean - baseEjectionTime: - default: 30s - description: BaseEjectionTime - defines the base duration for - which a host will be ejected - on consecutive failures. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - consecutive5XxErrors: - default: 5 - description: Consecutive5xxErrors - sets the number of consecutive - 5xx errors triggering ejection. - format: int32 - type: integer - consecutiveGatewayErrors: - description: ConsecutiveGatewayErrors - sets the number of consecutive - gateway errors triggering ejection. - format: int32 - type: integer - consecutiveLocalOriginFailures: - default: 5 - description: |- - ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. - Parameter takes effect only when split_external_local_origin_errors is set to true. - format: int32 - type: integer - failurePercentageThreshold: - description: |- - FailurePercentageThreshold sets the failure percentage threshold for outlier detection. - If the failure percentage of a given host is greater than or equal to this value, it will be ejected. - Defaults to 85. - format: int32 - maximum: 100 - minimum: 0 - type: integer - interval: - default: 3s - description: Interval defines - the time between passive health - checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxEjectionPercent: - default: 10 - description: MaxEjectionPercent - sets the maximum percentage - of hosts in a cluster that can - be ejected. - format: int32 - maximum: 100 - minimum: 0 - type: integer - splitExternalLocalOriginErrors: - default: false - description: SplitExternalLocalOriginErrors - enables splitting of errors - between external and local origin. - type: boolean - type: object - type: object - http2: - description: HTTP2 provides HTTP/2 configuration - for backend connections. - properties: - connectionKeepalive: - description: ConnectionKeepalive configures - HTTP/2 connection keepalive using - PING frames. - properties: - idleInterval: - description: IdleInterval specifies - how long a connection must be - idle before a PING is sent. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - description: Interval specifies - how often to send HTTP/2 PING - frames to keep the connection - alive. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - intervalJitter: + requestBody: + description: RequestBody defines + the HTTP request body payload + sent during health checking. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + retriableStatuses: description: |- - IntervalJitter specifies a random jitter percentage added to each interval. - Defaults to 15% if not specified. - format: int32 - maximum: 100 - minimum: 0 - type: integer - timeout: - description: Timeout specifies - how long to wait for a PING - response before considering - the connection dead. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + RetriableStatuses defines a list of HTTP response statuses considered retriable. + Responses matching these statuses count towards the unhealthy threshold but + do not result in the host being considered immediately unhealthy. + The expected statuses take precedence for any range overlaps with this field. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + required: + - path type: object x-kubernetes-validations: - - message: timeout must be less than - interval - rule: '!has(self.timeout) || !has(self.interval) - || duration(self.timeout) < duration(self.interval)' - initialConnectionWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. - If not set, the default value is 1 MiB. - x-kubernetes-int-or-string: true - initialStreamWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - InitialStreamWindowSize sets the initial window size for HTTP/2 streams. - If not set, the default value is 64 KiB(64*1024). - x-kubernetes-int-or-string: true - maxConcurrentStreams: - description: |- - MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. - If not set, the default value is 100. - format: int32 - maximum: 2147483647 - minimum: 1 - type: integer - onInvalidMessage: + - message: The requestBody field can + only be set when method is POST + or PUT. + rule: '!has(self.requestBody) || + (has(self.method) && self.method.upperAscii() + in [''POST'',''PUT''])' + initialJitter: description: |- - OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error - It's recommended for L2 Envoy deployments to set this value to TerminateStream. - https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two - Default: TerminateConnection + InitialJitter defines the maximum time Envoy will wait before the first health check. + Envoy will randomly select a value between 0 and the initial jitter value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - type: object - loadBalancer: - description: |- - LoadBalancer policy to apply when routing traffic from the gateway to - the backend endpoints. Defaults to `LeastRequest`. - properties: - backendUtilization: + interval: + default: 3s + description: Interval defines the + time between active health checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + overrides: description: |- - BackendUtilization defines the configuration when the load balancer type is - set to BackendUtilization. + Overrides defines the configuration of the overriding health check settings for all endpoints + in the backend cluster. This allows customization of port and other settings that may differ + from the main service configuration. properties: - blackoutPeriod: - description: |- - A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. - Default is 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - errorUtilizationPenaltyPercent: + port: description: |- - ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). - This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. - - For example: - - 100 => 1.0x - - 120 => 1.2x - - 200 => 2.0x - - Must be non-negative. + Port overrides the health check port. + If not set, the endpoint's serving port is used for health checks. + This is useful when health checks are served on a different port than + the main service port (e.g., port 443 for service, port 9090 for health checks). format: int32 - minimum: 0 + maximum: 65535 + minimum: 1 type: integer - keepResponseHeaders: - default: false - description: |- - KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. - Defaults to false. - type: boolean - metricNamesForComputingUtilization: - description: |- - Metric names used to compute utilization if application_utilization is not set. - For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". - items: - type: string - type: array - outOfBand: - description: |- - OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a - server-streaming gRPC connection to each endpoint's - xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load - reports periodically, instead of relying on in-band ORCA metrics - carried in response headers/trailers. - - The backend must implement OpenRcaService for this to take effect. - properties: - authority: - description: |- - Authority overrides the :authority header on the OutOfBand gRPC stream. - If unset, Envoy uses the endpoint hostname, then the dialed address, then - the cluster name. - maxLength: 259 - minLength: 1 - pattern: ^[^\x00\n\r]*$ - type: string - port: - description: |- - Port overrides the port used for the OutOfBand reporting connection, e.g. to - reach a separate reporting sidecar. Defaults to the endpoint's port. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - reportingPeriod: - description: |- - ReportingPeriod is how often Envoy requests load reports from the server. - Must be greater than 0. Defaults to 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - x-kubernetes-validations: - - message: reportingPeriod - must be greater than 0 - rule: duration(self) > duration('0s') - type: object - weightExpirationPeriod: - description: If a given endpoint - has not reported load metrics - in this long, stop using the - reported weight. Defaults to - 3m. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - weightUpdatePeriod: - description: How often endpoint - weights are recalculated. Values - less than 100ms are capped at - 100ms. Default 1s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string type: object - consistentHash: + tcp: description: |- - ConsistentHash defines the configuration when the load balancer type is - set to ConsistentHash + TCP defines the configuration of tcp health checker. + It's required while the health checker type is TCP. properties: - cookie: - description: Cookie configures - the cookie hash policy when - the consistent hash type is - set to Cookie. + receive: + description: Receive defines the + expected response payload. properties: - attributes: - additionalProperties: - type: string - description: Additional Attributes - to set for the generated - cookie. - type: object - name: - description: |- - Name of the cookie to hash. - If this cookie does not exist in the request, Envoy will generate a cookie and set - the TTL on the response back to the client based on Layer 4 - attributes of the backend endpoint, to ensure that these future requests - go to the same backend endpoint. Make sure to set the TTL field for this case. + binary: + description: Binary payload + base64 encoded. + format: byte type: string - ttl: - description: |- - TTL of the generated cookie if the cookie is not present. This value sets the - Max-Age attribute value. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. type: string required: - - name + - type type: object - header: - description: |- - Header configures the header hash policy when the consistent hash type is set to Header. - - Deprecated: use Headers instead + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + send: + description: Send defines the + request payload. properties: - name: - description: Name of the header - to hash. + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. type: string required: - - name + - type type: object - headers: - description: Headers configures - the header hash policy for each - header, when the consistent - hash type is set to Headers. - items: - description: |- - Header defines the header hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the - header to hash. - type: string - required: - - name - type: object - type: array - queryParams: - description: QueryParams configures - the query parameter hash policy - when the consistent hash type - is set to QueryParams. - items: - description: |- - QueryParam defines the query parameter name hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the - query param to hash. - type: string - required: - - name - type: object - type: array - tableSize: - default: 65537 - description: The table size for - consistent hashing, must be - prime number limited to 5000011. - format: int64 - maximum: 5000011 - minimum: 2 - type: integer - type: - description: |- - ConsistentHashType defines the type of input to hash on. Valid Type values are - "SourceIP", - "Header", - "Headers", - "Cookie". - "QueryParams". - enum: - - SourceIP - - Header - - Headers - - Cookie - - QueryParams - type: string - required: - - type + x-kubernetes-validations: + - message: If payload type is + Text, text field needs to + be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type is + Binary, binary field needs + to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' type: object - x-kubernetes-validations: - - message: If consistent hash type - is header, the header field must - be set. - rule: 'self.type == ''Header'' ? - has(self.header) : !has(self.header)' - - message: If consistent hash type - is headers, the headers field - must be set. - rule: 'self.type == ''Headers'' - ? has(self.headers) : !has(self.headers)' - - message: If consistent hash type - is cookie, the cookie field must - be set. - rule: 'self.type == ''Cookie'' ? - has(self.cookie) : !has(self.cookie)' - - message: If consistent hash type - is queryParams, the queryParams - field must be set. - rule: 'self.type == ''QueryParams'' - ? has(self.queryParams) : !has(self.queryParams)' - dynamicModule: - description: |- - DynamicModule defines the configuration when the load balancer type is - set to DynamicModule. The referenced module must be registered in the - EnvoyProxy resource's dynamicModules allowlist. - properties: - config: - description: |- - Config is optional configuration for the module's load balancer - implementation. This is serialized and passed to the module's - initialization function. - x-kubernetes-preserve-unknown-fields: true - lbPolicyName: - description: |- - LBPolicyName identifies a specific load balancer implementation within - the dynamic module. A single shared library can contain multiple LB - policy implementations. This value is passed to the module's - initialization function to select the appropriate implementation. - maxLength: 253 + timeout: + default: 1s + description: Timeout defines the time + to wait for a health check response. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: + allOf: + - enum: + - HTTP + - TCP + - GRPC + - enum: + - HTTP + - TCP + - GRPC + description: Type defines the type + of health checker. + type: string + unhealthyThreshold: + default: 3 + description: |- + UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. + Without RetriableStatuses configured, any health check failure results in the host being immediately + considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried + up to this threshold before the host is marked unhealthy. + format: int32 + minimum: 1 + type: integer + required: + - type + type: object + x-kubernetes-validations: + - message: If Health Checker type is HTTP, + http field needs to be set. + rule: 'self.type == ''HTTP'' ? has(self.http) + : !has(self.http)' + - message: If Health Checker type is TCP, + tcp field needs to be set. + rule: 'self.type == ''TCP'' ? has(self.tcp) + : !has(self.tcp)' + - message: The grpc field can only be + set if the Health Checker type is + GRPC. + rule: 'has(self.grpc) ? self.type == + ''GRPC'' : true' + panicThreshold: + description: |- + When number of unhealthy endpoints for a backend reaches this threshold + Envoy will disregard health status and balance across all endpoints. + It's designed to prevent a situation in which host failures cascade throughout the cluster + as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. + format: int32 + maximum: 100 + minimum: 0 + type: integer + passive: + description: Passive passive check configuration + properties: + alwaysEjectOneEndpoint: + default: false + description: |- + AlwaysEjectOneEndpoint defines whether at least one host should be ejected, + regardless of MaxEjectionPercent. + type: boolean + baseEjectionTime: + default: 30s + description: BaseEjectionTime defines + the base duration for which a host + will be ejected on consecutive failures. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors + sets the number of consecutive 5xx + errors triggering ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + description: ConsecutiveGatewayErrors + sets the number of consecutive gateway + errors triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 + description: |- + ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors is set to true. + format: int32 + type: integer + failurePercentageThreshold: + description: |- + FailurePercentageThreshold sets the failure percentage threshold for outlier detection. + If the failure percentage of a given host is greater than or equal to this value, it will be ejected. + Defaults to 85. + format: int32 + maximum: 100 + minimum: 0 + type: integer + interval: + default: 3s + description: Interval defines the + time between passive health checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent sets + the maximum percentage of hosts + in a cluster that can be ejected. + format: int32 + maximum: 100 + minimum: 0 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors + enables splitting of errors between + external and local origin. + type: boolean + type: object + type: object + http2: + description: HTTP2 provides HTTP/2 configuration + for backend connections. + properties: + connectionKeepalive: + description: ConnectionKeepalive configures + HTTP/2 connection keepalive using PING + frames. + properties: + idleInterval: + description: IdleInterval specifies + how long a connection must be idle + before a PING is sent. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: Interval specifies how + often to send HTTP/2 PING frames + to keep the connection alive. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + intervalJitter: + description: |- + IntervalJitter specifies a random jitter percentage added to each interval. + Defaults to 15% if not specified. + format: int32 + maximum: 100 + minimum: 0 + type: integer + timeout: + description: Timeout specifies how + long to wait for a PING response + before considering the connection + dead. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: timeout must be less than interval + rule: '!has(self.timeout) || !has(self.interval) + || duration(self.timeout) < duration(self.interval)' + initialConnectionWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. + If not set, the default value is 1 MiB. + x-kubernetes-int-or-string: true + initialStreamWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialStreamWindowSize sets the initial window size for HTTP/2 streams. + If not set, the default value is 64 KiB(64*1024). + x-kubernetes-int-or-string: true + maxConcurrentStreams: + description: |- + MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. + If not set, the default value is 100. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + onInvalidMessage: + description: |- + OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error + It's recommended for L2 Envoy deployments to set this value to TerminateStream. + https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two + Default: TerminateConnection + type: string + type: object + loadBalancer: + description: |- + LoadBalancer policy to apply when routing traffic from the gateway to + the backend endpoints. Defaults to `LeastRequest`. + properties: + backendUtilization: + description: |- + BackendUtilization defines the configuration when the load balancer type is + set to BackendUtilization. + properties: + blackoutPeriod: + description: |- + A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. + Default is 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + errorUtilizationPenaltyPercent: + description: |- + ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). + This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. + + For example: + - 100 => 1.0x + - 120 => 1.2x + - 200 => 2.0x + + Must be non-negative. + format: int32 + minimum: 0 + type: integer + keepResponseHeaders: + default: false + description: |- + KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. + Defaults to false. + type: boolean + metricNamesForComputingUtilization: + description: |- + Metric names used to compute utilization if application_utilization is not set. + For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". + items: + type: string + type: array + outOfBand: + description: |- + OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a + server-streaming gRPC connection to each endpoint's + xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load + reports periodically, instead of relying on in-band ORCA metrics + carried in response headers/trailers. + + The backend must implement OpenRcaService for this to take effect. + properties: + authority: + description: |- + Authority overrides the :authority header on the OutOfBand gRPC stream. + If unset, Envoy uses the endpoint hostname, then the dialed address, then + the cluster name. + maxLength: 259 minLength: 1 + pattern: ^[^\x00\n\r]*$ type: string - name: + port: description: |- - Name references a dynamic module registered in the EnvoyProxy resource's - dynamicModules list. The referenced module must exist in the registry; - otherwise, the policy will be rejected. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + Port overrides the port used for the OutOfBand reporting connection, e.g. to + reach a separate reporting sidecar. Defaults to the endpoint's port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + reportingPeriod: + description: |- + ReportingPeriod is how often Envoy requests load reports from the server. + Must be greater than 0. Defaults to 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - required: - - lbPolicyName - - name + x-kubernetes-validations: + - message: reportingPeriod must + be greater than 0 + rule: duration(self) > duration('0s') type: object - endpointOverride: - description: |- - EndpointOverride defines the configuration for endpoint override. - When specified, the load balancer will attempt to route requests to endpoints - based on the override information extracted from request headers or metadata. - If the override endpoints are not available, the configured load balancer policy will be used as fallback. + weightExpirationPeriod: + description: If a given endpoint has + not reported load metrics in this + long, stop using the reported weight. + Defaults to 3m. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + weightUpdatePeriod: + description: How often endpoint weights + are recalculated. Values less than + 100ms are capped at 100ms. Default + 1s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + consistentHash: + description: |- + ConsistentHash defines the configuration when the load balancer type is + set to ConsistentHash + properties: + cookie: + description: Cookie configures the + cookie hash policy when the consistent + hash type is set to Cookie. properties: - extractFrom: - description: ExtractFrom defines - the sources to extract endpoint - override information from. - items: - description: EndpointOverrideExtractFrom - defines a source to extract - endpoint override information - from. - properties: - header: - description: |- - Header defines the header to get the override endpoint addresses. - The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. - For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. - The IPv6 address is enclosed in square brackets. - type: string - type: object - maxItems: 10 - minItems: 1 - type: array + attributes: + additionalProperties: + type: string + description: Additional Attributes + to set for the generated cookie. + type: object + name: + description: |- + Name of the cookie to hash. + If this cookie does not exist in the request, Envoy will generate a cookie and set + the TTL on the response back to the client based on Layer 4 + attributes of the backend endpoint, to ensure that these future requests + go to the same backend endpoint. Make sure to set the TTL field for this case. + type: string + ttl: + description: |- + TTL of the generated cookie if the cookie is not present. This value sets the + Max-Age attribute value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string required: - - extractFrom + - name type: object - slowStart: + header: description: |- - SlowStart defines the configuration related to the slow start load balancer policy. - If set, during slow start window, traffic sent to the newly added hosts will gradually increase. - Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. + Header configures the header hash policy when the consistent hash type is set to Header. + + Deprecated: use Headers instead properties: - window: - description: |- - Window defines the duration of the warm up period for newly added host. - During slow start window, traffic sent to the newly added hosts will gradually increase. - Currently only supports linear growth of traffic. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + name: + description: Name of the header + to hash. type: string required: - - window + - name type: object + headers: + description: Headers configures the + header hash policy for each header, + when the consistent hash type is + set to Headers. + items: + description: |- + Header defines the header hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the header + to hash. + type: string + required: + - name + type: object + type: array + queryParams: + description: QueryParams configures + the query parameter hash policy + when the consistent hash type is + set to QueryParams. + items: + description: |- + QueryParam defines the query parameter name hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the query + param to hash. + type: string + required: + - name + type: object + type: array + tableSize: + default: 65537 + description: The table size for consistent + hashing, must be prime number limited + to 5000011. + format: int64 + maximum: 5000011 + minimum: 2 + type: integer type: description: |- - Type decides the type of Load Balancer policy. - Valid LoadBalancerType values are - "ConsistentHash", - "LeastRequest", - "Random", - "RoundRobin", - "BackendUtilization", - "DynamicModule". + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Headers", + "Cookie". + "QueryParams". enum: - - ConsistentHash - - LeastRequest - - Random - - RoundRobin - - BackendUtilization - - DynamicModule + - SourceIP + - Header + - Headers + - Cookie + - QueryParams + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If consistent hash type is + header, the header field must be set. + rule: 'self.type == ''Header'' ? has(self.header) + : !has(self.header)' + - message: If consistent hash type is + headers, the headers field must be + set. + rule: 'self.type == ''Headers'' ? has(self.headers) + : !has(self.headers)' + - message: If consistent hash type is + cookie, the cookie field must be set. + rule: 'self.type == ''Cookie'' ? has(self.cookie) + : !has(self.cookie)' + - message: If consistent hash type is + queryParams, the queryParams field + must be set. + rule: 'self.type == ''QueryParams'' + ? has(self.queryParams) : !has(self.queryParams)' + dynamicModule: + description: |- + DynamicModule defines the configuration when the load balancer type is + set to DynamicModule. The referenced module must be registered in the + EnvoyProxy resource's dynamicModules allowlist. + properties: + config: + description: |- + Config is optional configuration for the module's load balancer + implementation. This is serialized and passed to the module's + initialization function. + x-kubernetes-preserve-unknown-fields: true + lbPolicyName: + description: |- + LBPolicyName identifies a specific load balancer implementation within + the dynamic module. A single shared library can contain multiple LB + policy implementations. This value is passed to the module's + initialization function to select the appropriate implementation. + maxLength: 253 + minLength: 1 + type: string + name: + description: |- + Name references a dynamic module registered in the EnvoyProxy resource's + dynamicModules list. The referenced module must exist in the registry; + otherwise, the policy will be rejected. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ type: string - zoneAware: - description: ZoneAware defines the - configuration related to the distribution - of requests between locality zones. - properties: - preferLocal: - description: PreferLocalZone configures - zone-aware routing to prefer - sending traffic to the local - locality zone. - properties: - force: - description: |- - ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior - which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. - properties: - minEndpointsInZoneThreshold: - description: |- - MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone - override. This is useful for protecting zones with fewer endpoints. - format: int32 - type: integer - type: object - minEndpointsThreshold: - description: MinEndpointsThreshold - is the minimum number of - total upstream endpoints - across all zones required - to enable zone-aware routing. - format: int64 - type: integer - percentageEnabled: - description: Configures percentage - of requests that will be - considered for zone aware - routing if zone aware routing - is configured. If not specified, - Envoy defaults to 100%. - format: int32 - maximum: 100 - minimum: 0 - type: integer - type: object - weightedZones: - description: |- - WeightedZones configures weight-based traffic distribution across locality zones. - Traffic is distributed proportionally based on the sum of all zone weights. - items: - description: WeightedZoneConfig - defines the weight for a specific - locality zone. - properties: - weight: - description: |- - Weight defines the weight for this locality. - Higher values receive more traffic. The actual traffic distribution - is proportional to this value relative to other localities. - format: int32 - type: integer - zone: - description: |- - Zone specifies the topology zone this weight applies to. - The value should match the topology.kubernetes.io/zone label - of the nodes where endpoints are running. - Zones not listed in the configuration receive a default weight of 1. - type: string - required: - - weight - - zone - type: object - type: array - x-kubernetes-list-map-keys: - - zone - x-kubernetes-list-type: map - type: object required: - - type + - lbPolicyName + - name type: object - x-kubernetes-validations: - - message: If LoadBalancer type is consistentHash, - consistentHash field needs to be set. - rule: 'self.type == ''ConsistentHash'' - ? has(self.consistentHash) : !has(self.consistentHash)' - - message: If LoadBalancer type is BackendUtilization, - backendUtilization field needs to - be set. - rule: 'self.type == ''BackendUtilization'' - ? has(self.backendUtilization) : !has(self.backendUtilization)' - - message: If LoadBalancer type is DynamicModule, - dynamicModule field needs to be set. - rule: 'self.type == ''DynamicModule'' - ? has(self.dynamicModule) : !has(self.dynamicModule)' - - message: Currently SlowStart is only - supported for RoundRobin, LeastRequest, - and BackendUtilization load balancers. - rule: 'self.type in [''Random'', ''ConsistentHash'', - ''DynamicModule''] ? !has(self.slowStart) - : true' - - message: PreferLocal zone-aware routing - is not supported for ConsistentHash - load balancers. Use weightedZones - instead. - rule: 'self.type == ''ConsistentHash'' - && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: PreferLocal zone-aware routing - is not currently supported for BackendUtilization - load balancers. Only WeightedZones - can be used with BackendUtilization. - rule: 'self.type == ''BackendUtilization'' - && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: ZoneAware routing is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' - ? !has(self.zoneAware) : true' - - message: ZoneAware PreferLocal and WeightedZones - cannot be specified together. - rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) - && has(self.zoneAware.weightedZones)) - : true' - - message: EndpointOverride is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' - ? !has(self.endpointOverride) : true' - proxyProtocol: - description: ProxyProtocol enables the - Proxy Protocol when communicating with - the backend. + endpointOverride: + description: |- + EndpointOverride defines the configuration for endpoint override. + When specified, the load balancer will attempt to route requests to endpoints + based on the override information extracted from request headers or metadata. + If the override endpoints are not available, the configured load balancer policy will be used as fallback. + properties: + extractFrom: + description: ExtractFrom defines the + sources to extract endpoint override + information from. + items: + description: EndpointOverrideExtractFrom + defines a source to extract endpoint + override information from. + properties: + header: + description: |- + Header defines the header to get the override endpoint addresses. + The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. + For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. + The IPv6 address is enclosed in square brackets. + type: string + type: object + maxItems: 10 + minItems: 1 + type: array + required: + - extractFrom + type: object + slowStart: + description: |- + SlowStart defines the configuration related to the slow start load balancer policy. + If set, during slow start window, traffic sent to the newly added hosts will gradually increase. + Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. properties: - version: + window: description: |- - Version of ProxyProtocol - Valid ProxyProtocolVersion values are - "V1" - "V2" - enum: - - V1 - - V2 + Window defines the duration of the warm up period for newly added host. + During slow start window, traffic sent to the newly added hosts will gradually increase. + Currently only supports linear growth of traffic. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string required: - - version + - window type: object - retry: + type: description: |- - Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. - If not set, retry will be disabled. + Type decides the type of Load Balancer policy. + Valid LoadBalancerType values are + "ConsistentHash", + "LeastRequest", + "Random", + "RoundRobin", + "BackendUtilization", + "DynamicModule". + enum: + - ConsistentHash + - LeastRequest + - Random + - RoundRobin + - BackendUtilization + - DynamicModule + type: string + zoneAware: + description: ZoneAware defines the configuration + related to the distribution of requests + between locality zones. properties: - numAttemptsPerPriority: - description: |- - NumAttemptsPerPriority defines the number of requests (initial attempt + retries) - that should be sent to the same priority before switching to a different one. - If not specified or set to 0, all requests are sent to the highest priority that is healthy. - format: int32 - type: integer - numRetries: - default: 2 - description: NumRetries is the number - of retries to be attempted. Defaults - to 2. - format: int32 - minimum: 0 - type: integer - perRetry: - description: PerRetry is the retry - policy to be applied per retry attempt. + preferLocal: + description: PreferLocalZone configures + zone-aware routing to prefer sending + traffic to the local locality zone. properties: - backOff: + force: description: |- - Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential - back-off algorithm for retries. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior + which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. properties: - baseInterval: - description: BaseInterval - is the base interval between - retries. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxInterval: + minEndpointsInZoneThreshold: description: |- - MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. - The default is 10 times the base_interval - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone + override. This is useful for protecting zones with fewer endpoints. + format: int32 + type: integer type: object - timeout: - description: Timeout is the timeout - per retry attempt. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + minEndpointsThreshold: + description: MinEndpointsThreshold + is the minimum number of total + upstream endpoints across all + zones required to enable zone-aware + routing. + format: int64 + type: integer + percentageEnabled: + description: Configures percentage + of requests that will be considered + for zone aware routing if zone + aware routing is configured. + If not specified, Envoy defaults + to 100%. + format: int32 + maximum: 100 + minimum: 0 + type: integer type: object - retryOn: + weightedZones: description: |- - RetryOn specifies the retry trigger condition. - - If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). - properties: - httpStatusCodes: - description: |- - HttpStatusCodes specifies the http status codes to be retried. - The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 + WeightedZones configures weight-based traffic distribution across locality zones. + Traffic is distributed proportionally based on the sum of all zone weights. + items: + description: WeightedZoneConfig + defines the weight for a specific + locality zone. + properties: + weight: + description: |- + Weight defines the weight for this locality. + Higher values receive more traffic. The actual traffic distribution + is proportional to this value relative to other localities. + format: int32 type: integer - type: array - triggers: - description: Triggers specifies - the retry trigger condition(Http/Grpc). - items: - description: TriggerEnum specifies - the conditions that trigger - retries. - enum: - - 5xx - - gateway-error - - reset - - reset-before-request - - connect-failure - - retriable-4xx - - refused-stream - - retriable-status-codes - - cancelled - - deadline-exceeded - - internal - - resource-exhausted - - unavailable + zone: + description: |- + Zone specifies the topology zone this weight applies to. + The value should match the topology.kubernetes.io/zone label + of the nodes where endpoints are running. + Zones not listed in the configuration receive a default weight of 1. type: string - type: array + required: + - weight + - zone + type: object + type: array + x-kubernetes-list-map-keys: + - zone + x-kubernetes-list-type: map + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: If LoadBalancer type is consistentHash, + consistentHash field needs to be set. + rule: 'self.type == ''ConsistentHash'' ? + has(self.consistentHash) : !has(self.consistentHash)' + - message: If LoadBalancer type is BackendUtilization, + backendUtilization field needs to be set. + rule: 'self.type == ''BackendUtilization'' + ? has(self.backendUtilization) : !has(self.backendUtilization)' + - message: If LoadBalancer type is DynamicModule, + dynamicModule field needs to be set. + rule: 'self.type == ''DynamicModule'' ? + has(self.dynamicModule) : !has(self.dynamicModule)' + - message: Currently SlowStart is only supported + for RoundRobin, LeastRequest, and BackendUtilization + load balancers. + rule: 'self.type in [''Random'', ''ConsistentHash'', + ''DynamicModule''] ? !has(self.slowStart) + : true' + - message: PreferLocal zone-aware routing + is not supported for ConsistentHash load + balancers. Use weightedZones instead. + rule: 'self.type == ''ConsistentHash'' && + has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: PreferLocal zone-aware routing + is not currently supported for BackendUtilization + load balancers. Only WeightedZones can + be used with BackendUtilization. + rule: 'self.type == ''BackendUtilization'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: ZoneAware routing is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' ? + !has(self.zoneAware) : true' + - message: ZoneAware PreferLocal and WeightedZones + cannot be specified together. + rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) + && has(self.zoneAware.weightedZones)) + : true' + - message: EndpointOverride is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' ? + !has(self.endpointOverride) : true' + proxyProtocol: + description: ProxyProtocol enables the Proxy + Protocol when communicating with the backend. + properties: + version: + description: |- + Version of ProxyProtocol + Valid ProxyProtocolVersion values are + "V1" + "V2" + enum: + - V1 + - V2 + type: string + required: + - version + type: object + retry: + description: |- + Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. + If not set, retry will be disabled. + properties: + numAttemptsPerPriority: + description: |- + NumAttemptsPerPriority defines the number of requests (initial attempt + retries) + that should be sent to the same priority before switching to a different one. + If not specified or set to 0, all requests are sent to the highest priority that is healthy. + format: int32 + type: integer + numRetries: + default: 2 + description: NumRetries is the number + of retries to be attempted. Defaults + to 2. + format: int32 + minimum: 0 + type: integer + perRetry: + description: PerRetry is the retry policy + to be applied per retry attempt. + properties: + backOff: + description: |- + Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential + back-off algorithm for retries. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + properties: + baseInterval: + description: BaseInterval is the + base interval between retries. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxInterval: + description: |- + MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. + The default is 10 times the base_interval + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string type: object + timeout: + description: Timeout is the timeout + per retry attempt. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + retryOn: + description: |- + RetryOn specifies the retry trigger condition. + + If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). + properties: + httpStatusCodes: + description: |- + HttpStatusCodes specifies the http status codes to be retried. + The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + triggers: + description: Triggers specifies the + retry trigger condition(Http/Grpc). + items: + description: TriggerEnum specifies + the conditions that trigger retries. + enum: + - 5xx + - gateway-error + - reset + - reset-before-request + - connect-failure + - retriable-4xx + - refused-stream + - retriable-status-codes + - cancelled + - deadline-exceeded + - internal + - resource-exhausted + - unavailable + type: string + type: array type: object - tcpKeepalive: + type: object + tcpKeepalive: + description: |- + TcpKeepalive settings associated with the upstream client connection. + Disabled by default. + properties: + idleTime: + description: |- + The duration a connection needs to be idle before keep-alive + probes start being sent. + The duration format is + Defaults to `7200s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: |- + The duration between keep-alive probes. + Defaults to `75s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + probes: description: |- - TcpKeepalive settings associated with the upstream client connection. - Disabled by default. + The total number of unacknowledged probes to send before deciding + the connection is dead. + Defaults to 9. + format: int32 + type: integer + type: object + timeout: + description: Timeout settings for the backend + connections. + properties: + http: + description: Timeout settings for HTTP. properties: - idleTime: + connectionIdleTimeout: description: |- - The duration a connection needs to be idle before keep-alive - probes start being sent. - The duration format is - Defaults to `7200s`. + The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + Default: 1 hour. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - interval: + maxConnectionDuration: description: |- - The duration between keep-alive probes. - Defaults to `75s`. + The maximum duration of an HTTP connection. + Default: unlimited. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - probes: + maxStreamDuration: description: |- - The total number of unacknowledged probes to send before deciding - the connection is dead. - Defaults to 9. - format: int32 - type: integer + MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time + from when the request is sent until the response stream is fully consumed and does not apply to + non-streaming requests. + When set to "0s", no max duration is applied and streams can run indefinitely. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + requestTimeout: + description: RequestTimeout is the + time until which entire response + is received from the upstream. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + streamIdleTimeout: + description: |2- + The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. + If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string type: object - timeout: - description: Timeout settings for the - backend connections. + tcp: + description: Timeout settings for TCP. properties: - http: - description: Timeout settings for - HTTP. - properties: - connectionIdleTimeout: - description: |- - The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. - Default: 1 hour. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxConnectionDuration: - description: |- - The maximum duration of an HTTP connection. - Default: unlimited. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxStreamDuration: - description: |- - MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time - from when the request is sent until the response stream is fully consumed and does not apply to - non-streaming requests. - When set to "0s", no max duration is applied and streams can run indefinitely. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - requestTimeout: - description: RequestTimeout is - the time until which entire - response is received from the - upstream. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - streamIdleTimeout: - description: |2- - The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. - If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - tcp: - description: Timeout settings for - TCP. - properties: - connectTimeout: - description: |- - The timeout for network connection establishment, including TCP and TLS handshakes. - Default: 10 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object + connectTimeout: + description: |- + The timeout for network connection establishment, including TCP and TLS handshakes. + Default: 10 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string type: object type: object - x-kubernetes-validations: - - message: predictivePercent in preconnect - policy only works with RoundRobin or Random - load balancers - rule: '!((has(self.connection) && has(self.connection.preconnect) - && has(self.connection.preconnect.predictivePercent)) - && !(has(self.loadBalancer) && has(self.loadBalancer.type) - && self.loadBalancer.type in [''Random'', - ''RoundRobin'']))' - endSessionEndpoint: - description: |- - The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). - - If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. - EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. - type: string - issuer: - description: |- - The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). - Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST - be https, a host component, and optionally, port and path components and - no query or fragment components. - pattern: ^https://[^/?#@]+(/[^?#]*)?$ - type: string - tokenEndpoint: - description: |- - The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - type: string - required: - - issuer type: object x-kubernetes-validations: - - message: BackendRefs must be used, backendRef - is not supported. - rule: '!has(self.backendRef)' - - message: Retry timeout is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? - !has(self.backendSettings.retry.perRetry.timeout):true):true):true - - message: HTTPStatusCodes is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? - !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true - redirectURL: + - message: predictivePercent in preconnect policy + only works with RoundRobin or Random load + balancers + rule: '!((has(self.connection) && has(self.connection.preconnect) + && has(self.connection.preconnect.predictivePercent)) + && !(has(self.loadBalancer) && has(self.loadBalancer.type) + && self.loadBalancer.type in [''Random'', + ''RoundRobin'']))' + endSessionEndpoint: description: |- - The redirect URL to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" + The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). + + If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. + EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. type: string - refreshToken: - default: true - description: |- - RefreshToken indicates whether the Envoy should automatically refresh the - id token and access token when they expire. - When set to true, the Envoy will use the refresh token to get a new id token - and access token when they expire. - - If not specified, defaults to true. - type: boolean - resources: + issuer: description: |- - The OIDC resources to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - items: - type: string - type: array - scopes: + The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). + Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST + be https, a host component, and optionally, port and path components and + no query or fragment components. + pattern: ^https://[^/?#@]+(/[^?#]*)?$ + type: string + tokenEndpoint: description: |- - The OIDC scopes to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - The "openid" scope is always added to the list of scopes if not already - specified. - items: - type: string - type: array + The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string required: - - clientSecret - - provider + - issuer type: object x-kubernetes-validations: - - message: only one of clientID or clientIDRef must - be set - rule: (has(self.clientID) && !has(self.clientIDRef)) - || (!has(self.clientID) && has(self.clientIDRef)) - - message: forwardAccessToken cannot be true when - forwardIDToken.header is Authorization - rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken - && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() - == ''authorization'')' - required: - - oidc - type: object - spiffe: - description: SPIFFE obtains the subject token as a JWT-SVID - from the SPIFFE Workload API. - properties: - audience: - description: Audience is the audience of the requested - JWT-SVID. - minLength: 1 - type: string - socketPath: + - message: BackendRefs must be used, backendRef is + not supported. + rule: '!has(self.backendRef)' + - message: Retry timeout is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? + !has(self.backendSettings.retry.perRetry.timeout):true):true):true + - message: HTTPStatusCodes is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? + !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true + redirectURL: description: |- - SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". - When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. - pattern: ^(unix|tcp)://.+ + The redirect URL to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" type: string + refreshToken: + default: true + description: |- + RefreshToken indicates whether the Envoy should automatically refresh the + id token and access token when they expire. + When set to true, the Envoy will use the refresh token to get a new id token + and access token when they expire. + + If not specified, defaults to true. + type: boolean + resources: + description: |- + The OIDC resources to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + items: + type: string + type: array + scopes: + description: |- + The OIDC scopes to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + The "openid" scope is always added to the list of scopes if not already + specified. + items: + type: string + type: array required: - - audience + - clientSecret + - provider type: object + x-kubernetes-validations: + - message: only one of clientID or clientIDRef must be + set + rule: (has(self.clientID) && !has(self.clientIDRef)) + || (!has(self.clientID) && has(self.clientIDRef)) + - message: forwardAccessToken cannot be true when forwardIDToken.header + is Authorization + rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken + && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() + == ''authorization'')' + required: + - oidc + type: object + scopes: + description: |- + Scopes is the list of scopes requested for the exchanged access token. They are unrelated to + the scopes requested from the OIDC provider for the subject token. + items: + type: string + maxItems: 16 + type: array + spiffeJWTSVID: + description: SPIFFEJWTSVID obtains the subject token as a + JWT-SVID from the SPIFFE Workload API. + properties: + audience: + description: |- + Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint + validates it, so it must match the audience that endpoint expects. + minLength: 1 + type: string + socketPath: + description: |- + SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". + When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. + pattern: ^(unix|tcp)://.+ + type: string + required: + - audience type: object - x-kubernetes-validations: - - message: Exactly one of oidcExchangeToken or spiffe must - be specified - rule: has(self.oidcExchangeToken) != has(self.spiffe) subjectTokenType: default: urn:ietf:params:oauth:token-type:jwt - description: SubjectTokenType is the RFC 8693 subject_token_type, - identifying the format of the subject token. + description: |- + SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token. + Use "urn:ietf:params:oauth:token-type:access_token" when the OIDC provider returns an opaque access token. minLength: 1 type: string tokenURL: @@ -14894,9 +14855,12 @@ spec: - message: tokenURL must be a valid URL rule: isURL(self) required: - - subjectToken - tokenURL type: object + x-kubernetes-validations: + - message: Exactly one of oidcExchangeToken or spiffeJWTSVID must + be specified + rule: has(self.oidcExchangeToken) != has(self.spiffeJWTSVID) required: - tokenExchange type: object diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 39df89898f..2ffca28c52 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -3188,10 +3188,9 @@ MCPRouteList contains a list of MCPRoute. - [BackendSecurityPolicyGCPCredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicygcpcredentials) - [BackendSecurityPolicyOIDC](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyoidc) - [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) -- [BackendSecurityPolicySPIFFE](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffe) +- [BackendSecurityPolicySPIFFEJWTSVID](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffejwtsvid) - [BackendSecurityPolicySpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspec) - [BackendSecurityPolicyStatus](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicystatus) -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) - [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - [BackendSecurityPolicyType](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytype) - [CredentialOverrideFromDynamicMetadata](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromdynamicmetadata) @@ -3881,7 +3880,7 @@ default service account credentials when running on GCP. **Appears in:** - [AWSOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-awsoidcexchangetoken) - [AzureOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-azureoidcexchangetoken) -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) +- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - [GCPOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-gcpoidcexchangetoken) BackendSecurityPolicyOIDC specifies OIDC related fields. @@ -3939,14 +3938,14 @@ BackendSecurityPolicyOpenAICredentials contains the supported authentication mec /> -#### BackendSecurityPolicySPIFFE +#### BackendSecurityPolicySPIFFEJWTSVID **Appears in:** -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) +- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) -BackendSecurityPolicySPIFFE specifies how to obtain a JWT-SVID from the SPIFFE Workload API. +BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the SPIFFE Workload API. ##### Fields @@ -3956,7 +3955,7 @@ BackendSecurityPolicySPIFFE specifies how to obtain a JWT-SVID from the SPIFFE W name="audience" type="string" required="true" - description="Audience is the audience of the requested JWT-SVID." + description="Audience is the audience (`aud` claim) of the requested JWT-SVID. The token exchange endpoint
validates it, so it must match the audience that endpoint expects." /> -#### BackendSecurityPolicySubjectToken - - - -**Appears in:** -- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - -BackendSecurityPolicySubjectToken specifies the source of a subject token. -Exactly one source must be configured. - -##### Fields - - - - - - #### BackendSecurityPolicyTokenExchange @@ -4090,8 +4062,12 @@ Exactly one source must be configured. - [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -The controller obtains a subject token, exchanges it at TokenURL for an access token, -and stores the access token in a secret, rotating it before it expires. +The controller obtains a subject token from exactly one source (oidcExchangeToken or spiffeJWTSVID), +exchanges it at TokenURL for an access token, and stores the access token in a secret, +rotating it before it expires. + +TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply +to either subject token source. https://datatracker.ietf.org/doc/html/rfc8693 @@ -4109,22 +4085,27 @@ https://datatracker.ietf.org/doc/html/rfc8693 type="string" required="false" defaultValue="urn:ietf:params:oauth:token-type:jwt" - description="SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token." + description="SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token.
Use `urn:ietf:params:oauth:token-type:access_token` when the OIDC provider returns an opaque access token." /> diff --git a/tests/crdcel/main_test.go b/tests/crdcel/main_test.go index ca20ead81b..9fbe93371b 100644 --- a/tests/crdcel/main_test.go +++ b/tests/crdcel/main_test.go @@ -243,11 +243,11 @@ func TestBackendSecurityPolicies(t *testing.T) { {name: "openai_oidc.yaml"}, { name: "openai_missing_subject_token.yaml", - expErr: "Exactly one of oidcExchangeToken or spiffe must be specified", + expErr: "Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified", }, { name: "openai_multiple_subject_tokens.yaml", - expErr: "Exactly one of oidcExchangeToken or spiffe must be specified", + expErr: "Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified", }, { name: "openai_invalid_token_url.yaml", @@ -255,7 +255,7 @@ func TestBackendSecurityPolicies(t *testing.T) { }, { name: "openai_invalid_socket_path.yaml", - expErr: "spec.openAICredentials.tokenExchange.subjectToken.spiffe.socketPath in body should match", + expErr: "spec.openAICredentials.tokenExchange.spiffeJWTSVID.socketPath in body should match", }, { name: "openai_with_apikey.yaml", diff --git a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml index f860f61ebe..d3a056605a 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml @@ -16,6 +16,5 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - subjectToken: - spiffe: - audience: https://auth.example.com + spiffeJWTSVID: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml index 51d6fec8ac..b258126b26 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml @@ -13,7 +13,6 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - subjectToken: - spiffe: - audience: https://auth.example.com - socketPath: /run/spire/sockets/agent.sock + spiffeJWTSVID: + audience: https://auth.example.com + socketPath: /run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml index d39758ae52..bae1a2cbe1 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml @@ -13,6 +13,5 @@ spec: openAICredentials: tokenExchange: tokenURL: not a url - subjectToken: - spiffe: - audience: https://auth.example.com + spiffeJWTSVID: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml index b99786f1de..beb716be9e 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml @@ -13,4 +13,3 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - subjectToken: {} diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml index 41a3b4baf0..99f59a714a 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml @@ -13,13 +13,12 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - subjectToken: - spiffe: - audience: https://auth.example.com - oidcExchangeToken: - oidc: - provider: - issuer: https://placeholder - clientID: placeholder - clientSecret: - name: placeholder + spiffeJWTSVID: + audience: https://auth.example.com + oidcExchangeToken: + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml index bf39686f00..f0ef3b8da0 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml @@ -14,11 +14,10 @@ spec: tokenExchange: tokenURL: https://auth.example.com/oauth/token subjectTokenType: urn:ietf:params:oauth:token-type:access_token - subjectToken: - oidcExchangeToken: - oidc: - provider: - issuer: https://placeholder - clientID: placeholder - clientSecret: - name: placeholder + oidcExchangeToken: + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml index 7d362e6ccf..40c2201cd3 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml @@ -18,7 +18,6 @@ spec: audience: https://api.openai.com scopes: - api.model.read - subjectToken: - spiffe: - audience: https://auth.example.com - socketPath: unix:///run/spire/sockets/agent.sock + spiffeJWTSVID: + audience: https://auth.example.com + socketPath: unix:///run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml index bbfe9544f9..20685b6399 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml @@ -16,6 +16,5 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - subjectToken: - spiffe: - audience: https://auth.example.com + spiffeJWTSVID: + audience: https://auth.example.com From 2419a96afb7dde10434d147f030d7a2d82d4899c Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 14:58:12 -0400 Subject: [PATCH 05/17] api: restore subjectToken wrapper for OpenAI Group the subject token sources under tokenExchange.subjectToken again, keeping the oidcExchangeToken and spiffeJWTSVID names. The wrapper keeps the sources separate from the exchange parameters and gives new sources an obvious home. Write the one-of rule as a list count so a new source only adds an entry to it. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 20 +- api/v1beta1/zz_generated.deepcopy.go | 28 +- ...envoyproxy.io_backendsecuritypolicies.yaml | 3723 +++++++++-------- site/docs/api/api.mdx | 52 +- tests/crdcel/main_test.go | 2 +- .../apikey_with_openai_credentials.yaml | 5 +- .../openai_invalid_socket_path.yaml | 7 +- .../openai_invalid_token_url.yaml | 5 +- .../openai_missing_subject_token.yaml | 1 + .../openai_multiple_subject_tokens.yaml | 19 +- .../backendsecuritypolicies/openai_oidc.yaml | 15 +- .../openai_spiffe.yaml | 7 +- .../openai_with_apikey.yaml | 5 +- 13 files changed, 1992 insertions(+), 1897 deletions(-) diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 03f8a60b6b..b22f427c2b 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -376,16 +376,13 @@ type BackendSecurityPolicyOpenAICredentials struct { } // BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -// The controller obtains a subject token from exactly one source (oidcExchangeToken or spiffeJWTSVID), -// exchanges it at TokenURL for an access token, and stores the access token in a secret, -// rotating it before it expires. +// The controller obtains a subject token, exchanges it at TokenURL for an access token, +// and stores the access token in a secret, rotating it before it expires. // // TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply -// to either subject token source. +// to any subject token source. // // https://datatracker.ietf.org/doc/html/rfc8693 -// -// +kubebuilder:validation:XValidation:rule="has(self.oidcExchangeToken) != has(self.spiffeJWTSVID)",message="Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified" type BackendSecurityPolicyTokenExchange struct { // TokenURL is the token exchange endpoint. // @@ -415,6 +412,17 @@ type BackendSecurityPolicyTokenExchange struct { // +kubebuilder:validation:MaxItems=16 Scopes []string `json:"scopes,omitempty"` + // SubjectToken specifies where the subject token is obtained from. + // + // +kubebuilder:validation:Required + SubjectToken BackendSecurityPolicySubjectToken `json:"subjectToken"` +} + +// BackendSecurityPolicySubjectToken specifies the source of a subject token. +// Exactly one source must be configured. +// +// +kubebuilder:validation:XValidation:rule="[has(self.oidcExchangeToken), has(self.spiffeJWTSVID)].filter(x, x).size() == 1",message="Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified" +type BackendSecurityPolicySubjectToken struct { // OIDCExchangeToken obtains the subject token from an OIDC provider via the client-credentials flow. // // +optional diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 1a5fef3573..819a632df0 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -780,13 +780,8 @@ func (in *BackendSecurityPolicyStatus) DeepCopy() *BackendSecurityPolicyStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { +func (in *BackendSecurityPolicySubjectToken) DeepCopyInto(out *BackendSecurityPolicySubjectToken) { *out = *in - if in.Scopes != nil { - in, out := &in.Scopes, &out.Scopes - *out = make([]string, len(*in)) - copy(*out, *in) - } if in.OIDCExchangeToken != nil { in, out := &in.OIDCExchangeToken, &out.OIDCExchangeToken *out = new(BackendSecurityPolicyOIDC) @@ -799,6 +794,27 @@ func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityP } } +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySubjectToken. +func (in *BackendSecurityPolicySubjectToken) DeepCopy() *BackendSecurityPolicySubjectToken { + if in == nil { + return nil + } + out := new(BackendSecurityPolicySubjectToken) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { + *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.SubjectToken.DeepCopyInto(&out.SubjectToken) +} + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyTokenExchange. func (in *BackendSecurityPolicyTokenExchange) DeepCopy() *BackendSecurityPolicyTokenExchange { if in == nil { diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 7a09b59c1d..8af10593aa 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -12848,1998 +12848,2042 @@ spec: Audience is the RFC 8693 audience parameter: the logical name of the service the exchanged access token is intended for. It is unrelated to the audience of the subject token. type: string - oidcExchangeToken: - description: OIDCExchangeToken obtains the subject token from - an OIDC provider via the client-credentials flow. + scopes: + description: |- + Scopes is the list of scopes requested for the exchanged access token. They are unrelated to + the scopes requested from the OIDC provider for the subject token. + items: + type: string + maxItems: 16 + type: array + subjectToken: + description: SubjectToken specifies where the subject token + is obtained from. properties: - aud: - description: Aud defines the audience that this ID Token - is intended for. - type: string - grantType: - description: GrantType is the method application gets - access token. - type: string - oidc: - description: OIDC is used to obtain oidc tokens via an - SSO server which will be used to exchange for provider - credentials. + oidcExchangeToken: + description: OIDCExchangeToken obtains the subject token + from an OIDC provider via the client-credentials flow. properties: - clientID: - description: |- - The client ID to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - - Only one of clientID or clientIDRef must be set. - minLength: 1 + aud: + description: Aud defines the audience that this ID + Token is intended for. type: string - clientIDRef: - description: |- - The Kubernetes secret which contains the client ID to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - Exactly one of clientID or clientIDRef must be set. - This is an Opaque secret. The client ID should be stored in the key "client-id". - - Only one of clientID or clientIDRef must be set. + grantType: + description: GrantType is the method application gets + access token. + type: string + oidc: + description: OIDC is used to obtain oidc tokens via + an SSO server which will be used to exchange for + provider credentials. properties: - group: - default: "" + clientID: description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. For - example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 + The client ID to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + Only one of clientID or clientIDRef must be set. minLength: 1 type: string - namespace: + clientIDRef: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. + The Kubernetes secret which contains the client ID to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Exactly one of clientID or clientIDRef must be set. + This is an Opaque secret. The client ID should be stored in the key "client-id". - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + Only one of clientID or clientIDRef must be set. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. + For example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name - type: object - clientSecret: - description: |- - The Kubernetes secret which contains the OIDC client secret to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. - This is an Opaque secret. The client secret should be stored in the key - "client-secret". - properties: - group: - default: "" + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + clientSecret: description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Secret - description: Kind is kind of the referent. For - example "Secret". - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 + The Kubernetes secret which contains the OIDC client secret to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + + This is an Opaque secret. The client secret should be stored in the key + "client-secret". + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. + For example "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + cookieConfig: + description: |- + CookieConfigs allows setting the SameSite attribute for OIDC cookies. + By default, its unset. + properties: + sameSite: + enum: + - Lax + - Strict + - None + type: string + type: object + cookieDomain: + description: |- + The optional domain to set the access and ID token cookies on. + If not set, the cookies will default to the host of the request, not including the subdomains. + If set, the cookies will be set on the specified domain and all subdomains. + This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string - namespace: + cookieNames: description: |- - Namespace is the namespace of the referenced object. When unspecified, the local - namespace is inferred. + The optional cookie name overrides to be used for Bearer and IdToken cookies in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses a randomly generated suffix + properties: + accessToken: + description: |- + The name of the cookie used to store the AccessToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "AccessToken-(randomly generated uid)" + type: string + idToken: + description: |- + The name of the cookie used to store the IdToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "IdToken-(randomly generated uid)" + type: string + type: object + csrfTokenTTL: + description: |- + CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token + in the "state" parameter when the provider redirects back to the callback endpoint. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - required: - - name - type: object - cookieConfig: - description: |- - CookieConfigs allows setting the SameSite attribute for OIDC cookies. - By default, its unset. - properties: - sameSite: - enum: - - Lax - - Strict - - None + If omitted, Envoy Gateway defaults the token expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - type: object - cookieDomain: - description: |- - The optional domain to set the access and ID token cookies on. - If not set, the cookies will default to the host of the request, not including the subdomains. - If set, the cookies will be set on the specified domain and all subdomains. - This means that requests to any subdomain will not require reauthentication after users log in to the parent domain. - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - cookieNames: - description: |- - The optional cookie name overrides to be used for Bearer and IdToken cookies in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses a randomly generated suffix - properties: - accessToken: + defaultRefreshTokenTTL: description: |- - The name of the cookie used to store the AccessToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "AccessToken-(randomly generated uid)" + DefaultRefreshTokenTTL is the default lifetime of the refresh token. + This field is only used when the exp (expiration time) claim is omitted in + the refresh token or the refresh token is not JWT. + + If not specified, defaults to 604800s (one week). + Note: this field is only applicable when the "refreshToken" field is set to true. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - idToken: + defaultTokenTTL: description: |- - The name of the cookie used to store the IdToken in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, defaults to "IdToken-(randomly generated uid)" + DefaultTokenTTL is the default lifetime of the id token and access token. + Please note that Envoy will always use the expiry time from the response + of the authorization server if it is provided. This field is only used when + the expiry time is not provided by the authorization. + + If not specified, defaults to 0. In this case, the "expires_in" field in + the authorization response must be set by the authorization server, or the + OAuth flow will fail. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - type: object - csrfTokenTTL: - description: |- - CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid. + denyRedirect: + description: |- + Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. + This behavior can be useful for AJAX or machine requests. + properties: + headers: + description: Defines the headers to match + against the request to deny redirect to + the OIDC Provider. + items: + description: OIDCDenyRedirectHeader defines + how a header is matched + properties: + name: + description: Specifies the name of the + header in the request. + minLength: 1 + type: string + type: + default: Exact + description: Type specifies how to match + against a string. + enum: + - Exact + - Prefix + - Suffix + - RegularExpression + type: string + value: + description: Value specifies the string + value that the match must have. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - headers + type: object + disableTokenEncryption: + description: |- + Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. + This option should only be used in secure environments where token encryption is not required. + Default is false (tokens are encrypted). + type: boolean + forwardAccessToken: + description: |- + ForwardAccessToken indicates whether the Envoy should forward the access token + via the Authorization header Bearer scheme to the upstream. + If not specified, defaults to false. + type: boolean + forwardIDToken: + description: |- + ForwardIDToken configures forwarding of the OIDC ID token to the upstream. - This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token - in the "state" parameter when the provider redirects back to the callback endpoint. + If the configured header is "Authorization", EG forwards the ID token using + the "Bearer " prefix. For any other header, EG forwards the raw token value. + If not specified, the ID token will not be forwarded. - If omitted, Envoy Gateway defaults the token expiration to 10 minutes. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - defaultRefreshTokenTTL: - description: |- - DefaultRefreshTokenTTL is the default lifetime of the refresh token. - This field is only used when the exp (expiration time) claim is omitted in - the refresh token or the refresh token is not JWT. - - If not specified, defaults to 604800s (one week). - Note: this field is only applicable when the "refreshToken" field is set to true. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - defaultTokenTTL: - description: |- - DefaultTokenTTL is the default lifetime of the id token and access token. - Please note that Envoy will always use the expiry time from the response - of the authorization server if it is provided. This field is only used when - the expiry time is not provided by the authorization. - - If not specified, defaults to 0. In this case, the "expires_in" field in - the authorization response must be set by the authorization server, or the - OAuth flow will fail. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - denyRedirect: - description: |- - Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider. - This behavior can be useful for AJAX or machine requests. - properties: - headers: - description: Defines the headers to match against - the request to deny redirect to the OIDC Provider. - items: - description: OIDCDenyRedirectHeader defines - how a header is matched - properties: - name: - description: Specifies the name of the header - in the request. - minLength: 1 - type: string - type: - default: Exact - description: Type specifies how to match - against a string. - enum: - - Exact - - Prefix - - Suffix - - RegularExpression - type: string - value: - description: Value specifies the string - value that the match must have. - maxLength: 1024 - minLength: 1 - type: string - required: - - name - - value - type: object - maxItems: 16 - minItems: 1 - type: array - required: - - headers - type: object - disableTokenEncryption: - description: |- - Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. - This option should only be used in secure environments where token encryption is not required. - Default is false (tokens are encrypted). - type: boolean - forwardAccessToken: - description: |- - ForwardAccessToken indicates whether the Envoy should forward the access token - via the Authorization header Bearer scheme to the upstream. - If not specified, defaults to false. - type: boolean - forwardIDToken: - description: |- - ForwardIDToken configures forwarding of the OIDC ID token to the upstream. - - If the configured header is "Authorization", EG forwards the ID token using - the "Bearer " prefix. For any other header, EG forwards the raw token value. - If not specified, the ID token will not be forwarded. - - Note: when passThroughAuthHeader is enabled, this header must not be the same - as a header a JWT provider extracts from (the "Authorization" header by - default). The forwarded ID token header is owned by Envoy, and Envoy rejects - an OAuth2 configuration whose pass-through matcher keys on it. - properties: - header: + Note: when passThroughAuthHeader is enabled, this header must not be the same + as a header a JWT provider extracts from (the "Authorization" header by + default). The forwarded ID token header is owned by Envoy, and Envoy rejects + an OAuth2 configuration whose pass-through matcher keys on it. + properties: + header: + description: |- + Header is the upstream request header that will carry the ID token. + It must be a valid HTTP header name. Pseudo-headers (names starting with ":") + and the "Host" header are not allowed. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + x-kubernetes-validations: + - message: header cannot be the Host header + rule: self.lowerAscii() != 'host' + required: + - header + type: object + logoutPath: description: |- - Header is the upstream request header that will carry the ID token. - It must be a valid HTTP header name. Pseudo-headers (names starting with ":") - and the "Host" header are not allowed. - maxLength: 256 - minLength: 1 - pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ - type: string - x-kubernetes-validations: - - message: header cannot be the Host header - rule: self.lowerAscii() != 'host' - required: - - header - type: object - logoutPath: - description: |- - The path to log a user out, clearing their credential cookies. + The path to log a user out, clearing their credential cookies. - If not specified, uses a default logout path "/logout" - type: string - passThroughAuthHeader: - description: |- - Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless - explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. - - The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC - redirects and wish to directly supply a token instead. - - If not specified, defaults to false. - type: boolean - provider: - description: The OIDC Provider configuration. - properties: - authorizationEndpoint: - description: |- - The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + If not specified, uses a default logout path "/logout" type: string - backendRef: + passThroughAuthHeader: description: |- - BackendRef references a Kubernetes object that represents the - backend server to which the authorization request will be sent. + Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless + explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header. - Deprecated: Use BackendRefs instead. + The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC + redirects and wish to directly supply a token instead. + + If not specified, defaults to false. + type: boolean + provider: + description: The OIDC Provider configuration. properties: - group: - default: "" + authorizationEndpoint: description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). type: string - kind: - default: Service + backendRef: description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". + BackendRef references a Kubernetes object that represents the + backend server to which the authorization request will be sent. - Defaults to "Service" when not specified. + Deprecated: Use BackendRefs instead. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. + Defaults to "Service" when not specified. - Support: Core (Services with a type other than ExternalName) + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. + Support: Core (Services with a type other than ExternalName) - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + backendRefs: description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind == - ''Service'') ? has(self.port) : true' - backendRefs: - description: |- - BackendRefs references a Kubernetes object that represents the - backend server to which the authorization request will be sent. - items: - description: BackendRef defines how an ObjectReference - that is specific to BackendRef. - properties: - fallback: - description: |- - Fallback indicates whether the backend is designated as a fallback. - Multiple fallback backends can be configured. - It is highly recommended to configure active or passive health checks to ensure that failover can be detected - when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. - The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when - the health of the active backends falls below 72%. - type: boolean - group: - default: "" - description: |- - Group is the group of the referent. For example, "gateway.networking.k8s.io". - When unspecified or empty string, core API group is inferred. - maxLength: 253 - pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - kind: - default: Service - description: |- - Kind is the Kubernetes resource kind of the referent. For example - "Service". + BackendRefs references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + items: + description: BackendRef defines how an ObjectReference + that is specific to BackendRef. + properties: + fallback: + description: |- + Fallback indicates whether the backend is designated as a fallback. + Multiple fallback backends can be configured. + It is highly recommended to configure active or passive health checks to ensure that failover can be detected + when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + the health of the active backends falls below 72%. + type: boolean + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". - Defaults to "Service" when not specified. + Defaults to "Service" when not specified. - ExternalName services can refer to CNAME DNS records that may live - outside of the cluster and as such are difficult to reason about in - terms of conformance. They also may not be safe to forward to (see - CVE-2021-25740 for more information). Implementations SHOULD NOT - support ExternalName Services. + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. - Support: Core (Services with a type other than ExternalName) - - Support: Implementation-specific (Services with type ExternalName) - maxLength: 63 - minLength: 1 - pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ - type: string - name: - description: Name is the name of the referent. - maxLength: 253 - minLength: 1 - type: string - namespace: - description: |- - Namespace is the namespace of the backend. When unspecified, the local - namespace is inferred. - - Note that when a namespace different than the local namespace is specified, - a ReferenceGrant object is required in the referent namespace to allow that - namespace's owner to accept the reference. See the ReferenceGrant - documentation for details. - - Support: Core - maxLength: 63 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - port: - description: |- - Port specifies the destination port number to use for this resource. - Port is required when the referent is a Kubernetes Service. In this - case, the port number is the service port number, not the target port. - For other resources, destination port might be derived from the referent - resource or this field. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - weight: - default: 1 - description: |- - Weight specifies the proportion of requests forwarded to the referenced - backend. This is computed as weight/(sum of all weights in this - BackendRefs list). For non-zero values, there may be some epsilon from - the exact proportion defined here depending on the precision an - implementation supports. Weight is not a percentage and the sum of - weights does not need to equal 100. - - If only one backend is specified and it has a weight greater than 0, 100% - of the traffic is forwarded to that backend. If weight is set to 0, no - traffic should be forwarded for this entry. If unspecified, weight - defaults to 1. + Support: Core (Services with a type other than ExternalName) - Support for this field varies based on the context where used. - format: int32 - maximum: 1000000 - minimum: 0 - type: integer - required: - - name - type: object - x-kubernetes-validations: - - message: Must have port for Service reference - rule: '(size(self.group) == 0 && self.kind - == ''Service'') ? has(self.port) : true' - maxItems: 16 - type: array - backendSettings: - description: |- - BackendSettings holds configuration for managing the connection - to the backend. - properties: - circuitBreaker: + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the + referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + maxItems: 16 + type: array + backendSettings: description: |- - Circuit Breaker settings for the upstream connections and requests. - If not set, circuit breakers will be enabled with the default thresholds + BackendSettings holds configuration for managing the connection + to the backend. properties: - maxConnections: - default: 1024 - description: The maximum number of connections - that Envoy will establish to the referenced - backend defined within a xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxParallelRequests: - default: 1024 - description: The maximum number of parallel - requests that Envoy will make to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxParallelRetries: - default: 1024 - description: The maximum number of parallel - retries that Envoy will make to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxPendingRequests: - default: 1024 - description: The maximum number of pending - requests that Envoy will queue to the - referenced backend defined within a - xRoute rule. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - maxRequestsPerConnection: + circuitBreaker: description: |- - The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. - Default: unlimited. - format: int64 - maximum: 4294967295 - minimum: 0 - type: integer - perEndpoint: - description: PerEndpoint defines Circuit - Breakers that will apply per-endpoint - for an upstream cluster + Circuit Breaker settings for the upstream connections and requests. + If not set, circuit breakers will be enabled with the default thresholds properties: maxConnections: default: 1024 - description: MaxConnections configures - the maximum number of connections - that Envoy will establish per-endpoint + description: The maximum number of + connections that Envoy will establish to the referenced backend defined within a xRoute rule. format: int64 maximum: 4294967295 minimum: 0 type: integer - type: object - retryBudget: - description: |- - RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. - RetryBudget take precedence over maxParallelRetries. - properties: - minRetryConcurrency: - description: |- - MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. - For example, a budget of 20% with a minimum retry concurrency of 3 - will allow 5 active retries while there are 25 active requests. - If there are 2 active requests, there are still 3 active retries - allowed because of the minimum retry concurrency. - Defaults to 3. - format: int32 + maxParallelRequests: + default: 1024 + description: The maximum number of + parallel requests that Envoy will + make to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 type: integer - percent: + maxParallelRetries: + default: 1024 + description: The maximum number of + parallel retries that Envoy will + make to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxPendingRequests: + default: 1024 + description: The maximum number of + pending requests that Envoy will + queue to the referenced backend + defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxRequestsPerConnection: description: |- - Percent specifies the limit on concurrent retries as a percentage [0, 100] of - the sum of active requests and active pending requests. + The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. + Default: unlimited. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + perEndpoint: + description: PerEndpoint defines Circuit + Breakers that will apply per-endpoint + for an upstream cluster properties: - denominator: - default: 100 - format: int32 - minimum: 1 + maxConnections: + default: 1024 + description: MaxConnections configures + the maximum number of connections + that Envoy will establish per-endpoint + to the referenced backend defined + within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 type: integer - numerator: + type: object + retryBudget: + description: |- + RetryBudget provides settings for retry budget, which limits the number of retries in a given percentage. + RetryBudget take precedence over maxParallelRetries. + properties: + minRetryConcurrency: + description: |- + MinRetryConcurrency specifies the minimum retry concurrency allowed for the retry budget. + For example, a budget of 20% with a minimum retry concurrency of 3 + will allow 5 active retries while there are 25 active requests. + If there are 2 active requests, there are still 3 active retries + allowed because of the minimum retry concurrency. + Defaults to 3. format: int32 - minimum: 0 type: integer + percent: + description: |- + Percent specifies the limit on concurrent retries as a percentage [0, 100] of + the sum of active requests and active pending requests. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less + than or equal to denominator + rule: self.numerator <= self.denominator required: - - numerator + - percent type: object - x-kubernetes-validations: - - message: numerator must be less - than or equal to denominator - rule: self.numerator <= self.denominator - required: - - percent type: object - type: object - connection: - description: Connection includes backend connection - settings. - properties: - bufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - BufferLimit Soft limit on size of the cluster’s connections read and write buffers. - BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. - If unspecified, an implementation defined default is applied (32768 bytes). - For example, 20Mi, 1Gi, 256Ki etc. - Note: that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true - preconnect: - description: |- - Preconnect configures proactive upstream connections to reduce latency by establishing - connections before they’re needed and avoiding connection establishment overhead. + connection: + description: Connection includes backend + connection settings. + properties: + bufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + BufferLimit Soft limit on size of the cluster’s connections read and write buffers. + BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. + If unspecified, an implementation defined default is applied (32768 bytes). + For example, 20Mi, 1Gi, 256Ki etc. + Note: that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + preconnect: + description: |- + Preconnect configures proactive upstream connections to reduce latency by establishing + connections before they’re needed and avoiding connection establishment overhead. + + If unset, Envoy will fetch connections as needed to serve in-flight requests. + properties: + perEndpointPercent: + description: |- + PerEndpointPercent configures how many additional connections to maintain per + upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a + percentage of the connections required by active streams + (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). - If unset, Envoy will fetch connections as needed to serve in-flight requests. + Allowed value range is between 100-300. When both PerEndpointPercent and + PredictivePercent are set, Envoy ensures both are satisfied (max of the two). + format: int32 + maximum: 300 + minimum: 100 + type: integer + predictivePercent: + description: |- + PredictivePercent configures how many additional connections to maintain + across the cluster by anticipating which upstream endpoint the load balancer + will select next, useful for low-QPS services. Relies on deterministic + loadbalancing and is only supported with Random or RoundRobin. + Expressed as a percentage of the connections required by active streams + (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). + + Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are + set Envoy ensures both are satisfied per host (max of the two). + format: int32 + minimum: 100 + type: integer + type: object + socketBufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket + to backend. + SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. + For example, 20Mi, 1Gi, 256Ki etc. + Note that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + type: object + dns: + description: DNS includes dns resolution + settings. properties: - perEndpointPercent: + dnsRefreshRate: description: |- - PerEndpointPercent configures how many additional connections to maintain per - upstream endpoint, useful for high-QPS or latency sensitive services. Expressed as a - percentage of the connections required by active streams - (e.g. 100 = preconnect disabled, 105 = 1.05x connections per-endpoint, 200 = 2.00×). - - Allowed value range is between 100-300. When both PerEndpointPercent and - PredictivePercent are set, Envoy ensures both are satisfied (max of the two). - format: int32 - maximum: 300 - minimum: 100 - type: integer - predictivePercent: + DNSRefreshRate specifies the rate at which DNS records should be refreshed. + Defaults to 30 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + lookupFamily: description: |- - PredictivePercent configures how many additional connections to maintain - across the cluster by anticipating which upstream endpoint the load balancer - will select next, useful for low-QPS services. Relies on deterministic - loadbalancing and is only supported with Random or RoundRobin. - Expressed as a percentage of the connections required by active streams - (e.g. 100 = 1.0 (no preconnect), 105 = 1.05× connections across the cluster, 200 = 2.00×). - - Minimum allowed value is 100. When both PerEndpointPercent and PredictivePercent are - set Envoy ensures both are satisfied per host (max of the two). - format: int32 - minimum: 100 - type: integer + LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). + If set, this configuration overrides other defaults. + enum: + - IPv4 + - IPv6 + - IPv4Preferred + - IPv6Preferred + - IPv4AndIPv6 + type: string + respectDnsTtl: + description: |- + RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. + If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. + Defaults to true. + type: boolean type: object - socketBufferLimit: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket - to backend. - SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. - For example, 20Mi, 1Gi, 256Ki etc. - Note that when the suffix is not provided, the value is interpreted as bytes. - x-kubernetes-int-or-string: true - type: object - dns: - description: DNS includes dns resolution settings. - properties: - dnsRefreshRate: - description: |- - DNSRefreshRate specifies the rate at which DNS records should be refreshed. - Defaults to 30 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - lookupFamily: - description: |- - LookupFamily determines how Envoy would resolve DNS for Routes where the backend is specified as a fully qualified domain name (FQDN). - If set, this configuration overrides other defaults. - enum: - - IPv4 - - IPv6 - - IPv4Preferred - - IPv6Preferred - - IPv4AndIPv6 - type: string - respectDnsTtl: - description: |- - RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. - If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. - Defaults to true. - type: boolean - type: object - healthCheck: - description: HealthCheck allows gateway to - perform active health checking on backends. - properties: - active: - description: Active health check configuration + healthCheck: + description: HealthCheck allows gateway + to perform active health checking on + backends. properties: - grpc: - description: |- - GRPC defines the configuration of the GRPC health checker. - It's optional, and can only be used if the specified type is GRPC. - properties: - service: - description: |- - Service to send in the health check request. - If this is not specified, then the health check request applies to the entire - server and not to a specific service. - type: string - type: object - healthCheckLog: - description: |- - HealthCheckLog defines health check event logging configuration for this cluster. - When set, HC probe outcomes are logged to the configured sinks. - Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. + active: + description: Active health check configuration properties: - matches: + grpc: description: |- - Matches defines which health check probe outcomes produce a log entry. - When omitted or empty, all events are logged. - - Each value must be unique. Multiple values are ORed. If any failure type is - specified then a success type must also be specified, and vice versa. - items: - description: ProxyHealthCheckLogEventType - specifies which health check - probe outcomes produce a log - entry. - enum: - - Failure - - FailureSeriesStart - - Success - - HealthyTransition - type: string - maxItems: 4 - type: array - x-kubernetes-list-type: set - x-kubernetes-validations: - - message: a failure type and - a success type must both be - specified together - rule: self.exists(e, e == 'Failure' - || e == 'FailureSeriesStart') - == self.exists(e, e == 'Success' - || e == 'HealthyTransition') - sinks: + GRPC defines the configuration of the GRPC health checker. + It's optional, and can only be used if the specified type is GRPC. + properties: + service: + description: |- + Service to send in the health check request. + If this is not specified, then the health check request applies to the entire + server and not to a specific service. + type: string + type: object + healthCheckLog: description: |- - Sinks defines where health check events are written. - When omitted, events are written to /dev/stdout. - items: - description: ProxyHealthCheckLogSink - defines a destination for - health check event logs. - properties: - file: - description: |- - File defines the file sink configuration. - Required when type is File. + HealthCheckLog defines health check event logging configuration for this cluster. + When set, HC probe outcomes are logged to the configured sinks. + Takes precedence over the gateway-level EnvoyProxy.spec.telemetry.healthCheckLog. + properties: + matches: + description: |- + Matches defines which health check probe outcomes produce a log entry. + When omitted or empty, all events are logged. + + Each value must be unique. Multiple values are ORed. If any failure type is + specified then a success type must also be specified, and vice versa. + items: + description: ProxyHealthCheckLogEventType + specifies which health + check probe outcomes produce + a log entry. + enum: + - Failure + - FailureSeriesStart + - Success + - HealthyTransition + type: string + maxItems: 4 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: a failure type + and a success type must + both be specified together + rule: self.exists(e, e == + 'Failure' || e == 'FailureSeriesStart') + == self.exists(e, e == + 'Success' || e == 'HealthyTransition') + sinks: + description: |- + Sinks defines where health check events are written. + When omitted, events are written to /dev/stdout. + items: + description: ProxyHealthCheckLogSink + defines a destination + for health check event + logs. properties: - path: + file: description: |- - Path specifies the file path for health check event output. - Use /dev/stdout to write to standard output. - minLength: 1 + File defines the file sink configuration. + Required when type is File. + properties: + path: + description: |- + Path specifies the file path for health check event output. + Use /dev/stdout to write to standard output. + minLength: 1 + type: string + required: + - path + type: object + type: + description: Type defines + the type of sink. + enum: + - File type: string required: - - path + - type type: object - type: - description: Type defines - the type of sink. - enum: - - File - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If ProxyHealthCheckLogSink - type is File, file field - needs to be set. - rule: 'self.type == ''File'' - ? has(self.file) : !has(self.file)' - maxItems: 1 - type: array - type: object - healthyThreshold: - default: 1 - description: HealthyThreshold defines - the number of healthy health checks - required before a backend host is - marked healthy. - format: int32 - minimum: 1 - type: integer - http: - description: |- - HTTP defines the configuration of http health checker. - It's required while the health checker type is HTTP. - properties: - expectedResponse: - description: ExpectedResponse - defines a list of HTTP expected - responses to match. + x-kubernetes-validations: + - message: If ProxyHealthCheckLogSink + type is File, file field + needs to be set. + rule: 'self.type == ''File'' + ? has(self.file) : !has(self.file)' + maxItems: 1 + type: array + type: object + healthyThreshold: + default: 1 + description: HealthyThreshold + defines the number of healthy + health checks required before + a backend host is marked healthy. + format: int32 + minimum: 1 + type: integer + http: + description: |- + HTTP defines the configuration of http health checker. + It's required while the health checker type is HTTP. properties: - binary: - description: Binary payload - base64 encoded. - format: byte + expectedResponse: + description: ExpectedResponse + defines a list of HTTP expected + responses to match. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + expectedStatuses: + description: |- + ExpectedStatuses defines a list of HTTP response statuses considered healthy. + Defaults to 200 only + items: + description: HTTPStatus + defines the http status + code. + maximum: 599 + minimum: 100 + type: integer + type: array + hostname: + description: |- + Hostname defines the HTTP Host header used for active HTTP health checks. + Host selection uses this order: this field, the associated Backend endpoint + hostname if available, then the effective Route hostname. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string - text: - description: Text payload - in plain text. + method: + description: |- + Method defines the HTTP method used for health checking. + Defaults to GET + maxLength: 16 type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. + path: + description: Path defines + the HTTP path that will + be requested during health + checking. + maxLength: 1024 + minLength: 1 type: string + requestBody: + description: RequestBody defines + the HTTP request body payload + sent during health checking. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + retriableStatuses: + description: |- + RetriableStatuses defines a list of HTTP response statuses considered retriable. + Responses matching these statuses count towards the unhealthy threshold but + do not result in the host being considered immediately unhealthy. + The expected statuses take precedence for any range overlaps with this field. + items: + description: HTTPStatus + defines the http status + code. + maximum: 599 + minimum: 100 + type: integer + type: array required: - - type + - path type: object x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - expectedStatuses: - description: |- - ExpectedStatuses defines a list of HTTP response statuses considered healthy. - Defaults to 200 only - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array - hostname: + - message: The requestBody field + can only be set when method + is POST or PUT. + rule: '!has(self.requestBody) + || (has(self.method) && self.method.upperAscii() + in [''POST'',''PUT''])' + initialJitter: description: |- - Hostname defines the HTTP Host header used for active HTTP health checks. - Host selection uses this order: this field, the associated Backend endpoint - hostname if available, then the effective Route hostname. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + InitialJitter defines the maximum time Envoy will wait before the first health check. + Envoy will randomly select a value between 0 and the initial jitter value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - method: + interval: + default: 3s + description: Interval defines + the time between active health + checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + overrides: description: |- - Method defines the HTTP method used for health checking. - Defaults to GET - maxLength: 16 + Overrides defines the configuration of the overriding health check settings for all endpoints + in the backend cluster. This allows customization of port and other settings that may differ + from the main service configuration. + properties: + port: + description: |- + Port overrides the health check port. + If not set, the endpoint's serving port is used for health checks. + This is useful when health checks are served on a different port than + the main service port (e.g., port 443 for service, port 9090 for health checks). + format: int32 + maximum: 65535 + minimum: 1 + type: integer + type: object + tcp: + description: |- + TCP defines the configuration of tcp health checker. + It's required while the health checker type is TCP. + properties: + receive: + description: Receive defines + the expected response payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + send: + description: Send defines + the request payload. + properties: + binary: + description: Binary payload + base64 encoded. + format: byte + type: string + text: + description: Text payload + in plain text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines + the type of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type + is Text, text field needs + to be set. + rule: 'self.type == ''Text'' + ? has(self.text) : !has(self.text)' + - message: If payload type + is Binary, binary field + needs to be set. + rule: 'self.type == ''Binary'' + ? has(self.binary) : !has(self.binary)' + type: object + timeout: + default: 1s + description: Timeout defines the + time to wait for a health check + response. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - path: - description: Path defines the - HTTP path that will be requested - during health checking. - maxLength: 1024 - minLength: 1 + type: + allOf: + - enum: + - HTTP + - TCP + - GRPC + - enum: + - HTTP + - TCP + - GRPC + description: Type defines the + type of health checker. type: string - requestBody: - description: RequestBody defines - the HTTP request body payload - sent during health checking. - properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - retriableStatuses: + unhealthyThreshold: + default: 3 description: |- - RetriableStatuses defines a list of HTTP response statuses considered retriable. - Responses matching these statuses count towards the unhealthy threshold but - do not result in the host being considered immediately unhealthy. - The expected statuses take precedence for any range overlaps with this field. - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array + UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. + Without RetriableStatuses configured, any health check failure results in the host being immediately + considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried + up to this threshold before the host is marked unhealthy. + format: int32 + minimum: 1 + type: integer required: - - path + - type type: object x-kubernetes-validations: - - message: The requestBody field can - only be set when method is POST - or PUT. - rule: '!has(self.requestBody) || - (has(self.method) && self.method.upperAscii() - in [''POST'',''PUT''])' - initialJitter: + - message: If Health Checker type + is HTTP, http field needs to be + set. + rule: 'self.type == ''HTTP'' ? has(self.http) + : !has(self.http)' + - message: If Health Checker type + is TCP, tcp field needs to be + set. + rule: 'self.type == ''TCP'' ? has(self.tcp) + : !has(self.tcp)' + - message: The grpc field can only + be set if the Health Checker type + is GRPC. + rule: 'has(self.grpc) ? self.type + == ''GRPC'' : true' + panicThreshold: + description: |- + When number of unhealthy endpoints for a backend reaches this threshold + Envoy will disregard health status and balance across all endpoints. + It's designed to prevent a situation in which host failures cascade throughout the cluster + as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. + format: int32 + maximum: 100 + minimum: 0 + type: integer + passive: + description: Passive passive check + configuration + properties: + alwaysEjectOneEndpoint: + default: false + description: |- + AlwaysEjectOneEndpoint defines whether at least one host should be ejected, + regardless of MaxEjectionPercent. + type: boolean + baseEjectionTime: + default: 30s + description: BaseEjectionTime + defines the base duration for + which a host will be ejected + on consecutive failures. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors + sets the number of consecutive + 5xx errors triggering ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + description: ConsecutiveGatewayErrors + sets the number of consecutive + gateway errors triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 + description: |- + ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors is set to true. + format: int32 + type: integer + failurePercentageThreshold: + description: |- + FailurePercentageThreshold sets the failure percentage threshold for outlier detection. + If the failure percentage of a given host is greater than or equal to this value, it will be ejected. + Defaults to 85. + format: int32 + maximum: 100 + minimum: 0 + type: integer + interval: + default: 3s + description: Interval defines + the time between passive health + checks. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent + sets the maximum percentage + of hosts in a cluster that can + be ejected. + format: int32 + maximum: 100 + minimum: 0 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors + enables splitting of errors + between external and local origin. + type: boolean + type: object + type: object + http2: + description: HTTP2 provides HTTP/2 configuration + for backend connections. + properties: + connectionKeepalive: + description: ConnectionKeepalive configures + HTTP/2 connection keepalive using + PING frames. + properties: + idleInterval: + description: IdleInterval specifies + how long a connection must be + idle before a PING is sent. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: Interval specifies + how often to send HTTP/2 PING + frames to keep the connection + alive. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + intervalJitter: + description: |- + IntervalJitter specifies a random jitter percentage added to each interval. + Defaults to 15% if not specified. + format: int32 + maximum: 100 + minimum: 0 + type: integer + timeout: + description: Timeout specifies + how long to wait for a PING + response before considering + the connection dead. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: timeout must be less than + interval + rule: '!has(self.timeout) || !has(self.interval) + || duration(self.timeout) < duration(self.interval)' + initialConnectionWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. + If not set, the default value is 1 MiB. + x-kubernetes-int-or-string: true + initialStreamWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialStreamWindowSize sets the initial window size for HTTP/2 streams. + If not set, the default value is 64 KiB(64*1024). + x-kubernetes-int-or-string: true + maxConcurrentStreams: + description: |- + MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. + If not set, the default value is 100. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + onInvalidMessage: description: |- - InitialJitter defines the maximum time Envoy will wait before the first health check. - Envoy will randomly select a value between 0 and the initial jitter value. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - default: 3s - description: Interval defines the - time between active health checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error + It's recommended for L2 Envoy deployments to set this value to TerminateStream. + https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two + Default: TerminateConnection type: string - overrides: + type: object + loadBalancer: + description: |- + LoadBalancer policy to apply when routing traffic from the gateway to + the backend endpoints. Defaults to `LeastRequest`. + properties: + backendUtilization: description: |- - Overrides defines the configuration of the overriding health check settings for all endpoints - in the backend cluster. This allows customization of port and other settings that may differ - from the main service configuration. + BackendUtilization defines the configuration when the load balancer type is + set to BackendUtilization. properties: - port: + blackoutPeriod: description: |- - Port overrides the health check port. - If not set, the endpoint's serving port is used for health checks. - This is useful when health checks are served on a different port than - the main service port (e.g., port 443 for service, port 9090 for health checks). + A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. + Default is 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + errorUtilizationPenaltyPercent: + description: |- + ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). + This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. + + For example: + - 100 => 1.0x + - 120 => 1.2x + - 200 => 2.0x + + Must be non-negative. format: int32 - maximum: 65535 - minimum: 1 + minimum: 0 type: integer + keepResponseHeaders: + default: false + description: |- + KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. + Defaults to false. + type: boolean + metricNamesForComputingUtilization: + description: |- + Metric names used to compute utilization if application_utilization is not set. + For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". + items: + type: string + type: array + outOfBand: + description: |- + OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a + server-streaming gRPC connection to each endpoint's + xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load + reports periodically, instead of relying on in-band ORCA metrics + carried in response headers/trailers. + + The backend must implement OpenRcaService for this to take effect. + properties: + authority: + description: |- + Authority overrides the :authority header on the OutOfBand gRPC stream. + If unset, Envoy uses the endpoint hostname, then the dialed address, then + the cluster name. + maxLength: 259 + minLength: 1 + pattern: ^[^\x00\n\r]*$ + type: string + port: + description: |- + Port overrides the port used for the OutOfBand reporting connection, e.g. to + reach a separate reporting sidecar. Defaults to the endpoint's port. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + reportingPeriod: + description: |- + ReportingPeriod is how often Envoy requests load reports from the server. + Must be greater than 0. Defaults to 10s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + x-kubernetes-validations: + - message: reportingPeriod + must be greater than 0 + rule: duration(self) > duration('0s') + type: object + weightExpirationPeriod: + description: If a given endpoint + has not reported load metrics + in this long, stop using the + reported weight. Defaults to + 3m. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + weightUpdatePeriod: + description: How often endpoint + weights are recalculated. Values + less than 100ms are capped at + 100ms. Default 1s. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string type: object - tcp: + consistentHash: description: |- - TCP defines the configuration of tcp health checker. - It's required while the health checker type is TCP. + ConsistentHash defines the configuration when the load balancer type is + set to ConsistentHash properties: - receive: - description: Receive defines the - expected response payload. + cookie: + description: Cookie configures + the cookie hash policy when + the consistent hash type is + set to Cookie. properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. + attributes: + additionalProperties: + type: string + description: Additional Attributes + to set for the generated + cookie. + type: object + name: + description: |- + Name of the cookie to hash. + If this cookie does not exist in the request, Envoy will generate a cookie and set + the TTL on the response back to the client based on Layer 4 + attributes of the backend endpoint, to ensure that these future requests + go to the same backend endpoint. Make sure to set the TTL field for this case. type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. + ttl: + description: |- + TTL of the generated cookie if the cookie is not present. This value sets the + Max-Age attribute value. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string required: - - type + - name type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' - send: - description: Send defines the - request payload. + header: + description: |- + Header configures the header hash policy when the consistent hash type is set to Header. + + Deprecated: use Headers instead properties: - binary: - description: Binary payload - base64 encoded. - format: byte - type: string - text: - description: Text payload - in plain text. - type: string - type: - allOf: - - enum: - - Text - - Binary - - enum: - - Text - - Binary - description: Type defines - the type of the payload. + name: + description: Name of the header + to hash. type: string required: - - type + - name type: object - x-kubernetes-validations: - - message: If payload type is - Text, text field needs to - be set. - rule: 'self.type == ''Text'' - ? has(self.text) : !has(self.text)' - - message: If payload type is - Binary, binary field needs - to be set. - rule: 'self.type == ''Binary'' - ? has(self.binary) : !has(self.binary)' + headers: + description: Headers configures + the header hash policy for each + header, when the consistent + hash type is set to Headers. + items: + description: |- + Header defines the header hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the + header to hash. + type: string + required: + - name + type: object + type: array + queryParams: + description: QueryParams configures + the query parameter hash policy + when the consistent hash type + is set to QueryParams. + items: + description: |- + QueryParam defines the query parameter name hashing configuration for consistent hash based + load balancing. + properties: + name: + description: Name of the + query param to hash. + type: string + required: + - name + type: object + type: array + tableSize: + default: 65537 + description: The table size for + consistent hashing, must be + prime number limited to 5000011. + format: int64 + maximum: 5000011 + minimum: 2 + type: integer + type: + description: |- + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Headers", + "Cookie". + "QueryParams". + enum: + - SourceIP + - Header + - Headers + - Cookie + - QueryParams + type: string + required: + - type type: object - timeout: - default: 1s - description: Timeout defines the time - to wait for a health check response. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: - allOf: - - enum: - - HTTP - - TCP - - GRPC - - enum: - - HTTP - - TCP - - GRPC - description: Type defines the type - of health checker. - type: string - unhealthyThreshold: - default: 3 - description: |- - UnhealthyThreshold defines the number of unhealthy health checks required before a backend host is marked unhealthy. - Without RetriableStatuses configured, any health check failure results in the host being immediately - considered unhealthy. When RetriableStatuses is set, health checks returning those statuses are retried - up to this threshold before the host is marked unhealthy. - format: int32 - minimum: 1 - type: integer - required: - - type - type: object - x-kubernetes-validations: - - message: If Health Checker type is HTTP, - http field needs to be set. - rule: 'self.type == ''HTTP'' ? has(self.http) - : !has(self.http)' - - message: If Health Checker type is TCP, - tcp field needs to be set. - rule: 'self.type == ''TCP'' ? has(self.tcp) - : !has(self.tcp)' - - message: The grpc field can only be - set if the Health Checker type is - GRPC. - rule: 'has(self.grpc) ? self.type == - ''GRPC'' : true' - panicThreshold: - description: |- - When number of unhealthy endpoints for a backend reaches this threshold - Envoy will disregard health status and balance across all endpoints. - It's designed to prevent a situation in which host failures cascade throughout the cluster - as load increases. If not set, the default value is 50%. To disable panic mode, set value to `0`. - format: int32 - maximum: 100 - minimum: 0 - type: integer - passive: - description: Passive passive check configuration - properties: - alwaysEjectOneEndpoint: - default: false - description: |- - AlwaysEjectOneEndpoint defines whether at least one host should be ejected, - regardless of MaxEjectionPercent. - type: boolean - baseEjectionTime: - default: 30s - description: BaseEjectionTime defines - the base duration for which a host - will be ejected on consecutive failures. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - consecutive5XxErrors: - default: 5 - description: Consecutive5xxErrors - sets the number of consecutive 5xx - errors triggering ejection. - format: int32 - type: integer - consecutiveGatewayErrors: - description: ConsecutiveGatewayErrors - sets the number of consecutive gateway - errors triggering ejection. - format: int32 - type: integer - consecutiveLocalOriginFailures: - default: 5 - description: |- - ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. - Parameter takes effect only when split_external_local_origin_errors is set to true. - format: int32 - type: integer - failurePercentageThreshold: - description: |- - FailurePercentageThreshold sets the failure percentage threshold for outlier detection. - If the failure percentage of a given host is greater than or equal to this value, it will be ejected. - Defaults to 85. - format: int32 - maximum: 100 - minimum: 0 - type: integer - interval: - default: 3s - description: Interval defines the - time between passive health checks. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxEjectionPercent: - default: 10 - description: MaxEjectionPercent sets - the maximum percentage of hosts - in a cluster that can be ejected. - format: int32 - maximum: 100 - minimum: 0 - type: integer - splitExternalLocalOriginErrors: - default: false - description: SplitExternalLocalOriginErrors - enables splitting of errors between - external and local origin. - type: boolean - type: object - type: object - http2: - description: HTTP2 provides HTTP/2 configuration - for backend connections. - properties: - connectionKeepalive: - description: ConnectionKeepalive configures - HTTP/2 connection keepalive using PING - frames. - properties: - idleInterval: - description: IdleInterval specifies - how long a connection must be idle - before a PING is sent. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: - description: Interval specifies how - often to send HTTP/2 PING frames - to keep the connection alive. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - intervalJitter: - description: |- - IntervalJitter specifies a random jitter percentage added to each interval. - Defaults to 15% if not specified. - format: int32 - maximum: 100 - minimum: 0 - type: integer - timeout: - description: Timeout specifies how - long to wait for a PING response - before considering the connection - dead. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - x-kubernetes-validations: - - message: timeout must be less than interval - rule: '!has(self.timeout) || !has(self.interval) - || duration(self.timeout) < duration(self.interval)' - initialConnectionWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. - If not set, the default value is 1 MiB. - x-kubernetes-int-or-string: true - initialStreamWindowSize: - allOf: - - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ - anyOf: - - type: integer - - type: string - description: |- - InitialStreamWindowSize sets the initial window size for HTTP/2 streams. - If not set, the default value is 64 KiB(64*1024). - x-kubernetes-int-or-string: true - maxConcurrentStreams: - description: |- - MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. - If not set, the default value is 100. - format: int32 - maximum: 2147483647 - minimum: 1 - type: integer - onInvalidMessage: - description: |- - OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error - It's recommended for L2 Envoy deployments to set this value to TerminateStream. - https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two - Default: TerminateConnection - type: string - type: object - loadBalancer: - description: |- - LoadBalancer policy to apply when routing traffic from the gateway to - the backend endpoints. Defaults to `LeastRequest`. - properties: - backendUtilization: - description: |- - BackendUtilization defines the configuration when the load balancer type is - set to BackendUtilization. - properties: - blackoutPeriod: - description: |- - A given endpoint must report load metrics continuously for at least this long before the endpoint weight will be used. - Default is 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - errorUtilizationPenaltyPercent: - description: |- - ErrorUtilizationPenaltyPercent adjusts endpoint weights based on the error rate (eps/qps). - This is expressed as a percentage-based integer where 100 represents 1.0, 150 represents 1.5, etc. - - For example: - - 100 => 1.0x - - 120 => 1.2x - - 200 => 2.0x - - Must be non-negative. - format: int32 - minimum: 0 - type: integer - keepResponseHeaders: - default: false - description: |- - KeepResponseHeaders keeps the ORCA load report headers/trailers before sending the response to the client. - Defaults to false. - type: boolean - metricNamesForComputingUtilization: - description: |- - Metric names used to compute utilization if application_utilization is not set. - For map fields in ORCA proto, use the form ".", e.g., "named_metrics.foo". - items: - type: string - type: array - outOfBand: - description: |- - OutOfBand enables out-of-band ORCA load reporting. When set, Envoy opens a - server-streaming gRPC connection to each endpoint's - xds.service.orca.v3.OpenRcaService/StreamCoreMetrics and pulls load - reports periodically, instead of relying on in-band ORCA metrics - carried in response headers/trailers. - - The backend must implement OpenRcaService for this to take effect. - properties: - authority: - description: |- - Authority overrides the :authority header on the OutOfBand gRPC stream. - If unset, Envoy uses the endpoint hostname, then the dialed address, then - the cluster name. - maxLength: 259 - minLength: 1 - pattern: ^[^\x00\n\r]*$ - type: string - port: - description: |- - Port overrides the port used for the OutOfBand reporting connection, e.g. to - reach a separate reporting sidecar. Defaults to the endpoint's port. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - reportingPeriod: - description: |- - ReportingPeriod is how often Envoy requests load reports from the server. - Must be greater than 0. Defaults to 10s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - x-kubernetes-validations: - - message: reportingPeriod must - be greater than 0 - rule: duration(self) > duration('0s') - type: object - weightExpirationPeriod: - description: If a given endpoint has - not reported load metrics in this - long, stop using the reported weight. - Defaults to 3m. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - weightUpdatePeriod: - description: How often endpoint weights - are recalculated. Values less than - 100ms are capped at 100ms. Default - 1s. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - consistentHash: - description: |- - ConsistentHash defines the configuration when the load balancer type is - set to ConsistentHash - properties: - cookie: - description: Cookie configures the - cookie hash policy when the consistent - hash type is set to Cookie. - properties: - attributes: - additionalProperties: - type: string - description: Additional Attributes - to set for the generated cookie. - type: object - name: - description: |- - Name of the cookie to hash. - If this cookie does not exist in the request, Envoy will generate a cookie and set - the TTL on the response back to the client based on Layer 4 - attributes of the backend endpoint, to ensure that these future requests - go to the same backend endpoint. Make sure to set the TTL field for this case. - type: string - ttl: - description: |- - TTL of the generated cookie if the cookie is not present. This value sets the - Max-Age attribute value. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - required: - - name - type: object - header: - description: |- - Header configures the header hash policy when the consistent hash type is set to Header. - - Deprecated: use Headers instead - properties: - name: - description: Name of the header - to hash. - type: string - required: - - name - type: object - headers: - description: Headers configures the - header hash policy for each header, - when the consistent hash type is - set to Headers. - items: - description: |- - Header defines the header hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the header - to hash. - type: string - required: - - name - type: object - type: array - queryParams: - description: QueryParams configures - the query parameter hash policy - when the consistent hash type is - set to QueryParams. - items: - description: |- - QueryParam defines the query parameter name hashing configuration for consistent hash based - load balancing. - properties: - name: - description: Name of the query - param to hash. - type: string - required: - - name - type: object - type: array - tableSize: - default: 65537 - description: The table size for consistent - hashing, must be prime number limited - to 5000011. - format: int64 - maximum: 5000011 - minimum: 2 - type: integer - type: - description: |- - ConsistentHashType defines the type of input to hash on. Valid Type values are - "SourceIP", - "Header", - "Headers", - "Cookie". - "QueryParams". - enum: - - SourceIP - - Header - - Headers - - Cookie - - QueryParams - type: string - required: - - type - type: object - x-kubernetes-validations: - - message: If consistent hash type is - header, the header field must be set. - rule: 'self.type == ''Header'' ? has(self.header) - : !has(self.header)' - - message: If consistent hash type is - headers, the headers field must be - set. - rule: 'self.type == ''Headers'' ? has(self.headers) - : !has(self.headers)' - - message: If consistent hash type is - cookie, the cookie field must be set. - rule: 'self.type == ''Cookie'' ? has(self.cookie) - : !has(self.cookie)' - - message: If consistent hash type is - queryParams, the queryParams field - must be set. - rule: 'self.type == ''QueryParams'' - ? has(self.queryParams) : !has(self.queryParams)' - dynamicModule: - description: |- - DynamicModule defines the configuration when the load balancer type is - set to DynamicModule. The referenced module must be registered in the - EnvoyProxy resource's dynamicModules allowlist. - properties: - config: - description: |- - Config is optional configuration for the module's load balancer - implementation. This is serialized and passed to the module's - initialization function. - x-kubernetes-preserve-unknown-fields: true - lbPolicyName: - description: |- - LBPolicyName identifies a specific load balancer implementation within - the dynamic module. A single shared library can contain multiple LB - policy implementations. This value is passed to the module's - initialization function to select the appropriate implementation. - maxLength: 253 - minLength: 1 - type: string - name: - description: |- - Name references a dynamic module registered in the EnvoyProxy resource's - dynamicModules list. The referenced module must exist in the registry; - otherwise, the policy will be rejected. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ - type: string - required: - - lbPolicyName - - name - type: object - endpointOverride: - description: |- - EndpointOverride defines the configuration for endpoint override. - When specified, the load balancer will attempt to route requests to endpoints - based on the override information extracted from request headers or metadata. - If the override endpoints are not available, the configured load balancer policy will be used as fallback. - properties: - extractFrom: - description: ExtractFrom defines the - sources to extract endpoint override - information from. - items: - description: EndpointOverrideExtractFrom - defines a source to extract endpoint - override information from. - properties: - header: - description: |- - Header defines the header to get the override endpoint addresses. - The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. - For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. - The IPv6 address is enclosed in square brackets. - type: string - type: object - maxItems: 10 - minItems: 1 - type: array - required: - - extractFrom - type: object - slowStart: - description: |- - SlowStart defines the configuration related to the slow start load balancer policy. - If set, during slow start window, traffic sent to the newly added hosts will gradually increase. - Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. - properties: - window: - description: |- - Window defines the duration of the warm up period for newly added host. - During slow start window, traffic sent to the newly added hosts will gradually increase. - Currently only supports linear growth of traffic. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - required: - - window - type: object - type: - description: |- - Type decides the type of Load Balancer policy. - Valid LoadBalancerType values are - "ConsistentHash", - "LeastRequest", - "Random", - "RoundRobin", - "BackendUtilization", - "DynamicModule". - enum: - - ConsistentHash - - LeastRequest - - Random - - RoundRobin - - BackendUtilization - - DynamicModule - type: string - zoneAware: - description: ZoneAware defines the configuration - related to the distribution of requests - between locality zones. - properties: - preferLocal: - description: PreferLocalZone configures - zone-aware routing to prefer sending - traffic to the local locality zone. + x-kubernetes-validations: + - message: If consistent hash type + is header, the header field must + be set. + rule: 'self.type == ''Header'' ? + has(self.header) : !has(self.header)' + - message: If consistent hash type + is headers, the headers field + must be set. + rule: 'self.type == ''Headers'' + ? has(self.headers) : !has(self.headers)' + - message: If consistent hash type + is cookie, the cookie field must + be set. + rule: 'self.type == ''Cookie'' ? + has(self.cookie) : !has(self.cookie)' + - message: If consistent hash type + is queryParams, the queryParams + field must be set. + rule: 'self.type == ''QueryParams'' + ? has(self.queryParams) : !has(self.queryParams)' + dynamicModule: + description: |- + DynamicModule defines the configuration when the load balancer type is + set to DynamicModule. The referenced module must be registered in the + EnvoyProxy resource's dynamicModules allowlist. properties: - force: + config: description: |- - ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior - which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. - properties: - minEndpointsInZoneThreshold: - description: |- - MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone - override. This is useful for protecting zones with fewer endpoints. - format: int32 - type: integer - type: object - minEndpointsThreshold: - description: MinEndpointsThreshold - is the minimum number of total - upstream endpoints across all - zones required to enable zone-aware - routing. - format: int64 - type: integer - percentageEnabled: - description: Configures percentage - of requests that will be considered - for zone aware routing if zone - aware routing is configured. - If not specified, Envoy defaults - to 100%. - format: int32 - maximum: 100 - minimum: 0 - type: integer + Config is optional configuration for the module's load balancer + implementation. This is serialized and passed to the module's + initialization function. + x-kubernetes-preserve-unknown-fields: true + lbPolicyName: + description: |- + LBPolicyName identifies a specific load balancer implementation within + the dynamic module. A single shared library can contain multiple LB + policy implementations. This value is passed to the module's + initialization function to select the appropriate implementation. + maxLength: 253 + minLength: 1 + type: string + name: + description: |- + Name references a dynamic module registered in the EnvoyProxy resource's + dynamicModules list. The referenced module must exist in the registry; + otherwise, the policy will be rejected. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ + type: string + required: + - lbPolicyName + - name type: object - weightedZones: + endpointOverride: description: |- - WeightedZones configures weight-based traffic distribution across locality zones. - Traffic is distributed proportionally based on the sum of all zone weights. - items: - description: WeightedZoneConfig - defines the weight for a specific - locality zone. - properties: - weight: - description: |- - Weight defines the weight for this locality. - Higher values receive more traffic. The actual traffic distribution - is proportional to this value relative to other localities. - format: int32 - type: integer - zone: - description: |- - Zone specifies the topology zone this weight applies to. - The value should match the topology.kubernetes.io/zone label - of the nodes where endpoints are running. - Zones not listed in the configuration receive a default weight of 1. - type: string - required: - - weight - - zone - type: object - type: array - x-kubernetes-list-map-keys: - - zone - x-kubernetes-list-type: map - type: object - required: - - type - type: object - x-kubernetes-validations: - - message: If LoadBalancer type is consistentHash, - consistentHash field needs to be set. - rule: 'self.type == ''ConsistentHash'' ? - has(self.consistentHash) : !has(self.consistentHash)' - - message: If LoadBalancer type is BackendUtilization, - backendUtilization field needs to be set. - rule: 'self.type == ''BackendUtilization'' - ? has(self.backendUtilization) : !has(self.backendUtilization)' - - message: If LoadBalancer type is DynamicModule, - dynamicModule field needs to be set. - rule: 'self.type == ''DynamicModule'' ? - has(self.dynamicModule) : !has(self.dynamicModule)' - - message: Currently SlowStart is only supported - for RoundRobin, LeastRequest, and BackendUtilization - load balancers. - rule: 'self.type in [''Random'', ''ConsistentHash'', - ''DynamicModule''] ? !has(self.slowStart) - : true' - - message: PreferLocal zone-aware routing - is not supported for ConsistentHash load - balancers. Use weightedZones instead. - rule: 'self.type == ''ConsistentHash'' && - has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: PreferLocal zone-aware routing - is not currently supported for BackendUtilization - load balancers. Only WeightedZones can - be used with BackendUtilization. - rule: 'self.type == ''BackendUtilization'' - && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) - : true' - - message: ZoneAware routing is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' ? - !has(self.zoneAware) : true' - - message: ZoneAware PreferLocal and WeightedZones - cannot be specified together. - rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) - && has(self.zoneAware.weightedZones)) - : true' - - message: EndpointOverride is not supported - for DynamicModule load balancers. - rule: 'self.type == ''DynamicModule'' ? - !has(self.endpointOverride) : true' - proxyProtocol: - description: ProxyProtocol enables the Proxy - Protocol when communicating with the backend. - properties: - version: - description: |- - Version of ProxyProtocol - Valid ProxyProtocolVersion values are - "V1" - "V2" - enum: - - V1 - - V2 - type: string - required: - - version - type: object - retry: - description: |- - Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. - If not set, retry will be disabled. - properties: - numAttemptsPerPriority: - description: |- - NumAttemptsPerPriority defines the number of requests (initial attempt + retries) - that should be sent to the same priority before switching to a different one. - If not specified or set to 0, all requests are sent to the highest priority that is healthy. - format: int32 - type: integer - numRetries: - default: 2 - description: NumRetries is the number - of retries to be attempted. Defaults - to 2. - format: int32 - minimum: 0 - type: integer - perRetry: - description: PerRetry is the retry policy - to be applied per retry attempt. - properties: - backOff: + EndpointOverride defines the configuration for endpoint override. + When specified, the load balancer will attempt to route requests to endpoints + based on the override information extracted from request headers or metadata. + If the override endpoints are not available, the configured load balancer policy will be used as fallback. + properties: + extractFrom: + description: ExtractFrom defines + the sources to extract endpoint + override information from. + items: + description: EndpointOverrideExtractFrom + defines a source to extract + endpoint override information + from. + properties: + header: + description: |- + Header defines the header to get the override endpoint addresses. + The header value must specify at least one endpoint in `IP:Port` format or multiple endpoints in `IP:Port,IP:Port,...` format. + For example `10.0.0.5:8080` or `[2600:4040:5204::1574:24ae]:80`. + The IPv6 address is enclosed in square brackets. + type: string + type: object + maxItems: 10 + minItems: 1 + type: array + required: + - extractFrom + type: object + slowStart: description: |- - Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential - back-off algorithm for retries. For additional details, - see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + SlowStart defines the configuration related to the slow start load balancer policy. + If set, during slow start window, traffic sent to the newly added hosts will gradually increase. + Supported for RoundRobin, LeastRequest, and BackendUtilization load balancers. properties: - baseInterval: - description: BaseInterval is the - base interval between retries. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - maxInterval: + window: description: |- - MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. - The default is 10 times the base_interval + Window defines the duration of the warm up period for newly added host. + During slow start window, traffic sent to the newly added hosts will gradually increase. + Currently only supports linear growth of traffic. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string + required: + - window type: object - timeout: - description: Timeout is the timeout - per retry attempt. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - type: object - retryOn: - description: |- - RetryOn specifies the retry trigger condition. - - If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). - properties: - httpStatusCodes: + type: description: |- - HttpStatusCodes specifies the http status codes to be retried. - The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. - items: - description: HTTPStatus defines - the http status code. - maximum: 599 - minimum: 100 - type: integer - type: array - triggers: - description: Triggers specifies the - retry trigger condition(Http/Grpc). - items: - description: TriggerEnum specifies - the conditions that trigger retries. - enum: - - 5xx - - gateway-error - - reset - - reset-before-request - - connect-failure - - retriable-4xx - - refused-stream - - retriable-status-codes - - cancelled - - deadline-exceeded - - internal - - resource-exhausted - - unavailable - type: string - type: array + Type decides the type of Load Balancer policy. + Valid LoadBalancerType values are + "ConsistentHash", + "LeastRequest", + "Random", + "RoundRobin", + "BackendUtilization", + "DynamicModule". + enum: + - ConsistentHash + - LeastRequest + - Random + - RoundRobin + - BackendUtilization + - DynamicModule + type: string + zoneAware: + description: ZoneAware defines the + configuration related to the distribution + of requests between locality zones. + properties: + preferLocal: + description: PreferLocalZone configures + zone-aware routing to prefer + sending traffic to the local + locality zone. + properties: + force: + description: |- + ForceLocalZone defines override configuration for forcing all traffic to stay within the local zone instead of the default behavior + which maintains equal distribution among upstream endpoints while sending as much traffic as possible locally. + properties: + minEndpointsInZoneThreshold: + description: |- + MinEndpointsInZoneThreshold is the minimum number of upstream endpoints in the local zone required to honor the forceLocalZone + override. This is useful for protecting zones with fewer endpoints. + format: int32 + type: integer + type: object + minEndpointsThreshold: + description: MinEndpointsThreshold + is the minimum number of + total upstream endpoints + across all zones required + to enable zone-aware routing. + format: int64 + type: integer + percentageEnabled: + description: Configures percentage + of requests that will be + considered for zone aware + routing if zone aware routing + is configured. If not specified, + Envoy defaults to 100%. + format: int32 + maximum: 100 + minimum: 0 + type: integer + type: object + weightedZones: + description: |- + WeightedZones configures weight-based traffic distribution across locality zones. + Traffic is distributed proportionally based on the sum of all zone weights. + items: + description: WeightedZoneConfig + defines the weight for a specific + locality zone. + properties: + weight: + description: |- + Weight defines the weight for this locality. + Higher values receive more traffic. The actual traffic distribution + is proportional to this value relative to other localities. + format: int32 + type: integer + zone: + description: |- + Zone specifies the topology zone this weight applies to. + The value should match the topology.kubernetes.io/zone label + of the nodes where endpoints are running. + Zones not listed in the configuration receive a default weight of 1. + type: string + required: + - weight + - zone + type: object + type: array + x-kubernetes-list-map-keys: + - zone + x-kubernetes-list-type: map + type: object + required: + - type type: object - type: object - tcpKeepalive: - description: |- - TcpKeepalive settings associated with the upstream client connection. - Disabled by default. - properties: - idleTime: - description: |- - The duration a connection needs to be idle before keep-alive - probes start being sent. - The duration format is - Defaults to `7200s`. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - interval: + x-kubernetes-validations: + - message: If LoadBalancer type is consistentHash, + consistentHash field needs to be set. + rule: 'self.type == ''ConsistentHash'' + ? has(self.consistentHash) : !has(self.consistentHash)' + - message: If LoadBalancer type is BackendUtilization, + backendUtilization field needs to + be set. + rule: 'self.type == ''BackendUtilization'' + ? has(self.backendUtilization) : !has(self.backendUtilization)' + - message: If LoadBalancer type is DynamicModule, + dynamicModule field needs to be set. + rule: 'self.type == ''DynamicModule'' + ? has(self.dynamicModule) : !has(self.dynamicModule)' + - message: Currently SlowStart is only + supported for RoundRobin, LeastRequest, + and BackendUtilization load balancers. + rule: 'self.type in [''Random'', ''ConsistentHash'', + ''DynamicModule''] ? !has(self.slowStart) + : true' + - message: PreferLocal zone-aware routing + is not supported for ConsistentHash + load balancers. Use weightedZones + instead. + rule: 'self.type == ''ConsistentHash'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: PreferLocal zone-aware routing + is not currently supported for BackendUtilization + load balancers. Only WeightedZones + can be used with BackendUtilization. + rule: 'self.type == ''BackendUtilization'' + && has(self.zoneAware) ? !has(self.zoneAware.preferLocal) + : true' + - message: ZoneAware routing is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' + ? !has(self.zoneAware) : true' + - message: ZoneAware PreferLocal and WeightedZones + cannot be specified together. + rule: 'has(self.zoneAware) ? !(has(self.zoneAware.preferLocal) + && has(self.zoneAware.weightedZones)) + : true' + - message: EndpointOverride is not supported + for DynamicModule load balancers. + rule: 'self.type == ''DynamicModule'' + ? !has(self.endpointOverride) : true' + proxyProtocol: + description: ProxyProtocol enables the + Proxy Protocol when communicating with + the backend. + properties: + version: + description: |- + Version of ProxyProtocol + Valid ProxyProtocolVersion values are + "V1" + "V2" + enum: + - V1 + - V2 + type: string + required: + - version + type: object + retry: description: |- - The duration between keep-alive probes. - Defaults to `75s`. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - probes: + Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. + If not set, retry will be disabled. + properties: + numAttemptsPerPriority: + description: |- + NumAttemptsPerPriority defines the number of requests (initial attempt + retries) + that should be sent to the same priority before switching to a different one. + If not specified or set to 0, all requests are sent to the highest priority that is healthy. + format: int32 + type: integer + numRetries: + default: 2 + description: NumRetries is the number + of retries to be attempted. Defaults + to 2. + format: int32 + minimum: 0 + type: integer + perRetry: + description: PerRetry is the retry + policy to be applied per retry attempt. + properties: + backOff: + description: |- + Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential + back-off algorithm for retries. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + properties: + baseInterval: + description: BaseInterval + is the base interval between + retries. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxInterval: + description: |- + MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. + The default is 10 times the base_interval + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + timeout: + description: Timeout is the timeout + per retry attempt. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + retryOn: + description: |- + RetryOn specifies the retry trigger condition. + + If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). + properties: + httpStatusCodes: + description: |- + HttpStatusCodes specifies the http status codes to be retried. + The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. + items: + description: HTTPStatus defines + the http status code. + maximum: 599 + minimum: 100 + type: integer + type: array + triggers: + description: Triggers specifies + the retry trigger condition(Http/Grpc). + items: + description: TriggerEnum specifies + the conditions that trigger + retries. + enum: + - 5xx + - gateway-error + - reset + - reset-before-request + - connect-failure + - retriable-4xx + - refused-stream + - retriable-status-codes + - cancelled + - deadline-exceeded + - internal + - resource-exhausted + - unavailable + type: string + type: array + type: object + type: object + tcpKeepalive: description: |- - The total number of unacknowledged probes to send before deciding - the connection is dead. - Defaults to 9. - format: int32 - type: integer - type: object - timeout: - description: Timeout settings for the backend - connections. - properties: - http: - description: Timeout settings for HTTP. + TcpKeepalive settings associated with the upstream client connection. + Disabled by default. properties: - connectionIdleTimeout: + idleTime: description: |- - The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. - Default: 1 hour. + The duration a connection needs to be idle before keep-alive + probes start being sent. + The duration format is + Defaults to `7200s`. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - maxConnectionDuration: + interval: description: |- - The maximum duration of an HTTP connection. - Default: unlimited. + The duration between keep-alive probes. + Defaults to `75s`. pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ type: string - maxStreamDuration: + probes: description: |- - MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time - from when the request is sent until the response stream is fully consumed and does not apply to - non-streaming requests. - When set to "0s", no max duration is applied and streams can run indefinitely. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - requestTimeout: - description: RequestTimeout is the - time until which entire response - is received from the upstream. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string - streamIdleTimeout: - description: |2- - The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. - If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + The total number of unacknowledged probes to send before deciding + the connection is dead. + Defaults to 9. + format: int32 + type: integer type: object - tcp: - description: Timeout settings for TCP. + timeout: + description: Timeout settings for the + backend connections. properties: - connectTimeout: - description: |- - The timeout for network connection establishment, including TCP and TLS handshakes. - Default: 10 seconds. - pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ - type: string + http: + description: Timeout settings for + HTTP. + properties: + connectionIdleTimeout: + description: |- + The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + Default: 1 hour. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxConnectionDuration: + description: |- + The maximum duration of an HTTP connection. + Default: unlimited. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxStreamDuration: + description: |- + MaxStreamDuration is the maximum duration for a stream to complete. This timeout measures the time + from when the request is sent until the response stream is fully consumed and does not apply to + non-streaming requests. + When set to "0s", no max duration is applied and streams can run indefinitely. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + requestTimeout: + description: RequestTimeout is + the time until which entire + response is received from the + upstream. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + streamIdleTimeout: + description: |2- + The stream idle timeout defines the amount of time a stream can exist without any upstream or downstream activity. + If not specified, StreamIdleTimeout is inherited from the listener-level setting, which can be configured via ClientTrafficPolicy. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + tcp: + description: Timeout settings for + TCP. + properties: + connectTimeout: + description: |- + The timeout for network connection establishment, including TCP and TLS handshakes. + Default: 10 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object type: object type: object + x-kubernetes-validations: + - message: predictivePercent in preconnect + policy only works with RoundRobin or Random + load balancers + rule: '!((has(self.connection) && has(self.connection.preconnect) + && has(self.connection.preconnect.predictivePercent)) + && !(has(self.loadBalancer) && has(self.loadBalancer.type) + && self.loadBalancer.type in [''Random'', + ''RoundRobin'']))' + endSessionEndpoint: + description: |- + The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). + + If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. + EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. + type: string + issuer: + description: |- + The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). + Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST + be https, a host component, and optionally, port and path components and + no query or fragment components. + pattern: ^https://[^/?#@]+(/[^?#]*)?$ + type: string + tokenEndpoint: + description: |- + The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). + If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). + type: string + required: + - issuer type: object x-kubernetes-validations: - - message: predictivePercent in preconnect policy - only works with RoundRobin or Random load - balancers - rule: '!((has(self.connection) && has(self.connection.preconnect) - && has(self.connection.preconnect.predictivePercent)) - && !(has(self.loadBalancer) && has(self.loadBalancer.type) - && self.loadBalancer.type in [''Random'', - ''RoundRobin'']))' - endSessionEndpoint: + - message: BackendRefs must be used, backendRef + is not supported. + rule: '!has(self.backendRef)' + - message: Retry timeout is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? + !has(self.backendSettings.retry.perRetry.timeout):true):true):true + - message: HTTPStatusCodes is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? + !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true + redirectURL: description: |- - The OIDC Provider's [end session endpoint](https://openid.net/specs/openid-connect-core-1_0.html#RPLogout). - - If the end session endpoint is provided, EG will use it to log out the user from the OIDC Provider when the user accesses the logout path. - EG will also try to discover the end session endpoint from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) when authorizationEndpoint or tokenEndpoint is not provided. + The redirect URL to be used in the OIDC + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" type: string - issuer: + refreshToken: + default: true description: |- - The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). - Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST - be https, a host component, and optionally, port and path components and - no query or fragment components. - pattern: ^https://[^/?#@]+(/[^?#]*)?$ - type: string - tokenEndpoint: + RefreshToken indicates whether the Envoy should automatically refresh the + id token and access token when they expire. + When set to true, the Envoy will use the refresh token to get a new id token + and access token when they expire. + + If not specified, defaults to true. + type: boolean + resources: description: |- - The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - type: string + The OIDC resources to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + items: + type: string + type: array + scopes: + description: |- + The OIDC scopes to be used in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + The "openid" scope is always added to the list of scopes if not already + specified. + items: + type: string + type: array required: - - issuer + - clientSecret + - provider type: object x-kubernetes-validations: - - message: BackendRefs must be used, backendRef is - not supported. - rule: '!has(self.backendRef)' - - message: Retry timeout is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? - !has(self.backendSettings.retry.perRetry.timeout):true):true):true - - message: HTTPStatusCodes is not supported. - rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? - !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true - redirectURL: + - message: only one of clientID or clientIDRef must + be set + rule: (has(self.clientID) && !has(self.clientIDRef)) + || (!has(self.clientID) && has(self.clientIDRef)) + - message: forwardAccessToken cannot be true when + forwardIDToken.header is Authorization + rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken + && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() + == ''authorization'')' + required: + - oidc + type: object + spiffeJWTSVID: + description: SPIFFEJWTSVID obtains the subject token as + a JWT-SVID from the SPIFFE Workload API. + properties: + audience: description: |- - The redirect URL to be used in the OIDC - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" + Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint + validates it, so it must match the audience that endpoint expects. + minLength: 1 type: string - refreshToken: - default: true - description: |- - RefreshToken indicates whether the Envoy should automatically refresh the - id token and access token when they expire. - When set to true, the Envoy will use the refresh token to get a new id token - and access token when they expire. - - If not specified, defaults to true. - type: boolean - resources: - description: |- - The OIDC resources to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - items: - type: string - type: array - scopes: + socketPath: description: |- - The OIDC scopes to be used in the - [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - The "openid" scope is always added to the list of scopes if not already - specified. - items: - type: string - type: array + SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". + When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. + pattern: ^(unix|tcp)://.+ + type: string required: - - clientSecret - - provider + - audience type: object - x-kubernetes-validations: - - message: only one of clientID or clientIDRef must be - set - rule: (has(self.clientID) && !has(self.clientIDRef)) - || (!has(self.clientID) && has(self.clientIDRef)) - - message: forwardAccessToken cannot be true when forwardIDToken.header - is Authorization - rule: '!(has(self.forwardAccessToken) && self.forwardAccessToken - && has(self.forwardIDToken) && self.forwardIDToken.header.lowerAscii() - == ''authorization'')' - required: - - oidc - type: object - scopes: - description: |- - Scopes is the list of scopes requested for the exchanged access token. They are unrelated to - the scopes requested from the OIDC provider for the subject token. - items: - type: string - maxItems: 16 - type: array - spiffeJWTSVID: - description: SPIFFEJWTSVID obtains the subject token as a - JWT-SVID from the SPIFFE Workload API. - properties: - audience: - description: |- - Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint - validates it, so it must match the audience that endpoint expects. - minLength: 1 - type: string - socketPath: - description: |- - SocketPath is the address of the SPIFFE Workload API, e.g. "unix:///run/spire/sockets/agent.sock". - When unset, the SPIFFE_ENDPOINT_SOCKET environment variable of the controller is used. - pattern: ^(unix|tcp)://.+ - type: string - required: - - audience type: object + x-kubernetes-validations: + - message: Exactly one of oidcExchangeToken or spiffeJWTSVID + must be specified + rule: '[has(self.oidcExchangeToken), has(self.spiffeJWTSVID)].filter(x, + x).size() == 1' subjectTokenType: default: urn:ietf:params:oauth:token-type:jwt description: |- @@ -14855,12 +14899,9 @@ spec: - message: tokenURL must be a valid URL rule: isURL(self) required: + - subjectToken - tokenURL type: object - x-kubernetes-validations: - - message: Exactly one of oidcExchangeToken or spiffeJWTSVID must - be specified - rule: has(self.oidcExchangeToken) != has(self.spiffeJWTSVID) required: - tokenExchange type: object diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 2ffca28c52..f227ffcba3 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -3191,6 +3191,7 @@ MCPRouteList contains a list of MCPRoute. - [BackendSecurityPolicySPIFFEJWTSVID](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffejwtsvid) - [BackendSecurityPolicySpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspec) - [BackendSecurityPolicyStatus](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicystatus) +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) - [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - [BackendSecurityPolicyType](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytype) - [CredentialOverrideFromDynamicMetadata](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromdynamicmetadata) @@ -3880,7 +3881,7 @@ default service account credentials when running on GCP. **Appears in:** - [AWSOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-awsoidcexchangetoken) - [AzureOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-azureoidcexchangetoken) -- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) - [GCPOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-gcpoidcexchangetoken) BackendSecurityPolicyOIDC specifies OIDC related fields. @@ -3943,7 +3944,7 @@ BackendSecurityPolicyOpenAICredentials contains the supported authentication mec **Appears in:** -- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) +- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the SPIFFE Workload API. @@ -4054,6 +4055,33 @@ BackendSecurityPolicyStatus contains the conditions by the reconciliation result /> +#### BackendSecurityPolicySubjectToken + + + +**Appears in:** +- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) + +BackendSecurityPolicySubjectToken specifies the source of a subject token. +Exactly one source must be configured. + +##### Fields + + + + + + #### BackendSecurityPolicyTokenExchange @@ -4062,12 +4090,11 @@ BackendSecurityPolicyStatus contains the conditions by the reconciliation result - [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -The controller obtains a subject token from exactly one source (oidcExchangeToken or spiffeJWTSVID), -exchanges it at TokenURL for an access token, and stores the access token in a secret, -rotating it before it expires. +The controller obtains a subject token, exchanges it at TokenURL for an access token, +and stores the access token in a secret, rotating it before it expires. TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply -to either subject token source. +to any subject token source. https://datatracker.ietf.org/doc/html/rfc8693 @@ -4097,15 +4124,10 @@ https://datatracker.ietf.org/doc/html/rfc8693 required="false" description="Scopes is the list of scopes requested for the exchanged access token. They are unrelated to
the scopes requested from the OIDC provider for the subject token." /> diff --git a/tests/crdcel/main_test.go b/tests/crdcel/main_test.go index 9fbe93371b..84d49a110e 100644 --- a/tests/crdcel/main_test.go +++ b/tests/crdcel/main_test.go @@ -255,7 +255,7 @@ func TestBackendSecurityPolicies(t *testing.T) { }, { name: "openai_invalid_socket_path.yaml", - expErr: "spec.openAICredentials.tokenExchange.spiffeJWTSVID.socketPath in body should match", + expErr: "spec.openAICredentials.tokenExchange.subjectToken.spiffeJWTSVID.socketPath in body should match", }, { name: "openai_with_apikey.yaml", diff --git a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml index d3a056605a..f24bf4dbed 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml @@ -16,5 +16,6 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - spiffeJWTSVID: - audience: https://auth.example.com + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml index b258126b26..db5dd4a6cf 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml @@ -13,6 +13,7 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - spiffeJWTSVID: - audience: https://auth.example.com - socketPath: /run/spire/sockets/agent.sock + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com + socketPath: /run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml index bae1a2cbe1..77f3a5888e 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml @@ -13,5 +13,6 @@ spec: openAICredentials: tokenExchange: tokenURL: not a url - spiffeJWTSVID: - audience: https://auth.example.com + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml index beb716be9e..b99786f1de 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml @@ -13,3 +13,4 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token + subjectToken: {} diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml index 99f59a714a..3fce3c3c3f 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml @@ -13,12 +13,13 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - spiffeJWTSVID: - audience: https://auth.example.com - oidcExchangeToken: - oidc: - provider: - issuer: https://placeholder - clientID: placeholder - clientSecret: - name: placeholder + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com + oidcExchangeToken: + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml index f0ef3b8da0..bf39686f00 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml @@ -14,10 +14,11 @@ spec: tokenExchange: tokenURL: https://auth.example.com/oauth/token subjectTokenType: urn:ietf:params:oauth:token-type:access_token - oidcExchangeToken: - oidc: - provider: - issuer: https://placeholder - clientID: placeholder - clientSecret: - name: placeholder + subjectToken: + oidcExchangeToken: + oidc: + provider: + issuer: https://placeholder + clientID: placeholder + clientSecret: + name: placeholder diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml index 40c2201cd3..9ac5c502bb 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml @@ -18,6 +18,7 @@ spec: audience: https://api.openai.com scopes: - api.model.read - spiffeJWTSVID: - audience: https://auth.example.com - socketPath: unix:///run/spire/sockets/agent.sock + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com + socketPath: unix:///run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml index 20685b6399..31f4b61501 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml @@ -16,5 +16,6 @@ spec: openAICredentials: tokenExchange: tokenURL: https://auth.example.com/oauth/token - spiffeJWTSVID: - audience: https://auth.example.com + subjectToken: + spiffeJWTSVID: + audience: https://auth.example.com From 7eb77b063e614350767e2b085e8e577e37178146 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 15:50:08 -0400 Subject: [PATCH 06/17] controller: reject OpenAICredentials explicitly The controller does not act on OpenAICredentials yet. Return a "not yet supported" error from credential rotation and the gateway auth translation instead of the generic unsupported-type errors, and handle the type in getBSPGeneratedSecretName so it cannot panic once rotation reaches it. Document which hop each audience field applies to. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 5 +++ .../controller/backend_security_policy.go | 4 ++ .../backend_security_policy_test.go | 43 +++++++++++++++++++ internal/controller/gateway.go | 2 + internal/controller/gateway_test.go | 12 ++++++ site/docs/api/api.mdx | 5 +++ 6 files changed, 71 insertions(+) diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index b22f427c2b..55b2219592 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -382,6 +382,11 @@ type BackendSecurityPolicyOpenAICredentials struct { // TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply // to any subject token source. // +// Each hop has its own audience: +// - subjectToken.spiffeJWTSVID.audience: the "aud" claim of the JWT-SVID, i.e. the token exchange endpoint. +// - subjectToken.oidcExchangeToken.aud: the audience requested from the OIDC provider for the subject token. +// - audience: the service the exchanged access token is intended for, e.g. the OpenAI API. +// // https://datatracker.ietf.org/doc/html/rfc8693 type BackendSecurityPolicyTokenExchange struct { // TokenURL is the token exchange endpoint. diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go index f1c50f0c3a..adac8edf50 100644 --- a/internal/controller/backend_security_policy.go +++ b/internal/controller/backend_security_policy.go @@ -276,6 +276,8 @@ func (c *BackendSecurityPolicyController) rotateCredential(ctx context.Context, return ctrl.Result{}, nil } + case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: + return ctrl.Result{}, fmt.Errorf("backend security type %s is not yet supported", bsp.Spec.Type) default: err = fmt.Errorf("backend security type %s does not support OIDC token exchange", bsp.Spec.Type) c.logger.Error(err, "unsupported backend security type", "namespace", bsp.Namespace, "name", bsp.Name) @@ -471,6 +473,8 @@ func getBSPGeneratedSecretName(bsp *aigv1b1.BackendSecurityPolicy) string { aigv1b1.BackendSecurityPolicyTypeAzureAPIKey, aigv1b1.BackendSecurityPolicyTypeAnthropicAPIKey: return "" // APIKey does not require rotation. + case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: + return "" // Not yet supported, so no secret is generated. default: panic("BUG: unsupported backend security policy type: " + string(bsp.Spec.Type)) } diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index b4a0024d8e..b708b5d44b 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -864,6 +864,37 @@ func TestNewBackendSecurityPolicyController_RotateCredentialInvalidType(t *testi require.Equal(t, time.Duration(0), res.RequeueAfter) } +func TestBackendSecurityPolicyController_Reconcile_OpenAICredentialsNotYetSupported(t *testing.T) { + eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() + cl := requireNewFakeClientWithIndexes(t) + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) + + bsp := &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: "default"}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ + TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ + TokenURL: "https://auth.example.com/oauth/token", + SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{Audience: "https://auth.example.com"}, + }, + }, + }, + }, + } + require.NoError(t, cl.Create(t.Context(), bsp)) + + _, err := c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "openai-bsp"}}) + require.EqualError(t, err, "backend security type OpenAICredentials is not yet supported") + + var updated aigv1b1.BackendSecurityPolicy + require.NoError(t, cl.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "openai-bsp"}, &updated)) + require.Len(t, updated.Status.Conditions, 1) + require.Equal(t, aigv1b1.ConditionTypeNotAccepted, updated.Status.Conditions[0].Type) + require.Equal(t, "backend security type OpenAICredentials is not yet supported", updated.Status.Conditions[0].Message) +} + func TestNewBackendSecurityPolicyController_RotateCredentialAwsCredentialFile(t *testing.T) { eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() cl := fake.NewClientBuilder().WithScheme(Scheme).Build() @@ -1577,6 +1608,18 @@ func TestGetBSPGeneratedSecretName(t *testing.T) { }, expectedName: "", }, + { + name: "OpenAICredentials type", + bsp: &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openai-bsp", + }, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + }, + }, + expectedName: "", + }, } for _, tt := range tests { diff --git a/internal/controller/gateway.go b/internal/controller/gateway.go index f5e514c5a9..c056112cff 100644 --- a/internal/controller/gateway.go +++ b/internal/controller/gateway.go @@ -1006,6 +1006,8 @@ func (c *GatewayController) bspToFilterAPIBackendAuth(ctx context.Context, backe } hasStaticCred = true } + case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: + return nil, fmt.Errorf("backend security type %s is not yet supported for policy %s", spec.Type, backendSecurityPolicy.Name) default: return nil, fmt.Errorf("invalid backend security type %s for policy %s", spec.Type, backendSecurityPolicy.Name) } diff --git a/internal/controller/gateway_test.go b/internal/controller/gateway_test.go index 5a8ea0b4d0..21ea25f43c 100644 --- a/internal/controller/gateway_test.go +++ b/internal/controller/gateway_test.go @@ -1500,6 +1500,18 @@ func TestGatewayController_bspToFilterAPIBackendAuth_ErrorCases(t *testing.T) { }, expectedError: "failed to get secret test-namespace/missing-aws-secret", }, + { + name: "openai credentials not yet supported", + bspName: "openai-bsp", + bsp: &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: namespace}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{}, + }, + }, + expectedError: "backend security type OpenAICredentials is not yet supported for policy openai-bsp", + }, } for _, tt := range tests { diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index f227ffcba3..9fb8a30d1e 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -4096,6 +4096,11 @@ and stores the access token in a secret, rotating it before it expires. TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply to any subject token source. +Each hop has its own audience: + - subjectToken.spiffeJWTSVID.audience: the "aud" claim of the JWT-SVID, i.e. the token exchange endpoint. + - subjectToken.oidcExchangeToken.aud: the audience requested from the OIDC provider for the subject token. + - audience: the service the exchanged access token is intended for, e.g. the OpenAI API. + https://datatracker.ietf.org/doc/html/rfc8693 ##### Fields From 5d8522fee7a12ea312428a3479b249a302f5df10 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 23:31:01 -0400 Subject: [PATCH 07/17] backendauth: add OpenAI access token handler Add filterapi.OpenAIAuth carrying an access token plus optional organization and project IDs. The handler sets Authorization: Bearer and, when configured, OpenAI-Organization and OpenAI-Project. The same headers are set when the token comes from a credential override. Part of #2791. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- internal/backendauth/auth.go | 3 + internal/backendauth/auth_test.go | 6 ++ .../backendauth/credential_override_test.go | 19 ++++++ internal/backendauth/openai.go | 54 +++++++++++++++ internal/backendauth/openai_test.go | 65 +++++++++++++++++++ internal/filterapi/filterconfig.go | 21 ++++++ internal/filterapi/filterconfig_test.go | 8 +++ 7 files changed, 176 insertions(+) create mode 100644 internal/backendauth/openai.go create mode 100644 internal/backendauth/openai_test.go diff --git a/internal/backendauth/auth.go b/internal/backendauth/auth.go index 69abf894e4..40d11d0bd6 100644 --- a/internal/backendauth/auth.go +++ b/internal/backendauth/auth.go @@ -45,6 +45,9 @@ func NewHandler(ctx context.Context, config *filterapi.BackendAuth) (filterapi.B case config.GCPAuth != nil: inner, err = newGCPHandler(ctx, config.GCPAuth) applyFn = makeGCPApplyFn(config.GCPAuth.Region, config.GCPAuth.ProjectName) + case config.OpenAIAuth != nil: + inner, err = newOpenAIHandler(config.OpenAIAuth) + applyFn = makeOpenAIApplyFn(config.OpenAIAuth.Organization, config.OpenAIAuth.Project) case config.AnthropicAPIKey != nil: inner, err = newAnthropicAPIKeyHandler(config.AnthropicAPIKey) applyFn = applyAnthropicCredential diff --git a/internal/backendauth/auth_test.go b/internal/backendauth/auth_test.go index 7d445b55b1..1229221281 100644 --- a/internal/backendauth/auth_test.go +++ b/internal/backendauth/auth_test.go @@ -56,6 +56,12 @@ aws_secret_access_key = test AnthropicAPIKey: &filterapi.AnthropicAPIKeyAuth{Key: "TEST"}, }, }, + { + name: "OpenAIAuth", + config: &filterapi.BackendAuth{ + OpenAIAuth: &filterapi.OpenAIAuth{AccessToken: "some-access-token", Organization: "org-123"}, + }, + }, } { t.Run(tt.name, func(t *testing.T) { _, err := NewHandler(t.Context(), tt.config) diff --git a/internal/backendauth/credential_override_test.go b/internal/backendauth/credential_override_test.go index b0b92cd819..c9b3141170 100644 --- a/internal/backendauth/credential_override_test.go +++ b/internal/backendauth/credential_override_test.go @@ -252,6 +252,25 @@ func TestCredentialOverrideHandler_PerAuthType(t *testing.T) { require.Equal(t, "Bearer per-req-token", headers["Authorization"]) }) + t.Run("openai credentials sets Authorization Bearer and organization/project", func(t *testing.T) { + auth := &filterapi.OpenAIAuth{AccessToken: "static", Organization: "org-123", Project: "proj_456"} + inner, err := newOpenAIHandler(auth) + require.NoError(t, err) + + h := &credentialOverrideHandler{ + inner: inner, + config: makeHeaderOverride("x-aigw-openai-access-token", false), + applyFn: makeOpenAIApplyFn(auth.Organization, auth.Project), + } + + headers := map[string]string{"x-aigw-openai-access-token": "per-req-token"} + _, err = h.Do(t.Context(), headers, nil) + require.NoError(t, err) + require.Equal(t, "Bearer per-req-token", headers["Authorization"]) + require.Equal(t, "org-123", headers["OpenAI-Organization"]) + require.Equal(t, "proj_456", headers["OpenAI-Project"]) + }) + t.Run("gcp credentials sets Authorization Bearer and rewrites path", func(t *testing.T) { inner, err := newGCPHandler(t.Context(), &filterapi.GCPAuth{ AccessToken: "static-token", diff --git a/internal/backendauth/openai.go b/internal/backendauth/openai.go new file mode 100644 index 0000000000..4ab319cc09 --- /dev/null +++ b/internal/backendauth/openai.go @@ -0,0 +1,54 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package backendauth + +import ( + "context" + "fmt" + "strings" + + "github.com/envoyproxy/ai-gateway/internal/filterapi" + "github.com/envoyproxy/ai-gateway/internal/internalapi" +) + +type openAIHandler struct { + accessToken string + applyFn applyCredentialFn +} + +func newOpenAIHandler(auth *filterapi.OpenAIAuth) (filterapi.BackendAuthHandler, error) { + return &openAIHandler{ + accessToken: strings.TrimSpace(auth.AccessToken), + applyFn: makeOpenAIApplyFn(auth.Organization, auth.Project), + }, nil +} + +// Do implements [Handler.Do]. +// +// Sets the OpenAI access token as an authorization header, along with the organization and project +// headers when configured. +func (o *openAIHandler) Do(_ context.Context, requestHeaders map[string]string, _ []byte) ([]internalapi.Header, error) { + return o.applyFn(requestHeaders, o.accessToken) +} + +// makeOpenAIApplyFn returns an applyCredentialFn that sets Authorization: Bearer and, +// when non-empty, the OpenAI-Organization and OpenAI-Project headers. +func makeOpenAIApplyFn(organization, project string) applyCredentialFn { + return func(requestHeaders map[string]string, credential string) ([]internalapi.Header, error) { + v := fmt.Sprintf("Bearer %s", credential) + requestHeaders["Authorization"] = v + headers := []internalapi.Header{{"Authorization", v}} + if organization != "" { + requestHeaders["OpenAI-Organization"] = organization + headers = append(headers, internalapi.Header{"OpenAI-Organization", organization}) + } + if project != "" { + requestHeaders["OpenAI-Project"] = project + headers = append(headers, internalapi.Header{"OpenAI-Project", project}) + } + return headers, nil + } +} diff --git a/internal/backendauth/openai_test.go b/internal/backendauth/openai_test.go new file mode 100644 index 0000000000..518e9f3979 --- /dev/null +++ b/internal/backendauth/openai_test.go @@ -0,0 +1,65 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package backendauth + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/envoyproxy/ai-gateway/internal/filterapi" + "github.com/envoyproxy/ai-gateway/internal/internalapi" +) + +func TestNewOpenAIHandler(t *testing.T) { + handler, err := newOpenAIHandler(&filterapi.OpenAIAuth{AccessToken: " some-access-token \n"}) + require.NoError(t, err) + require.Equal(t, "some-access-token", handler.(*openAIHandler).accessToken) +} + +func TestOpenAIHandler_Do(t *testing.T) { + for _, tc := range []struct { + name string + auth filterapi.OpenAIAuth + expHeaders []internalapi.Header + }{ + { + name: "token only", + auth: filterapi.OpenAIAuth{AccessToken: "some-access-token"}, + expHeaders: []internalapi.Header{{"Authorization", "Bearer some-access-token"}}, + }, + { + name: "organization and project", + auth: filterapi.OpenAIAuth{AccessToken: "some-access-token", Organization: "org-123", Project: "proj_456"}, + expHeaders: []internalapi.Header{ + {"Authorization", "Bearer some-access-token"}, + {"OpenAI-Organization", "org-123"}, + {"OpenAI-Project", "proj_456"}, + }, + }, + { + name: "project only", + auth: filterapi.OpenAIAuth{AccessToken: "some-access-token", Project: "proj_456"}, + expHeaders: []internalapi.Header{ + {"Authorization", "Bearer some-access-token"}, + {"OpenAI-Project", "proj_456"}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + handler, err := newOpenAIHandler(&tc.auth) + require.NoError(t, err) + + requestHeaders := map[string]string{":method": "POST", ":path": "/v1/chat/completions"} + headers, err := handler.Do(t.Context(), requestHeaders, nil) + require.NoError(t, err) + require.Equal(t, tc.expHeaders, headers) + for _, h := range tc.expHeaders { + require.Equal(t, h[1], requestHeaders[h[0]]) + } + }) + } +} diff --git a/internal/filterapi/filterconfig.go b/internal/filterapi/filterconfig.go index 73d16ec9b1..e7a3aa7ad1 100644 --- a/internal/filterapi/filterconfig.go +++ b/internal/filterapi/filterconfig.go @@ -226,6 +226,8 @@ type BackendAuth struct { AzureAuth *AzureAuth `json:"azure,omitempty"` // GCPAuth specifies the location of GCP credential file. GCPAuth *GCPAuth `json:"gcp,omitempty"` + // OpenAIAuth specifies the OpenAI access token obtained via token exchange. + OpenAIAuth *OpenAIAuth `json:"openai,omitempty"` // CredentialOverride, when non-nil, sources the credential per-request instead of the // static credential above. nil disables per-request sourcing (the default). CredentialOverride *CredentialOverride `json:"credentialOverride,omitempty"` @@ -315,6 +317,25 @@ func (a AzureAuth) LogValue() slog.Value { return slog.GroupValue(slog.String("accessToken", "[REDACTED]")) } +// OpenAIAuth defines the OpenAI access token and the optional organization and project headers. +type OpenAIAuth struct { + // AccessToken is the access token as a literal string. + AccessToken string `json:"accessToken"` + // Organization is sent in the OpenAI-Organization header when set. + Organization string `json:"organization,omitempty"` + // Project is sent in the OpenAI-Project header when set. + Project string `json:"project,omitempty"` +} + +// LogValue implements slog.LogValuer for OpenAIAuth to redact sensitive information. +func (a OpenAIAuth) LogValue() slog.Value { + return slog.GroupValue( + slog.String("accessToken", "[REDACTED]"), + slog.String("organization", a.Organization), + slog.String("project", a.Project), + ) +} + // GCPAuth defines the GCP authentication configuration used to access Google Cloud AI services. type GCPAuth struct { // AccessToken is the access token as a literal string. diff --git a/internal/filterapi/filterconfig_test.go b/internal/filterapi/filterconfig_test.go index d60dd57974..e2627b9b2f 100644 --- a/internal/filterapi/filterconfig_test.go +++ b/internal/filterapi/filterconfig_test.go @@ -126,6 +126,14 @@ func TestAzureAuthLogValue(t *testing.T) { require.Equal(t, "[REDACTED]", attrs["accessToken"]) } +func TestOpenAIAuthLogValue(t *testing.T) { + a := filterapi.OpenAIAuth{AccessToken: "my-access-token", Organization: "org-123", Project: "proj_456"} + attrs := logAttrs(a.LogValue()) + require.Equal(t, "[REDACTED]", attrs["accessToken"]) + require.Equal(t, "org-123", attrs["organization"]) + require.Equal(t, "proj_456", attrs["project"]) +} + func TestGCPAuthLogValue(t *testing.T) { g := filterapi.GCPAuth{AccessToken: "gcp-token", Region: "us-central1", ProjectName: "my-project"} attrs := logAttrs(g.LogValue()) From 63c4e68d457c5725fe288b77aff74e7398e0495f Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 23:31:01 -0400 Subject: [PATCH 08/17] controller: rotate OpenAICredentials access tokens Add an RFC 8693 token exchange provider that wraps a subject token provider (SPIFFE JWT-SVID or OIDC), and an OpenAI token rotator that stores the exchanged access token in the generated BSP secret. Rotation follows the exchanged token's expires_in, falling back to the subject token's expiry when absent. Wire OpenAICredentials into the BSP controller and the gateway's filterapi translation, replacing the "not yet supported" errors, and default the credential override header to x-aigw-openai-access-token. Part of #2791. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 1 + .../controller/backend_security_policy.go | 43 +++- .../backend_security_policy_test.go | 108 +++++++- internal/controller/gateway.go | 14 +- internal/controller/gateway_test.go | 41 ++- .../rotators/openai_token_rotator.go | 143 +++++++++++ .../rotators/openai_token_rotator_test.go | 243 ++++++++++++++++++ .../tokenprovider/token_exchange_provider.go | 136 ++++++++++ .../token_exchange_provider_test.go | 206 +++++++++++++++ ...envoyproxy.io_backendsecuritypolicies.yaml | 1 + site/docs/api/api.mdx | 2 +- 11 files changed, 925 insertions(+), 13 deletions(-) create mode 100644 internal/controller/rotators/openai_token_rotator.go create mode 100644 internal/controller/rotators/openai_token_rotator_test.go create mode 100644 internal/controller/tokenprovider/token_exchange_provider.go create mode 100644 internal/controller/tokenprovider/token_exchange_provider_test.go diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 55b2219592..bbbf9d78e9 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -488,6 +488,7 @@ type CredentialOverrideFromRequestHeaders struct { // AzureAPIKey → x-aigw-azure-api-key // AzureCredentials → x-aigw-azure-access-token // GCPCredentials → x-aigw-gcp-access-token + // OpenAICredentials → x-aigw-openai-access-token // // For AWSCredentials this is a prefix, not a single header name, since SigV4 needs three // inputs. Defaults to "x-aigw-aws-", yielding: diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go index adac8edf50..ad454cc2e6 100644 --- a/internal/controller/backend_security_policy.go +++ b/internal/controller/backend_security_policy.go @@ -277,7 +277,15 @@ func (c *BackendSecurityPolicyController) rotateCredential(ctx context.Context, } case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: - return ctrl.Result{}, fmt.Errorf("backend security type %s is not yet supported", bsp.Spec.Type) + var provider tokenprovider.TokenProvider + provider, err = c.newOpenAITokenProvider(ctx, bsp) + if err != nil { + return ctrl.Result{}, err + } + rotator, err = rotators.NewOpenAITokenRotator(c.client, c.kube, c.logger, bsp.Namespace, bsp.Name, preRotationWindow, provider) + if err != nil { + return ctrl.Result{}, err + } default: err = fmt.Errorf("backend security type %s does not support OIDC token exchange", bsp.Spec.Type) c.logger.Error(err, "unsupported backend security type", "namespace", bsp.Namespace, "name", bsp.Name) @@ -344,6 +352,33 @@ func (c *BackendSecurityPolicyController) executeRotation(ctx context.Context, r return ctrl.Result{RequeueAfter: requeue}, err } +// newOpenAITokenProvider returns a TokenProvider that exchanges the configured subject token for an +// OpenAI access token. +func (c *BackendSecurityPolicyController) newOpenAITokenProvider(ctx context.Context, bsp *aigv1b1.BackendSecurityPolicy) (tokenprovider.TokenProvider, error) { + exchange := &bsp.Spec.OpenAICredentials.TokenExchange + var subjectTokens tokenprovider.TokenProvider + var err error + switch { + case exchange.SubjectToken.SPIFFEJWTSVID != nil: + svid := exchange.SubjectToken.SPIFFEJWTSVID + subjectTokens, err = tokenprovider.NewSPIFFETokenProvider(svid.SocketPath, svid.Audience) + case exchange.SubjectToken.OIDCExchangeToken != nil: + subjectTokens, err = tokenprovider.NewOidcTokenProvider(ctx, c.client, &exchange.SubjectToken.OIDCExchangeToken.OIDC, + bsp.Namespace, c.referenceGrantValidator.validateSecretReference) + default: + return nil, fmt.Errorf("one of spiffeJWTSVID or oidcExchangeToken must be defined, namespace %s name %s", bsp.Namespace, bsp.Name) + } + if err != nil { + return nil, fmt.Errorf("failed to initialize subject token provider: %w", err) + } + return tokenprovider.NewTokenExchangeProvider(tokenprovider.TokenExchangeConfig{ + TokenURL: exchange.TokenURL, + SubjectTokenType: exchange.SubjectTokenType, + Audience: exchange.Audience, + Scopes: exchange.Scopes, + }, subjectTokens) +} + // getBackendSecurityPolicyAuthOIDC returns the backendSecurityPolicy's OIDC pointer or nil. func getBackendSecurityPolicyAuthOIDC(spec *aigv1b1.BackendSecurityPolicySpec) *egv1a1.OIDC { switch spec.Type { @@ -360,6 +395,10 @@ func getBackendSecurityPolicyAuthOIDC(spec *aigv1b1.BackendSecurityPolicySpec) * if spec.GCPCredentials != nil && spec.GCPCredentials.WorkloadIdentityFederationConfig != nil { return &spec.GCPCredentials.WorkloadIdentityFederationConfig.OIDCExchangeToken.OIDC } + case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: + if spec.OpenAICredentials != nil && spec.OpenAICredentials.TokenExchange.SubjectToken.OIDCExchangeToken != nil { + return &spec.OpenAICredentials.TokenExchange.SubjectToken.OIDCExchangeToken.OIDC + } } return nil } @@ -474,7 +513,7 @@ func getBSPGeneratedSecretName(bsp *aigv1b1.BackendSecurityPolicy) string { aigv1b1.BackendSecurityPolicyTypeAnthropicAPIKey: return "" // APIKey does not require rotation. case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: - return "" // Not yet supported, so no secret is generated. + // The exchanged access token is always stored in a generated secret. default: panic("BUG: unsupported backend security policy type: " + string(bsp.Spec.Type)) } diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index b708b5d44b..53496b5745 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -10,6 +10,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" + "path/filepath" "testing" "time" @@ -864,7 +866,80 @@ func TestNewBackendSecurityPolicyController_RotateCredentialInvalidType(t *testi require.Equal(t, time.Duration(0), res.RequeueAfter) } -func TestBackendSecurityPolicyController_Reconcile_OpenAICredentialsNotYetSupported(t *testing.T) { +func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC(t *testing.T) { + var gotForm url.Values + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, _ *http.Request) { + _, _ = fmt.Fprintf(w, `{"issuer": %q, "token_endpoint": %q, "authorization_endpoint": "authorization_endpoint", "jwks_uri": "jwks_uri", "scopes_supported": []}`, + server.URL, server.URL+"/oidc/token") + }) + mux.HandleFunc("/oidc/token", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token": "subject-token", "token_type": "Bearer", "expires_in": 3600}`)) + }) + mux.HandleFunc("/exchange", func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + gotForm = r.PostForm + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token": "openai-access-token", "token_type": "Bearer", "expires_in": 3600}`)) + }) + + eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() + cl := fake.NewClientBuilder().WithScheme(Scheme).Build() + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) + const bspNamespace = "default" + require.NoError(t, cl.Create(t.Context(), &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "oidc-client-secret", Namespace: bspNamespace}, + Data: map[string][]byte{"client-secret": []byte("client-secret")}, + })) + + bsp := &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "openai-oidc", Namespace: bspNamespace}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ + TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ + TokenURL: server.URL + "/exchange", + SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", + Audience: "https://api.openai.com", + Scopes: []string{"api.model.read"}, + SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + OIDCExchangeToken: &aigv1b1.BackendSecurityPolicyOIDC{ + OIDC: egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{Issuer: server.URL}, + ClientID: ptr.To("some-client-id"), + ClientSecret: gwapiv1.SecretObjectReference{ + Name: "oidc-client-secret", + Namespace: ptr.To(gwapiv1.Namespace(bspNamespace)), + }, + }, + }, + }, + }, + }, + }, + } + require.NoError(t, cl.Create(t.Context(), bsp)) + + res, err := c.rotateCredential(t.Context(), bsp) + require.NoError(t, err) + require.Greater(t, res.RequeueAfter, 50*time.Minute) + + require.Equal(t, "subject-token", gotForm.Get("subject_token")) + require.Equal(t, "urn:ietf:params:oauth:token-type:access_token", gotForm.Get("subject_token_type")) + require.Equal(t, "https://api.openai.com", gotForm.Get("audience")) + require.Equal(t, "api.model.read", gotForm.Get("scope")) + + secret, err := rotators.LookupSecret(t.Context(), cl, bspNamespace, rotators.GetBSPSecretName("openai-oidc")) + require.NoError(t, err) + require.Equal(t, "openai-access-token", string(secret.Data[rotators.OpenAIAccessTokenKey])) + ok, _ := ctrlutil.HasOwnerReference(secret.OwnerReferences, bsp, c.client.Scheme()) + require.True(t, ok, "expected secret to have owner reference to BackendSecurityPolicy") +} + +func TestBackendSecurityPolicyController_Reconcile_OpenAICredentials_SPIFFEUnavailable(t *testing.T) { eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() cl := requireNewFakeClientWithIndexes(t) c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) @@ -875,9 +950,13 @@ func TestBackendSecurityPolicyController_Reconcile_OpenAICredentialsNotYetSuppor Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ - TokenURL: "https://auth.example.com/oauth/token", + TokenURL: "https://auth.example.com/oauth/token", + SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ - SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{Audience: "https://auth.example.com"}, + SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{ + Audience: "https://auth.example.com", + SocketPath: "unix://" + filepath.Join(t.TempDir(), "missing.sock"), + }, }, }, }, @@ -885,14 +964,29 @@ func TestBackendSecurityPolicyController_Reconcile_OpenAICredentialsNotYetSuppor } require.NoError(t, cl.Create(t.Context(), bsp)) - _, err := c.Reconcile(t.Context(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "openai-bsp"}}) - require.EqualError(t, err, "backend security type OpenAICredentials is not yet supported") + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + _, err := c.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "openai-bsp"}}) + require.ErrorContains(t, err, "failed to fetch JWT-SVID from SPIFFE Workload API") var updated aigv1b1.BackendSecurityPolicy require.NoError(t, cl.Get(t.Context(), types.NamespacedName{Namespace: "default", Name: "openai-bsp"}, &updated)) require.Len(t, updated.Status.Conditions, 1) require.Equal(t, aigv1b1.ConditionTypeNotAccepted, updated.Status.Conditions[0].Type) - require.Equal(t, "backend security type OpenAICredentials is not yet supported", updated.Status.Conditions[0].Message) +} + +func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_NoSubjectToken(t *testing.T) { + cl := fake.NewClientBuilder().WithScheme(Scheme).Build() + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, nil, nil) + bsp := &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: "default"}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{}, + }, + } + _, err := c.rotateCredential(t.Context(), bsp) + require.EqualError(t, err, "one of spiffeJWTSVID or oidcExchangeToken must be defined, namespace default name openai-bsp") } func TestNewBackendSecurityPolicyController_RotateCredentialAwsCredentialFile(t *testing.T) { @@ -1618,7 +1712,7 @@ func TestGetBSPGeneratedSecretName(t *testing.T) { Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, }, }, - expectedName: "", + expectedName: "ai-eg-bsp-openai-bsp", }, } diff --git a/internal/controller/gateway.go b/internal/controller/gateway.go index c056112cff..91db2732bf 100644 --- a/internal/controller/gateway.go +++ b/internal/controller/gateway.go @@ -843,6 +843,8 @@ func defaultOverrideHeaderName(t aigv1b1.BackendSecurityPolicyType) string { return "x-aigw-azure-access-token" case aigv1b1.BackendSecurityPolicyTypeGCPCredentials: return "x-aigw-gcp-access-token" + case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: + return "x-aigw-openai-access-token" case aigv1b1.BackendSecurityPolicyTypeAWSCredentials: return internalapi.AWSCredentialOverrideHeaderPrefix default: @@ -1007,7 +1009,17 @@ func (c *GatewayController) bspToFilterAPIBackendAuth(ctx context.Context, backe hasStaticCred = true } case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: - return nil, fmt.Errorf("backend security type %s is not yet supported for policy %s", spec.Type, backendSecurityPolicy.Name) + secretName := rotators.GetBSPSecretName(backendSecurityPolicy.Name) + openAIAccessToken, getErr := c.getSecretData(ctx, namespace, secretName, rotators.OpenAIAccessTokenKey) + if getErr != nil { + return nil, getErr + } + auth = &filterapi.BackendAuth{OpenAIAuth: &filterapi.OpenAIAuth{ + AccessToken: openAIAccessToken, + Organization: spec.OpenAICredentials.Organization, + Project: spec.OpenAICredentials.Project, + }} + hasStaticCred = true default: return nil, fmt.Errorf("invalid backend security type %s for policy %s", spec.Type, backendSecurityPolicy.Name) } diff --git a/internal/controller/gateway_test.go b/internal/controller/gateway_test.go index 21ea25f43c..1b31913c99 100644 --- a/internal/controller/gateway_test.go +++ b/internal/controller/gateway_test.go @@ -1311,6 +1311,16 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) { }, }, }, + { + ObjectMeta: metav1.ObjectMeta{Name: "openai-credentials", Namespace: namespace}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ + Organization: "org-123", + Project: "proj_456", + }, + }, + }, } { require.NoError(t, fakeClient.Create(t.Context(), bsp)) } @@ -1339,6 +1349,10 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: rotators.GetBSPSecretName("gcp-wif"), Namespace: namespace}, StringData: map[string]string{rotators.GCPAccessTokenKey: "thisisgcpcredentials"}, }, + { + ObjectMeta: metav1.ObjectMeta{Name: rotators.GetBSPSecretName("openai-credentials"), Namespace: namespace}, + StringData: map[string]string{rotators.OpenAIAccessTokenKey: "thisisopenaicredentials"}, + }, } { _, err := kube.CoreV1().Secrets(namespace).Create(t.Context(), s, metav1.CreateOptions{}) require.NoError(t, err) @@ -1402,6 +1416,16 @@ func TestGatewayController_bspToFilterAPIBackendAuth(t *testing.T) { AnthropicAPIKey: &filterapi.AnthropicAPIKeyAuth{Key: "thisisapikey"}, }, }, + { + bspName: "openai-credentials", + exp: &filterapi.BackendAuth{ + OpenAIAuth: &filterapi.OpenAIAuth{ + AccessToken: "thisisopenaicredentials", + Organization: "org-123", + Project: "proj_456", + }, + }, + }, } { t.Run(tc.bspName, func(t *testing.T) { bsp := &aigv1b1.BackendSecurityPolicy{} @@ -1501,7 +1525,7 @@ func TestGatewayController_bspToFilterAPIBackendAuth_ErrorCases(t *testing.T) { expectedError: "failed to get secret test-namespace/missing-aws-secret", }, { - name: "openai credentials not yet supported", + name: "openai credentials with missing generated secret", bspName: "openai-bsp", bsp: &aigv1b1.BackendSecurityPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: namespace}, @@ -1510,7 +1534,7 @@ func TestGatewayController_bspToFilterAPIBackendAuth_ErrorCases(t *testing.T) { OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{}, }, }, - expectedError: "backend security type OpenAICredentials is not yet supported for policy openai-bsp", + expectedError: "failed to get secret test-namespace/ai-eg-bsp-openai-bsp", }, } @@ -1739,6 +1763,19 @@ func TestResolveCredentialOverride(t *testing.T) { require.Equal(t, "x-aigw-gcp-access-token", result.DynamicMetadataKey) }) + t.Run("fromRequestHeaders default header for OpenAICredentials", func(t *testing.T) { + result, err := resolveCredentialOverride( + aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + &aigv1b1.BackendSecurityPolicyCredentialOverride{ + FromRequestHeaders: &aigv1b1.CredentialOverrideFromRequestHeaders{}, + }, + true, + ) + require.NoError(t, err) + require.Equal(t, "x-aigw-openai-access-token", result.HeaderName) + require.Equal(t, []string{"x-aigw-openai-access-token"}, result.InputHeadersToRemove) + }) + t.Run("fromRequestHeaders for AWSCredentials derives three headers from a prefix", func(t *testing.T) { result, err := resolveCredentialOverride( aigv1b1.BackendSecurityPolicyTypeAWSCredentials, diff --git a/internal/controller/rotators/openai_token_rotator.go b/internal/controller/rotators/openai_token_rotator.go new file mode 100644 index 0000000000..7eac7c6695 --- /dev/null +++ b/internal/controller/rotators/openai_token_rotator.go @@ -0,0 +1,143 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package rotators + +import ( + "context" + "time" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/envoyproxy/ai-gateway/internal/controller/tokenprovider" +) + +const ( + // OpenAIAccessTokenKey is the key used to store OpenAI access token in Kubernetes secrets. + OpenAIAccessTokenKey = "openAIAccessToken" +) + +// openAITokenRotator implements Rotator interface for OpenAI access token exchange. +type openAITokenRotator struct { + // client is used for Kubernetes API operations. + client client.Client + // kube provides additional API capabilities. + kube kubernetes.Interface + // logger is used for structured logging. + logger logr.Logger + // backendSecurityPolicyName provides name of backend security policy. + backendSecurityPolicyName string + // backendSecurityPolicyNamespace provides namespace of backend security policy. + backendSecurityPolicyNamespace string + // preRotationWindow specifies how long before expiry to rotate. + preRotationWindow time.Duration + // tokenProvider specifies provider to fetch OpenAI access token. + tokenProvider tokenprovider.TokenProvider +} + +// NewOpenAITokenRotator creates a new openAITokenRotator with the given parameters. +func NewOpenAITokenRotator( + client client.Client, + kube kubernetes.Interface, + logger logr.Logger, + backendSecurityPolicyNamespace string, + backendSecurityPolicyName string, + preRotationWindow time.Duration, + tokenProvider tokenprovider.TokenProvider, +) (Rotator, error) { + return &openAITokenRotator{ + client: client, + kube: kube, + logger: logger.WithName("openai-token-rotator"), + backendSecurityPolicyNamespace: backendSecurityPolicyNamespace, + backendSecurityPolicyName: backendSecurityPolicyName, + preRotationWindow: preRotationWindow, + tokenProvider: tokenProvider, + }, nil +} + +// IsExpired implements Rotator.IsExpired method to check if the preRotation time is before the current time. +func (r *openAITokenRotator) IsExpired(preRotationExpirationTime time.Time) bool { + return IsBufferedTimeExpired(0, preRotationExpirationTime) +} + +// GetPreRotationTime implements Rotator.GetPreRotationTime method to retrieve the pre-rotation time for OpenAI token. +func (r *openAITokenRotator) GetPreRotationTime(ctx context.Context) (time.Time, error) { + secret, err := LookupSecret(ctx, r.client, r.backendSecurityPolicyNamespace, GetBSPSecretName(r.backendSecurityPolicyName)) + if err != nil { + if apierrors.IsNotFound(err) { + return time.Time{}, nil + } + return time.Time{}, err + } + expirationTime, err := GetExpirationSecretAnnotation(secret) + if err != nil { + return time.Time{}, err + } + preRotationTime := expirationTime.Add(-r.preRotationWindow) + return preRotationTime, nil +} + +// Rotate implements Rotator.Rotate method to rotate OpenAI access token and updates the Kubernetes secret. +func (r *openAITokenRotator) Rotate(ctx context.Context) (time.Time, error) { + bspNamespace := r.backendSecurityPolicyNamespace + bspName := r.backendSecurityPolicyName + secretName := GetBSPSecretName(bspName) + + r.logger.Info("start rotating OpenAI access token", "namespace", bspNamespace, "name", bspName) + + openAIToken, err := r.tokenProvider.GetToken(ctx) + if err != nil { + r.logger.Error(err, "failed to get access token via token exchange") + return time.Time{}, err + } + secret, err := LookupSecret(ctx, r.client, bspNamespace, secretName) + if err != nil { + if apierrors.IsNotFound(err) { + r.logger.Info("creating a new OpenAI access token into secret", "namespace", bspNamespace, "name", bspName) + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: bspNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: make(map[string][]byte), + } + populateOpenAIAccessToken(secret, &openAIToken) + err = r.client.Create(ctx, secret) + if err != nil { + r.logger.Error(err, "failed to create OpenAI access token", "namespace", bspNamespace, "name", bspName) + return time.Time{}, err + } + return openAIToken.ExpiresAt, nil + } + r.logger.Error(err, "failed to lookup OpenAI access token secret", "namespace", bspNamespace, "name", bspName) + return time.Time{}, err + } + r.logger.Info("updating OpenAI access token secret", "namespace", bspNamespace, "name", bspName) + + populateOpenAIAccessToken(secret, &openAIToken) + err = r.client.Update(ctx, secret) + if err != nil { + r.logger.Error(err, "failed to update OpenAI access token", "namespace", bspNamespace, "name", bspName) + return time.Time{}, err + } + return openAIToken.ExpiresAt, nil +} + +// populateOpenAIAccessToken updates the secret with the OpenAI access token. +func populateOpenAIAccessToken(secret *corev1.Secret, token *tokenprovider.TokenExpiry) { + updateExpirationSecretAnnotation(secret, token.ExpiresAt) + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + secret.Data[OpenAIAccessTokenKey] = []byte(token.Token) +} diff --git a/internal/controller/rotators/openai_token_rotator_test.go b/internal/controller/rotators/openai_token_rotator_test.go new file mode 100644 index 0000000000..ec259923da --- /dev/null +++ b/internal/controller/rotators/openai_token_rotator_test.go @@ -0,0 +1,243 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package rotators + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/envoyproxy/ai-gateway/internal/controller/tokenprovider" +) + +func TestOpenAITokenRotator_Rotate(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Secret{}) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + t.Run("failed to get openai token", func(t *testing.T) { + now := time.Now() + oneHourBeforeNow := now.Add(-1 * time.Hour) + twoHourAfterNow := now.Add(2 * time.Hour) + mockProvider := tokenprovider.NewMockTokenProvider("fake-token", twoHourAfterNow, fmt.Errorf("failed to get OpenAI access token")) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetBSPSecretName("test-policy"), + Namespace: "default", + Annotations: map[string]string{ + ExpirationTimeAnnotationKey: oneHourBeforeNow.Format(time.RFC3339), + }, + }, + Data: map[string][]byte{ + OpenAIAccessTokenKey: []byte("some-openai-access-token"), + }, + } + err := client.Create(context.Background(), secret) + require.NoError(t, err) + + rotator := &openAITokenRotator{ + client: client, + backendSecurityPolicyNamespace: "default", + backendSecurityPolicyName: "test-policy", + preRotationWindow: 5 * time.Minute, + tokenProvider: mockProvider, + } + + _, err = rotator.Rotate(context.Background()) + require.Error(t, err) + err = client.Delete(context.Background(), secret) + require.NoError(t, err) + }) + + t.Run("secret does not exist", func(t *testing.T) { + now := time.Now() + twoHourAfterNow := now.Add(2 * time.Hour) + mockProvider := tokenprovider.NewMockTokenProvider("fake-token", twoHourAfterNow, nil) + + rotator := &openAITokenRotator{ + client: client, + + backendSecurityPolicyNamespace: "default", + backendSecurityPolicyName: "test-policy", + preRotationWindow: 5 * time.Minute, + tokenProvider: mockProvider, + } + expiration, err := rotator.Rotate(context.Background()) + require.NoError(t, err) + secret, err := LookupSecret(context.Background(), client, "default", GetBSPSecretName("test-policy")) + require.NoError(t, err) + err = client.Delete(context.Background(), secret) + require.NoError(t, err) + require.Equal(t, twoHourAfterNow, expiration) + }) + + t.Run("secret exist", func(t *testing.T) { + now := time.Now() + twoHourAfterNow := now.Add(2 * time.Hour) + oneHourBeforeNow := now.Add(-1 * time.Hour) + mockProvider := tokenprovider.NewMockTokenProvider("fake-token", twoHourAfterNow, nil) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetBSPSecretName("test-policy"), + Namespace: "default", + Annotations: map[string]string{ + ExpirationTimeAnnotationKey: oneHourBeforeNow.Format(time.RFC3339), + }, + }, + Data: map[string][]byte{ + OpenAIAccessTokenKey: []byte("some-openai-access-token"), + }, + } + err := client.Create(context.Background(), secret) + require.NoError(t, err) + + rotator := &openAITokenRotator{ + client: client, + tokenProvider: mockProvider, + backendSecurityPolicyNamespace: "default", + backendSecurityPolicyName: "test-policy", + preRotationWindow: 5 * time.Minute, + } + + expiration, err := rotator.Rotate(context.Background()) + require.NoError(t, err) + require.Equal(t, twoHourAfterNow, expiration) + + err = client.Delete(context.Background(), secret) + require.NoError(t, err) + }) +} + +func TestOpenAITokenRotator_GetPreRotationTime(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Secret{}) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + rotator := &openAITokenRotator{ + client: client, + preRotationWindow: 5 * time.Minute, + backendSecurityPolicyNamespace: "default", + backendSecurityPolicyName: "test-policy", + } + + now := time.Now() + + tests := []struct { + name string + secret *corev1.Secret + expectedTime time.Time + expectedError bool + }{ + { + name: "secret annotation missing", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetBSPSecretName("test-policy"), + Namespace: "default", + }, + Data: map[string][]byte{ + OpenAIAccessTokenKey: []byte("some-openai-access-token"), + }, + }, + expectedTime: time.Time{}, + expectedError: true, + }, + { + name: "rotation time before expiration time", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetBSPSecretName("test-policy"), + Namespace: "default", + Annotations: map[string]string{ + ExpirationTimeAnnotationKey: now.Add(2 * time.Hour).Format(time.RFC3339), + }, + }, + Data: map[string][]byte{ + OpenAIAccessTokenKey: []byte("some-openai-access-token"), + }, + }, + expectedTime: now.Add(2 * time.Hour), + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := client.Create(context.Background(), tt.secret) + require.NoError(t, err) + + got, err := rotator.GetPreRotationTime(context.Background()) + if (err != nil) != tt.expectedError { + t.Errorf("OpenAITokenRotator.GetPreRotationTime() error = %v, expectedError %v", err, tt.expectedError) + return + } + if !tt.expectedTime.IsZero() && got.Compare(tt.expectedTime) >= 0 { + t.Errorf("OpenAITokenRotator.GetPreRotationTime() = %v, expected %v", got, tt.expectedTime) + } + err = client.Delete(context.Background(), tt.secret) + require.NoError(t, err) + }) + } +} + +func TestOpenAITokenRotator_IsExpired(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Secret{}) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + rotator := &openAITokenRotator{ + client: client, + } + tests := []struct { + name string + expiration time.Time + expect bool + }{ + { + name: "not expired", + expiration: time.Now().Add(1 * time.Hour), + expect: false, + }, + { + name: "expired", + expiration: time.Now().Add(-1 * time.Hour), + expect: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := rotator.IsExpired(tt.expiration); got != tt.expect { + t.Errorf("OpenAITokenRotator.IsExpired() = %v, expect %v", got, tt.expect) + } + }) + } +} + +func TestPopulateOpenAIAccessToken(t *testing.T) { + secret := &corev1.Secret{} + expiration := time.Now() + + openAIToken := tokenprovider.TokenExpiry{Token: "some-openai-token", ExpiresAt: expiration} + populateOpenAIAccessToken(secret, &openAIToken) + + annotation, ok := secret.Annotations[ExpirationTimeAnnotationKey] + require.True(t, ok) + require.Equal(t, expiration.Format(time.RFC3339), annotation) + + require.Len(t, secret.Data, 1) + val, ok := secret.Data[OpenAIAccessTokenKey] + require.True(t, ok) + require.Equal(t, "some-openai-token", string(val)) +} diff --git a/internal/controller/tokenprovider/token_exchange_provider.go b/internal/controller/tokenprovider/token_exchange_provider.go new file mode 100644 index 0000000000..3888e05dc7 --- /dev/null +++ b/internal/controller/tokenprovider/token_exchange_provider.go @@ -0,0 +1,136 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package tokenprovider + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/envoyproxy/ai-gateway/internal/json" +) + +// tokenExchangeGrantType is the RFC 8693 grant type for token exchange requests. +const tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" // #nosec G101 + +// TokenExchangeConfig holds the parameters of an RFC 8693 token exchange request. +type TokenExchangeConfig struct { + // TokenURL is the token exchange endpoint. + TokenURL string + // SubjectTokenType is the subject_token_type parameter. + SubjectTokenType string + // Audience is the optional audience parameter. + Audience string + // Scopes are joined with spaces into the optional scope parameter. + Scopes []string +} + +// tokenExchangeProvider is a provider implements TokenProvider interface for OAuth 2.0 Token Exchange. +type tokenExchangeProvider struct { + config TokenExchangeConfig + subjectTokens TokenProvider + httpClient *http.Client +} + +// tokenExchangeResponse is the successful response of RFC 8693 section 2.2.1. +type tokenExchangeResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` +} + +// tokenExchangeErrorResponse is the error response of RFC 6749 section 5.2. +type tokenExchangeErrorResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +// NewTokenExchangeProvider creates a new TokenProvider that obtains a subject token from subjectTokens +// and exchanges it for an access token at config.TokenURL, following RFC 8693. +// +// https://datatracker.ietf.org/doc/html/rfc8693 +func NewTokenExchangeProvider(config TokenExchangeConfig, subjectTokens TokenProvider) (TokenProvider, error) { + if config.TokenURL == "" { + return nil, errors.New("token exchange URL must not be empty") + } + if config.SubjectTokenType == "" { + return nil, errors.New("token exchange subject token type must not be empty") + } + if subjectTokens == nil { + return nil, errors.New("token exchange subject token provider must not be nil") + } + return &tokenExchangeProvider{ + config: config, + subjectTokens: subjectTokens, + httpClient: &http.Client{Timeout: time.Minute}, + }, nil +} + +// GetToken implements TokenProvider.GetToken method to retrieve an exchanged access token and its expiration time. +// +// When the response has no expires_in, the access token is assumed to expire with the subject token. +func (t *tokenExchangeProvider) GetToken(ctx context.Context) (TokenExpiry, error) { + subject, err := t.subjectTokens.GetToken(ctx) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to get subject token: %w", err) + } + + form := url.Values{ + "grant_type": {tokenExchangeGrantType}, + "subject_token": {subject.Token}, + "subject_token_type": {t.config.SubjectTokenType}, + } + if t.config.Audience != "" { + form.Set("audience", t.config.Audience) + } + if len(t.config.Scopes) > 0 { + form.Set("scope", strings.Join(t.config.Scopes, " ")) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.config.TokenURL, strings.NewReader(form.Encode())) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to create token exchange request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + issuedAt := time.Now() + resp, err := t.httpClient.Do(req) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to send token exchange request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to read token exchange response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + var errResp tokenExchangeErrorResponse + if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" { + return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d: %s: %s", + resp.StatusCode, errResp.Error, errResp.ErrorDescription) + } + return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d", resp.StatusCode) + } + + var tokenResp tokenExchangeResponse + if err = json.Unmarshal(body, &tokenResp); err != nil { + return TokenExpiry{}, fmt.Errorf("failed to decode token exchange response: %w", err) + } + if tokenResp.AccessToken == "" { + return TokenExpiry{}, errors.New("token exchange response has no access_token") + } + expiresAt := subject.ExpiresAt + if tokenResp.ExpiresIn > 0 { + expiresAt = issuedAt.Add(time.Duration(tokenResp.ExpiresIn) * time.Second) + } + return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil +} diff --git a/internal/controller/tokenprovider/token_exchange_provider_test.go b/internal/controller/tokenprovider/token_exchange_provider_test.go new file mode 100644 index 0000000000..77a8111ee2 --- /dev/null +++ b/internal/controller/tokenprovider/token_exchange_provider_test.go @@ -0,0 +1,206 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package tokenprovider + +import ( + "errors" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const testJWTTokenType = "urn:ietf:params:oauth:token-type:jwt" + +func TestNewTokenExchangeProvider(t *testing.T) { + subject := NewMockTokenProvider("subject", time.Now(), nil) + for _, tc := range []struct { + name string + config TokenExchangeConfig + subject TokenProvider + expErr string + }{ + { + name: "valid", + config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token", SubjectTokenType: testJWTTokenType}, + subject: subject, + }, + { + name: "empty token URL", + config: TokenExchangeConfig{SubjectTokenType: testJWTTokenType}, + subject: subject, + expErr: "token exchange URL must not be empty", + }, + { + name: "empty subject token type", + config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token"}, + subject: subject, + expErr: "token exchange subject token type must not be empty", + }, + { + name: "nil subject token provider", + config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token", SubjectTokenType: testJWTTokenType}, + expErr: "token exchange subject token provider must not be nil", + }, + } { + t.Run(tc.name, func(t *testing.T) { + provider, err := NewTokenExchangeProvider(tc.config, tc.subject) + if tc.expErr != "" { + require.EqualError(t, err, tc.expErr) + require.Nil(t, provider) + return + } + require.NoError(t, err) + require.NotNil(t, provider) + }) + } +} + +func TestTokenExchangeProvider_GetToken(t *testing.T) { + subjectExpiry := time.Now().Add(time.Hour).Truncate(time.Second) + + t.Run("success", func(t *testing.T) { + var gotForm url.Values + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.NoError(t, r.ParseForm()) + gotForm = r.PostForm + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token":"exchanged","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":600}`)) + })) + defer server.Close() + + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{ + TokenURL: server.URL, + SubjectTokenType: testJWTTokenType, + Audience: "https://api.openai.com", + Scopes: []string{"a", "b"}, + }, NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + + before := time.Now() + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, "exchanged", token.Token) + require.WithinRange(t, token.ExpiresAt, before.Add(600*time.Second), time.Now().Add(600*time.Second)) + require.Equal(t, url.Values{ + "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, + "subject_token": {"subject"}, + "subject_token_type": {testJWTTokenType}, + "audience": {"https://api.openai.com"}, + "scope": {"a b"}, + }, gotForm) + }) + + t.Run("optional parameters omitted", func(t *testing.T) { + var gotForm url.Values + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + gotForm = r.PostForm + _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) + })) + defer server.Close() + + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.NoError(t, err) + require.NotContains(t, gotForm, "audience") + require.NotContains(t, gotForm, "scope") + }) + + t.Run("no expires_in uses subject token expiry", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"access_token":"exchanged"}`)) + })) + defer server.Close() + + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, TokenExpiry{Token: "exchanged", ExpiresAt: subjectExpiry}, token) + }) + + t.Run("subject token error", func(t *testing.T) { + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: "http://127.0.0.1:0", SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("", time.Time{}, errors.New("workload API unavailable"))) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.EqualError(t, err, "failed to get subject token: workload API unavailable") + }) + + for _, tc := range []struct { + name string + status int + body string + expErr string + }{ + { + name: "oauth error response", + status: http.StatusBadRequest, + body: `{"error":"invalid_grant","error_description":"subject token expired"}`, + expErr: "token exchange failed with status 400: invalid_grant: subject token expired", + }, + { + name: "non-json error response", + status: http.StatusBadGateway, + body: `bad gateway`, + expErr: "token exchange failed with status 502", + }, + { + name: "invalid json", + status: http.StatusOK, + body: `{`, + expErr: "failed to decode token exchange response", + }, + { + name: "missing access token", + status: http.StatusOK, + body: `{"expires_in":600}`, + expErr: "token exchange response has no access_token", + }, + } { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(tc.status) + _, _ = w.Write([]byte(tc.body)) + })) + defer server.Close() + + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.ErrorContains(t, err, tc.expErr) + }) + } + + t.Run("request error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + server.Close() + + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.ErrorContains(t, err, "failed to send token exchange request") + }) + + t.Run("invalid token URL", func(t *testing.T) { + provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: "://bad", SubjectTokenType: testJWTTokenType}, + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.ErrorContains(t, err, "failed to create token exchange request") + }) +} diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 8af10593aa..6520bd0b26 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -10733,6 +10733,7 @@ spec: AzureAPIKey → x-aigw-azure-api-key AzureCredentials → x-aigw-azure-access-token GCPCredentials → x-aigw-gcp-access-token + OpenAICredentials → x-aigw-openai-access-token For AWSCredentials this is a prefix, not a single header name, since SigV4 needs three inputs. Defaults to "x-aigw-aws-", yielding: diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 9fb8a30d1e..7a1040e9a3 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -4235,7 +4235,7 @@ CredentialOverrideFromRequestHeaders sources the per-request credential from a r name="header" type="string" required="false" - description="Header is the name of the request header that carries the credential.
Defaults to the x-aigw-* header for the configured auth type:
APIKey → x-aigw-api-key
AnthropicAPIKey → x-aigw-anthropic-api-key
AzureAPIKey → x-aigw-azure-api-key
AzureCredentials → x-aigw-azure-access-token
GCPCredentials → x-aigw-gcp-access-token
For AWSCredentials this is a prefix, not a single header name, since SigV4 needs three
inputs. Defaults to `x-aigw-aws-`, yielding:
x-aigw-aws-access-key-id
x-aigw-aws-secret-access-key
x-aigw-aws-session-token (optional; omit for long-lived credentials)
All three are stripped before the backend sees them. An access key ID without a secret
access key, or the reverse, fails the request instead of falling back.
Prefer FromDynamicMetadata for AWSCredentials. Headers are client-settable, and a secret
access key on the wire is a worse leak than an API key." + description="Header is the name of the request header that carries the credential.
Defaults to the x-aigw-* header for the configured auth type:
APIKey → x-aigw-api-key
AnthropicAPIKey → x-aigw-anthropic-api-key
AzureAPIKey → x-aigw-azure-api-key
AzureCredentials → x-aigw-azure-access-token
GCPCredentials → x-aigw-gcp-access-token
OpenAICredentials → x-aigw-openai-access-token
For AWSCredentials this is a prefix, not a single header name, since SigV4 needs three
inputs. Defaults to `x-aigw-aws-`, yielding:
x-aigw-aws-access-key-id
x-aigw-aws-secret-access-key
x-aigw-aws-session-token (optional; omit for long-lived credentials)
All three are stripped before the backend sees them. An access key ID without a secret
access key, or the reverse, fails the request instead of falling back.
Prefer FromDynamicMetadata for AWSCredentials. Headers are client-settable, and a secret
access key on the wire is a worse leak than an API key." /> Date: Wed, 7 Oct 2026 23:38:55 -0400 Subject: [PATCH 09/17] test: cover OpenAICredentials with SPIFFE in envtest Run the BackendSecurityPolicy controller against a fake SPIFFE Workload API and a fake token exchange endpoint, and check that the JWT-SVID is exchanged and the access token lands in the generated secret. Part of #2791. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- tests/controller/openai_credentials_test.go | 173 ++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 tests/controller/openai_credentials_test.go diff --git a/tests/controller/openai_credentials_test.go b/tests/controller/openai_credentials_test.go new file mode 100644 index 0000000000..779af7afc8 --- /dev/null +++ b/tests/controller/openai_credentials_test.go @@ -0,0 +1,173 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package controller + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/spiffe/go-spiffe/v2/proto/spiffe/workload" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/config" + gwaiev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" + + aigv1b1 "github.com/envoyproxy/ai-gateway/api/v1beta1" + "github.com/envoyproxy/ai-gateway/internal/controller" + "github.com/envoyproxy/ai-gateway/internal/controller/rotators" + internaltesting "github.com/envoyproxy/ai-gateway/internal/testing" + testsinternal "github.com/envoyproxy/ai-gateway/tests/internal" +) + +const testControllerSPIFFEID = "spiffe://example.org/ns/envoy-ai-gateway-system/sa/ai-gateway-controller" + +// TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE runs the BackendSecurityPolicy controller +// against a fake SPIFFE Workload API and a fake token exchange endpoint, and checks that the exchanged +// access token ends up in the generated secret. +func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { + c, cfg, k := testsinternal.NewEnvTest(t) + + workloadAPI := &fakeWorkloadAPI{t: t} + socketAddr := startFakeWorkloadAPI(t, workloadAPI) + + var ( + mu sync.Mutex + gotSubjectToken string + gotSubjectTokType string + ) + exchangeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + mu.Lock() + gotSubjectToken = r.PostForm.Get("subject_token") + gotSubjectTokType = r.PostForm.Get("subject_token_type") + mu.Unlock() + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"access_token":"openai-access-token","token_type":"Bearer","expires_in":3600}`)) + })) + t.Cleanup(exchangeServer.Close) + + eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() + eventChPool := internaltesting.NewControllerEventChan[*gwaiev1.InferencePool]() + opt := ctrl.Options{Scheme: c.Scheme(), LeaderElection: false, Controller: config.Controller{SkipNameValidation: ptr.To(true)}} + mgr, err := ctrl.NewManager(cfg, opt) + require.NoError(t, err) + require.NoError(t, controller.ApplyIndexing(t.Context(), mgr.GetFieldIndexer().IndexField)) + pc := controller.NewBackendSecurityPolicyController(mgr.GetClient(), k, defaultLogger(), eventCh.Ch, eventChPool.Ch) + require.NoError(t, controller.TypedControllerBuilderForCRD(mgr, &aigv1b1.BackendSecurityPolicy{}).Complete(pc)) + go func() { require.NoError(t, mgr.Start(t.Context())) }() + require.True(t, mgr.GetCache().WaitForCacheSync(t.Context())) + + const bspName, bspNamespace = "openai-spiffe", "default" + require.NoError(t, c.Create(t.Context(), &aigv1b1.BackendSecurityPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: bspName, Namespace: bspNamespace}, + Spec: aigv1b1.BackendSecurityPolicySpec{ + Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, + OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ + Organization: "org-123", + TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ + TokenURL: exchangeServer.URL, + SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{ + Audience: "https://auth.example.com", + SocketPath: socketAddr, + }, + }, + }, + }, + }, + })) + + var secret corev1.Secret + require.Eventually(t, func() bool { + return c.Get(t.Context(), client.ObjectKey{Name: rotators.GetBSPSecretName(bspName), Namespace: bspNamespace}, &secret) == nil + }, 30*time.Second, 200*time.Millisecond) + require.Equal(t, "openai-access-token", string(secret.Data[rotators.OpenAIAccessTokenKey])) + expiresAt, err := rotators.GetExpirationSecretAnnotation(&secret) + require.NoError(t, err) + require.WithinDuration(t, time.Now().Add(time.Hour), expiresAt, time.Minute) + + require.Eventually(t, func() bool { + var bsp aigv1b1.BackendSecurityPolicy + require.NoError(t, c.Get(t.Context(), client.ObjectKey{Name: bspName, Namespace: bspNamespace}, &bsp)) + return len(bsp.Status.Conditions) == 1 && bsp.Status.Conditions[0].Type == aigv1b1.ConditionTypeAccepted + }, 30*time.Second, 200*time.Millisecond) + + mu.Lock() + defer mu.Unlock() + require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", gotSubjectTokType) + claims := jwt.RegisteredClaims{} + _, err = jwt.ParseWithClaims(gotSubjectToken, &claims, func(*jwt.Token) (any, error) { return &workloadAPI.key().PublicKey, nil }) + require.NoError(t, err) + require.Equal(t, testControllerSPIFFEID, claims.Subject) + require.Equal(t, jwt.ClaimStrings{"https://auth.example.com"}, claims.Audience) +} + +// fakeWorkloadAPI issues JWT-SVIDs for testControllerSPIFFEID. +type fakeWorkloadAPI struct { + workload.UnimplementedSpiffeWorkloadAPIServer + t *testing.T + once sync.Once + signKey *ecdsa.PrivateKey +} + +func (f *fakeWorkloadAPI) key() *ecdsa.PrivateKey { + f.once.Do(func() { + var err error + f.signKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(f.t, err) + }) + return f.signKey +} + +func (f *fakeWorkloadAPI) FetchJWTSVID(_ context.Context, req *workload.JWTSVIDRequest) (*workload.JWTSVIDResponse, error) { + token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.RegisteredClaims{ + Subject: testControllerSPIFFEID, + Audience: req.Audience, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + }) + signed, err := token.SignedString(f.key()) + if err != nil { + return nil, err + } + return &workload.JWTSVIDResponse{Svids: []*workload.JWTSVID{{SpiffeId: testControllerSPIFFEID, Svid: signed}}}, nil +} + +// startFakeWorkloadAPI serves the given server on a unix socket and returns its address. +func startFakeWorkloadAPI(t *testing.T, server workload.SpiffeWorkloadAPIServer) string { + t.Helper() + // Use a short directory since unix socket paths are limited to ~104 bytes on macOS. + dir, err := os.MkdirTemp("", "spiffe") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(dir) }) + socketPath := filepath.Join(dir, "agent.sock") + + lis, err := net.Listen("unix", socketPath) + require.NoError(t, err) + s := grpc.NewServer() + workload.RegisterSpiffeWorkloadAPIServer(s, server) + go func() { _ = s.Serve(lis) }() + t.Cleanup(s.Stop) + return "unix://" + socketPath +} From f76444d3c1edfc6a3e574be0bdd23e264f1e3f87 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Wed, 7 Oct 2026 23:38:55 -0400 Subject: [PATCH 10/17] docs: add OpenAICredentials SPIFFE example Add an OpenAICredentials example using a SPIFFE JWT-SVID, and note in the API docs that such policies act as the controller's SPIFFE identity. Mounting the Workload API socket into the controller is left to the deployment. Part of #2791. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 2 + examples/basic/README.md | 1 + examples/basic/openai-spiffe.yaml | 88 +++++++++++++++++++ ...envoyproxy.io_backendsecuritypolicies.yaml | 6 +- site/docs/api/api.mdx | 2 +- 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 examples/basic/openai-spiffe.yaml diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index bbbf9d78e9..b9b1edaffc 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -434,6 +434,8 @@ type BackendSecurityPolicySubjectToken struct { OIDCExchangeToken *BackendSecurityPolicyOIDC `json:"oidcExchangeToken,omitempty"` // SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API. + // The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as + // the controller; only trusted cluster admins should be allowed to create such policies. // // +optional SPIFFEJWTSVID *BackendSecurityPolicySPIFFEJWTSVID `json:"spiffeJWTSVID,omitempty"` diff --git a/examples/basic/README.md b/examples/basic/README.md index fb0c022a17..43df21a53f 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -5,6 +5,7 @@ traffic for various AI providers. - `basic.yaml` - Basic configuration without any backends - `openai.yaml` - OpenAI integration +- `openai-spiffe.yaml` - OpenAI integration without a static API key, using SPIFFE and OAuth 2.0 Token Exchange - `aws.yaml` - AWS Bedrock with static credentials - `aws-irsa.yaml` - AWS Bedrock with IRSA (IAM Roles for Service Accounts) - `aws-pod-identity.yaml` - AWS Bedrock with EKS Pod Identity diff --git a/examples/basic/openai-spiffe.yaml b/examples/basic/openai-spiffe.yaml new file mode 100644 index 0000000000..33fea013c5 --- /dev/null +++ b/examples/basic/openai-spiffe.yaml @@ -0,0 +1,88 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: AIGatewayRoute +metadata: + name: envoy-ai-gateway-basic-openai-spiffe + namespace: default +spec: + parentRefs: + - name: envoy-ai-gateway-basic + kind: Gateway + group: gateway.networking.k8s.io + rules: + - matches: + - headers: + - type: Exact + name: x-ai-eg-model + value: gpt-4o-mini + backendRefs: + - name: envoy-ai-gateway-basic-openai-spiffe +--- +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: AIServiceBackend +metadata: + name: envoy-ai-gateway-basic-openai-spiffe + namespace: default +spec: + schema: + name: OpenAI + backendRef: + name: envoy-ai-gateway-basic-openai-spiffe + kind: Backend + group: gateway.envoyproxy.io +--- +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: envoy-ai-gateway-basic-openai-spiffe-credentials + namespace: default +spec: + targetRefs: + - group: aigateway.envoyproxy.io + kind: AIServiceBackend + name: envoy-ai-gateway-basic-openai-spiffe + type: OpenAICredentials + # The controller fetches a JWT-SVID for its own SPIFFE ID and exchanges it for a short-lived OpenAI + # access token (RFC 8693), rotating it before it expires. The SPIFFE Workload API socket must be + # mounted into the controller, with SPIFFE_ENDPOINT_SOCKET pointing at it (or set socketPath below). + # + # Policies run as the controller's SPIFFE identity, so only trusted cluster admins should be + # allowed to create them. + openAICredentials: + organization: OPENAI_ORGANIZATION_ID # Optional. Replace with your OpenAI organization ID, or remove. + project: OPENAI_PROJECT_ID # Optional. Replace with your OpenAI project ID, or remove. + tokenExchange: + tokenURL: https://TOKEN_EXCHANGE_HOST/oauth/token # Replace with the token exchange endpoint. + audience: https://api.openai.com # Optional. The audience of the exchanged access token. + subjectToken: + spiffeJWTSVID: + audience: TOKEN_EXCHANGE_AUDIENCE # Replace with the audience the token exchange endpoint expects. +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: Backend +metadata: + name: envoy-ai-gateway-basic-openai-spiffe + namespace: default +spec: + endpoints: + - fqdn: + hostname: api.openai.com + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: BackendTLSPolicy +metadata: + name: envoy-ai-gateway-basic-openai-spiffe-tls + namespace: default +spec: + targetRefs: + - group: "gateway.envoyproxy.io" + kind: Backend + name: envoy-ai-gateway-basic-openai-spiffe + validation: + wellKnownCACertificates: "System" + hostname: api.openai.com diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 6520bd0b26..e14cfae07a 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -14861,8 +14861,10 @@ spec: - oidc type: object spiffeJWTSVID: - description: SPIFFEJWTSVID obtains the subject token as - a JWT-SVID from the SPIFFE Workload API. + description: |- + SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API. + The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as + the controller; only trusted cluster admins should be allowed to create such policies. properties: audience: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 7a1040e9a3..bab46adab7 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -4078,7 +4078,7 @@ Exactly one source must be configured. name="spiffeJWTSVID" type="[BackendSecurityPolicySPIFFEJWTSVID](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffejwtsvid)" required="false" - description="SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API." + description="SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API.
The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as
the controller; only trusted cluster admins should be allowed to create such policies." /> From f417fe809bb222db3601c784e7d48f5608bf1e82 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Mon, 12 Oct 2026 18:47:06 -0400 Subject: [PATCH 11/17] openai: use OpenAI Workload Identity Federation Model OpenAICredentials on OpenAI Workload Identity Federation instead of a generic RFC 8693 token exchange, mirroring the GCP credentials API: - tokenExchange becomes workloadIdentityFederationConfig (OpenAIWorkloadIdentityFederationConfig) with required identityProviderID and serviceAccountID, and tokenURL defaulting to https://auth.openai.com/oauth/token. - Drop audience, scopes and subjectTokenType, which OpenAI does not accept; the subject token is always sent as a JWT. - Send the documented JSON body and surface OpenAI's error response ({"error": {"message", "code"}}) in the policy status, falling back to a truncated body. https://developers.openai.com/api/reference/workload-identity-federation Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 67 +++---- api/v1beta1/zz_generated.deepcopy.go | 89 +++++---- examples/basic/openai-spiffe.yaml | 17 +- .../controller/backend_security_policy.go | 25 ++- .../backend_security_policy_test.go | 33 ++-- ...openai_workload_identity_token_provider.go | 155 ++++++++++++++++ ..._workload_identity_token_provider_test.go} | 124 +++++++------ .../tokenprovider/token_exchange_provider.go | 136 -------------- ...envoyproxy.io_backendsecuritypolicies.yaml | 54 +++--- site/docs/api/api.mdx | 170 +++++++++--------- tests/controller/openai_credentials_test.go | 32 ++-- tests/crdcel/main_test.go | 14 +- .../apikey_with_openai_credentials.yaml | 7 +- .../openai_invalid_socket_path.yaml | 7 +- .../openai_invalid_token_url.yaml | 6 +- .../openai_missing_identity_provider_id.yaml | 18 ++ .../openai_missing_subject_token.yaml | 5 +- .../openai_multiple_subject_tokens.yaml | 7 +- .../backendsecuritypolicies/openai_oidc.yaml | 6 +- .../openai_spiffe.yaml | 11 +- .../openai_with_apikey.yaml | 7 +- 21 files changed, 512 insertions(+), 478 deletions(-) create mode 100644 internal/controller/tokenprovider/openai_workload_identity_token_provider.go rename internal/controller/tokenprovider/{token_exchange_provider_test.go => openai_workload_identity_token_provider_test.go} (54%) delete mode 100644 internal/controller/tokenprovider/token_exchange_provider.go create mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_missing_identity_provider_id.yaml diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index b9b1edaffc..4bf4e28435 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -102,7 +102,8 @@ type BackendSecurityPolicySpec struct { AnthropicAPIKey *BackendSecurityPolicyAnthropicAPIKey `json:"anthropicAPIKey,omitempty"` // OpenAICredentials is a mechanism to access OpenAI backend(s) without a static API key. The controller - // exchanges a subject token for a short-lived access token, which is injected into the Authorization header. + // exchanges a subject token for a short-lived access token via OpenAI Workload Identity Federation, which is + // injected into the Authorization header. // // +optional OpenAICredentials *BackendSecurityPolicyOpenAICredentials `json:"openAICredentials,omitempty"` @@ -369,66 +370,55 @@ type BackendSecurityPolicyOpenAICredentials struct { // +kubebuilder:validation:MinLength=1 Project string `json:"project,omitempty"` - // TokenExchange specifies how the controller obtains an access token via OAuth 2.0 Token Exchange. + // WorkloadIdentityFederationConfig is the configuration for OpenAI Workload Identity Federation. // // +kubebuilder:validation:Required - TokenExchange BackendSecurityPolicyTokenExchange `json:"tokenExchange"` + WorkloadIdentityFederationConfig OpenAIWorkloadIdentityFederationConfig `json:"workloadIdentityFederationConfig"` } -// BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -// The controller obtains a subject token, exchanges it at TokenURL for an access token, -// and stores the access token in a secret, rotating it before it expires. +// OpenAIWorkloadIdentityFederationConfig specifies how the controller obtains an OpenAI access token via +// OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at TokenURL for +// a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. // -// TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply -// to any subject token source. +// The identity provider and service account mapping must be configured in the OpenAI organization, with the +// issuer and audience of the subject token. // -// Each hop has its own audience: -// - subjectToken.spiffeJWTSVID.audience: the "aud" claim of the JWT-SVID, i.e. the token exchange endpoint. -// - subjectToken.oidcExchangeToken.aud: the audience requested from the OIDC provider for the subject token. -// - audience: the service the exchanged access token is intended for, e.g. the OpenAI API. -// -// https://datatracker.ietf.org/doc/html/rfc8693 -type BackendSecurityPolicyTokenExchange struct { - // TokenURL is the token exchange endpoint. +// https://developers.openai.com/api/reference/workload-identity-federation +type OpenAIWorkloadIdentityFederationConfig struct { + // IdentityProviderID is the ID of the OpenAI Workload Identity Provider configured for the issuer of the + // subject token, e.g. "idp_abc123". // // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:XValidation:rule="isURL(self)",message="tokenURL must be a valid URL" - TokenURL string `json:"tokenURL"` + IdentityProviderID string `json:"identityProviderID"` - // SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token. - // Use "urn:ietf:params:oauth:token-type:access_token" when the OIDC provider returns an opaque access token. + // ServiceAccountID is the ID of the OpenAI service account the access token is issued for. It must match a + // service account mapping of the identity provider. // - // +optional - // +kubebuilder:default="urn:ietf:params:oauth:token-type:jwt" + // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 - SubjectTokenType string `json:"subjectTokenType,omitempty"` + ServiceAccountID string `json:"serviceAccountID"` - // Audience is the RFC 8693 audience parameter: the logical name of the service the - // exchanged access token is intended for. It is unrelated to the audience of the subject token. + // TokenURL is the OpenAI token exchange endpoint. // // +optional - Audience string `json:"audience,omitempty"` - - // Scopes is the list of scopes requested for the exchanged access token. They are unrelated to - // the scopes requested from the OIDC provider for the subject token. - // - // +optional - // +kubebuilder:validation:MaxItems=16 - Scopes []string `json:"scopes,omitempty"` + // +kubebuilder:default="https://auth.openai.com/oauth/token" + // +kubebuilder:validation:XValidation:rule="isURL(self)",message="tokenURL must be a valid URL" + TokenURL string `json:"tokenURL,omitempty"` - // SubjectToken specifies where the subject token is obtained from. + // SubjectToken specifies where the subject token is obtained from. The subject token must be a JWT. // // +kubebuilder:validation:Required - SubjectToken BackendSecurityPolicySubjectToken `json:"subjectToken"` + SubjectToken OpenAISubjectToken `json:"subjectToken"` } -// BackendSecurityPolicySubjectToken specifies the source of a subject token. +// OpenAISubjectToken specifies the source of the subject token exchanged for an OpenAI access token. // Exactly one source must be configured. // // +kubebuilder:validation:XValidation:rule="[has(self.oidcExchangeToken), has(self.spiffeJWTSVID)].filter(x, x).size() == 1",message="Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified" -type BackendSecurityPolicySubjectToken struct { +type OpenAISubjectToken struct { // OIDCExchangeToken obtains the subject token from an OIDC provider via the client-credentials flow. + // The OIDC provider must return a JWT access token. // // +optional OIDCExchangeToken *BackendSecurityPolicyOIDC `json:"oidcExchangeToken,omitempty"` @@ -444,7 +434,8 @@ type BackendSecurityPolicySubjectToken struct { // BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the SPIFFE Workload API. type BackendSecurityPolicySPIFFEJWTSVID struct { // Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint - // validates it, so it must match the audience that endpoint expects. + // validates it, so it must match the audience that endpoint expects, e.g. the audience configured + // for the OpenAI Workload Identity Provider. // // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 819a632df0..ea0e318d00 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -669,7 +669,7 @@ func (in *BackendSecurityPolicyOIDC) DeepCopy() *BackendSecurityPolicyOIDC { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendSecurityPolicyOpenAICredentials) DeepCopyInto(out *BackendSecurityPolicyOpenAICredentials) { *out = *in - in.TokenExchange.DeepCopyInto(&out.TokenExchange) + in.WorkloadIdentityFederationConfig.DeepCopyInto(&out.WorkloadIdentityFederationConfig) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyOpenAICredentials. @@ -779,52 +779,6 @@ func (in *BackendSecurityPolicyStatus) DeepCopy() *BackendSecurityPolicyStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicySubjectToken) DeepCopyInto(out *BackendSecurityPolicySubjectToken) { - *out = *in - if in.OIDCExchangeToken != nil { - in, out := &in.OIDCExchangeToken, &out.OIDCExchangeToken - *out = new(BackendSecurityPolicyOIDC) - (*in).DeepCopyInto(*out) - } - if in.SPIFFEJWTSVID != nil { - in, out := &in.SPIFFEJWTSVID, &out.SPIFFEJWTSVID - *out = new(BackendSecurityPolicySPIFFEJWTSVID) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicySubjectToken. -func (in *BackendSecurityPolicySubjectToken) DeepCopy() *BackendSecurityPolicySubjectToken { - if in == nil { - return nil - } - out := new(BackendSecurityPolicySubjectToken) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BackendSecurityPolicyTokenExchange) DeepCopyInto(out *BackendSecurityPolicyTokenExchange) { - *out = *in - if in.Scopes != nil { - in, out := &in.Scopes, &out.Scopes - *out = make([]string, len(*in)) - copy(*out, *in) - } - in.SubjectToken.DeepCopyInto(&out.SubjectToken) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendSecurityPolicyTokenExchange. -func (in *BackendSecurityPolicyTokenExchange) DeepCopy() *BackendSecurityPolicyTokenExchange { - if in == nil { - return nil - } - out := new(BackendSecurityPolicyTokenExchange) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialOverrideFromDynamicMetadata) DeepCopyInto(out *CredentialOverrideFromDynamicMetadata) { *out = *in @@ -1838,6 +1792,47 @@ func (in *MCPToolFilter) DeepCopy() *MCPToolFilter { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenAISubjectToken) DeepCopyInto(out *OpenAISubjectToken) { + *out = *in + if in.OIDCExchangeToken != nil { + in, out := &in.OIDCExchangeToken, &out.OIDCExchangeToken + *out = new(BackendSecurityPolicyOIDC) + (*in).DeepCopyInto(*out) + } + if in.SPIFFEJWTSVID != nil { + in, out := &in.SPIFFEJWTSVID, &out.SPIFFEJWTSVID + *out = new(BackendSecurityPolicySPIFFEJWTSVID) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenAISubjectToken. +func (in *OpenAISubjectToken) DeepCopy() *OpenAISubjectToken { + if in == nil { + return nil + } + out := new(OpenAISubjectToken) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenAIWorkloadIdentityFederationConfig) DeepCopyInto(out *OpenAIWorkloadIdentityFederationConfig) { + *out = *in + in.SubjectToken.DeepCopyInto(&out.SubjectToken) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenAIWorkloadIdentityFederationConfig. +func (in *OpenAIWorkloadIdentityFederationConfig) DeepCopy() *OpenAIWorkloadIdentityFederationConfig { + if in == nil { + return nil + } + out := new(OpenAIWorkloadIdentityFederationConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProtectedResourceMetadata) DeepCopyInto(out *ProtectedResourceMetadata) { *out = *in diff --git a/examples/basic/openai-spiffe.yaml b/examples/basic/openai-spiffe.yaml index 33fea013c5..6a52fc5ecb 100644 --- a/examples/basic/openai-spiffe.yaml +++ b/examples/basic/openai-spiffe.yaml @@ -47,20 +47,25 @@ spec: name: envoy-ai-gateway-basic-openai-spiffe type: OpenAICredentials # The controller fetches a JWT-SVID for its own SPIFFE ID and exchanges it for a short-lived OpenAI - # access token (RFC 8693), rotating it before it expires. The SPIFFE Workload API socket must be - # mounted into the controller, with SPIFFE_ENDPOINT_SOCKET pointing at it (or set socketPath below). + # access token via OpenAI Workload Identity Federation, rotating it before it expires. The SPIFFE + # Workload API socket must be mounted into the controller, with SPIFFE_ENDPOINT_SOCKET pointing at it + # (or set socketPath below). + # + # The identity provider (trusting your SPIFFE trust domain) and the service account mapping must be + # configured in your OpenAI organization: + # https://developers.openai.com/api/reference/workload-identity-federation # # Policies run as the controller's SPIFFE identity, so only trusted cluster admins should be # allowed to create them. openAICredentials: organization: OPENAI_ORGANIZATION_ID # Optional. Replace with your OpenAI organization ID, or remove. project: OPENAI_PROJECT_ID # Optional. Replace with your OpenAI project ID, or remove. - tokenExchange: - tokenURL: https://TOKEN_EXCHANGE_HOST/oauth/token # Replace with the token exchange endpoint. - audience: https://api.openai.com # Optional. The audience of the exchanged access token. + workloadIdentityFederationConfig: + identityProviderID: OPENAI_IDENTITY_PROVIDER_ID # Replace with your OpenAI Workload Identity Provider ID. + serviceAccountID: OPENAI_SERVICE_ACCOUNT_ID # Replace with your OpenAI service account ID. subjectToken: spiffeJWTSVID: - audience: TOKEN_EXCHANGE_AUDIENCE # Replace with the audience the token exchange endpoint expects. + audience: OPENAI_IDENTITY_PROVIDER_AUDIENCE # Replace with the audience configured for the identity provider. --- apiVersion: gateway.envoyproxy.io/v1alpha1 kind: Backend diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go index ad454cc2e6..ee3b315573 100644 --- a/internal/controller/backend_security_policy.go +++ b/internal/controller/backend_security_policy.go @@ -353,17 +353,17 @@ func (c *BackendSecurityPolicyController) executeRotation(ctx context.Context, r } // newOpenAITokenProvider returns a TokenProvider that exchanges the configured subject token for an -// OpenAI access token. +// OpenAI access token via OpenAI Workload Identity Federation. func (c *BackendSecurityPolicyController) newOpenAITokenProvider(ctx context.Context, bsp *aigv1b1.BackendSecurityPolicy) (tokenprovider.TokenProvider, error) { - exchange := &bsp.Spec.OpenAICredentials.TokenExchange + wif := &bsp.Spec.OpenAICredentials.WorkloadIdentityFederationConfig var subjectTokens tokenprovider.TokenProvider var err error switch { - case exchange.SubjectToken.SPIFFEJWTSVID != nil: - svid := exchange.SubjectToken.SPIFFEJWTSVID + case wif.SubjectToken.SPIFFEJWTSVID != nil: + svid := wif.SubjectToken.SPIFFEJWTSVID subjectTokens, err = tokenprovider.NewSPIFFETokenProvider(svid.SocketPath, svid.Audience) - case exchange.SubjectToken.OIDCExchangeToken != nil: - subjectTokens, err = tokenprovider.NewOidcTokenProvider(ctx, c.client, &exchange.SubjectToken.OIDCExchangeToken.OIDC, + case wif.SubjectToken.OIDCExchangeToken != nil: + subjectTokens, err = tokenprovider.NewOidcTokenProvider(ctx, c.client, &wif.SubjectToken.OIDCExchangeToken.OIDC, bsp.Namespace, c.referenceGrantValidator.validateSecretReference) default: return nil, fmt.Errorf("one of spiffeJWTSVID or oidcExchangeToken must be defined, namespace %s name %s", bsp.Namespace, bsp.Name) @@ -371,11 +371,10 @@ func (c *BackendSecurityPolicyController) newOpenAITokenProvider(ctx context.Con if err != nil { return nil, fmt.Errorf("failed to initialize subject token provider: %w", err) } - return tokenprovider.NewTokenExchangeProvider(tokenprovider.TokenExchangeConfig{ - TokenURL: exchange.TokenURL, - SubjectTokenType: exchange.SubjectTokenType, - Audience: exchange.Audience, - Scopes: exchange.Scopes, + return tokenprovider.NewOpenAIWorkloadIdentityTokenProvider(tokenprovider.OpenAIWorkloadIdentityConfig{ + TokenURL: wif.TokenURL, + IdentityProviderID: wif.IdentityProviderID, + ServiceAccountID: wif.ServiceAccountID, }, subjectTokens) } @@ -396,8 +395,8 @@ func getBackendSecurityPolicyAuthOIDC(spec *aigv1b1.BackendSecurityPolicySpec) * return &spec.GCPCredentials.WorkloadIdentityFederationConfig.OIDCExchangeToken.OIDC } case aigv1b1.BackendSecurityPolicyTypeOpenAICredentials: - if spec.OpenAICredentials != nil && spec.OpenAICredentials.TokenExchange.SubjectToken.OIDCExchangeToken != nil { - return &spec.OpenAICredentials.TokenExchange.SubjectToken.OIDCExchangeToken.OIDC + if spec.OpenAICredentials != nil && spec.OpenAICredentials.WorkloadIdentityFederationConfig.SubjectToken.OIDCExchangeToken != nil { + return &spec.OpenAICredentials.WorkloadIdentityFederationConfig.SubjectToken.OIDCExchangeToken.OIDC } } return nil diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index 53496b5745..83a6028465 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -10,7 +10,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "net/url" "path/filepath" "testing" "time" @@ -867,7 +866,7 @@ func TestNewBackendSecurityPolicyController_RotateCredentialInvalidType(t *testi } func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC(t *testing.T) { - var gotForm url.Values + var gotBody map[string]string mux := http.NewServeMux() server := httptest.NewServer(mux) defer server.Close() @@ -880,8 +879,7 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC _, _ = w.Write([]byte(`{"access_token": "subject-token", "token_type": "Bearer", "expires_in": 3600}`)) }) mux.HandleFunc("/exchange", func(w http.ResponseWriter, r *http.Request) { - require.NoError(t, r.ParseForm()) - gotForm = r.PostForm + require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody)) w.Header().Add("Content-Type", "application/json") _, _ = w.Write([]byte(`{"access_token": "openai-access-token", "token_type": "Bearer", "expires_in": 3600}`)) }) @@ -900,12 +898,11 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC Spec: aigv1b1.BackendSecurityPolicySpec{ Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ - TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ - TokenURL: server.URL + "/exchange", - SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", - Audience: "https://api.openai.com", - Scopes: []string{"api.model.read"}, - SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + WorkloadIdentityFederationConfig: aigv1b1.OpenAIWorkloadIdentityFederationConfig{ + TokenURL: server.URL + "/exchange", + IdentityProviderID: "idp_123", + ServiceAccountID: "sa_456", + SubjectToken: aigv1b1.OpenAISubjectToken{ OIDCExchangeToken: &aigv1b1.BackendSecurityPolicyOIDC{ OIDC: egv1a1.OIDC{ Provider: egv1a1.OIDCProvider{Issuer: server.URL}, @@ -927,10 +924,9 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC require.NoError(t, err) require.Greater(t, res.RequeueAfter, 50*time.Minute) - require.Equal(t, "subject-token", gotForm.Get("subject_token")) - require.Equal(t, "urn:ietf:params:oauth:token-type:access_token", gotForm.Get("subject_token_type")) - require.Equal(t, "https://api.openai.com", gotForm.Get("audience")) - require.Equal(t, "api.model.read", gotForm.Get("scope")) + require.Equal(t, "subject-token", gotBody["subject_token"]) + require.Equal(t, "idp_123", gotBody["identity_provider_id"]) + require.Equal(t, "sa_456", gotBody["service_account_id"]) secret, err := rotators.LookupSecret(t.Context(), cl, bspNamespace, rotators.GetBSPSecretName("openai-oidc")) require.NoError(t, err) @@ -949,10 +945,11 @@ func TestBackendSecurityPolicyController_Reconcile_OpenAICredentials_SPIFFEUnava Spec: aigv1b1.BackendSecurityPolicySpec{ Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ - TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ - TokenURL: "https://auth.example.com/oauth/token", - SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt", - SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + WorkloadIdentityFederationConfig: aigv1b1.OpenAIWorkloadIdentityFederationConfig{ + TokenURL: "https://auth.openai.com/oauth/token", + IdentityProviderID: "idp_123", + ServiceAccountID: "sa_456", + SubjectToken: aigv1b1.OpenAISubjectToken{ SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{ Audience: "https://auth.example.com", SocketPath: "unix://" + filepath.Join(t.TempDir(), "missing.sock"), diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go new file mode 100644 index 0000000000..37f08b3456 --- /dev/null +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -0,0 +1,155 @@ +// Copyright Envoy AI Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package tokenprovider + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "time" + + "github.com/envoyproxy/ai-gateway/internal/json" +) + +const ( + // tokenExchangeGrantType is the RFC 8693 grant type for token exchange requests. + tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" // #nosec G101 + // jwtTokenType is the RFC 8693 token type of a JWT subject token. + jwtTokenType = "urn:ietf:params:oauth:token-type:jwt" // #nosec G101 + // maxErrorBodyLength caps how much of an unrecognized error response is included in errors. + maxErrorBodyLength = 512 +) + +// OpenAIWorkloadIdentityConfig holds the parameters of an OpenAI Workload Identity Federation token exchange. +type OpenAIWorkloadIdentityConfig struct { + // TokenURL is the OpenAI token exchange endpoint. + TokenURL string + // IdentityProviderID is the OpenAI Workload Identity Provider ID. + IdentityProviderID string + // ServiceAccountID is the OpenAI service account ID. + ServiceAccountID string +} + +// openAIWorkloadIdentityTokenProvider is a provider implements TokenProvider interface for OpenAI +// Workload Identity Federation. +type openAIWorkloadIdentityTokenProvider struct { + config OpenAIWorkloadIdentityConfig + subjectTokens TokenProvider + httpClient *http.Client +} + +// openAITokenExchangeRequest is the JSON body of an OpenAI token exchange request. +type openAITokenExchangeRequest struct { + GrantType string `json:"grant_type"` + SubjectToken string `json:"subject_token"` + SubjectTokenType string `json:"subject_token_type"` + IdentityProviderID string `json:"identity_provider_id"` + ServiceAccountID string `json:"service_account_id"` +} + +// openAITokenExchangeResponse is the successful response of an OpenAI token exchange. +type openAITokenExchangeResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` +} + +// openAIErrorResponse is the error response of the OpenAI API. +type openAIErrorResponse struct { + Error struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"error"` +} + +// NewOpenAIWorkloadIdentityTokenProvider creates a new TokenProvider that obtains a JWT subject token from +// subjectTokens and exchanges it for an OpenAI access token at config.TokenURL. +// +// https://developers.openai.com/api/reference/workload-identity-federation +func NewOpenAIWorkloadIdentityTokenProvider(config OpenAIWorkloadIdentityConfig, subjectTokens TokenProvider) (TokenProvider, error) { + if config.TokenURL == "" { + return nil, errors.New("token exchange URL must not be empty") + } + if config.IdentityProviderID == "" { + return nil, errors.New("identity provider ID must not be empty") + } + if config.ServiceAccountID == "" { + return nil, errors.New("service account ID must not be empty") + } + if subjectTokens == nil { + return nil, errors.New("subject token provider must not be nil") + } + return &openAIWorkloadIdentityTokenProvider{ + config: config, + subjectTokens: subjectTokens, + httpClient: &http.Client{Timeout: time.Minute}, + }, nil +} + +// GetToken implements TokenProvider.GetToken method to retrieve an OpenAI access token and its expiration time. +// +// When the response has no expires_in, the access token is assumed to expire with the subject token. +func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (TokenExpiry, error) { + subject, err := t.subjectTokens.GetToken(ctx) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to get subject token: %w", err) + } + + reqBody, err := json.Marshal(openAITokenExchangeRequest{ + GrantType: tokenExchangeGrantType, + SubjectToken: subject.Token, + SubjectTokenType: jwtTokenType, + IdentityProviderID: t.config.IdentityProviderID, + ServiceAccountID: t.config.ServiceAccountID, + }) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to encode token exchange request: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.config.TokenURL, bytes.NewReader(reqBody)) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to create token exchange request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + issuedAt := time.Now() + resp, err := t.httpClient.Do(req) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to send token exchange request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return TokenExpiry{}, fmt.Errorf("failed to read token exchange response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + var errResp openAIErrorResponse + if json.Unmarshal(body, &errResp) == nil && errResp.Error.Message != "" { + return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d: %s: %s", + resp.StatusCode, errResp.Error.Code, errResp.Error.Message) + } + if len(body) > maxErrorBodyLength { + body = body[:maxErrorBodyLength] + } + return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, body) + } + + var tokenResp openAITokenExchangeResponse + if err = json.Unmarshal(body, &tokenResp); err != nil { + return TokenExpiry{}, fmt.Errorf("failed to decode token exchange response: %w", err) + } + if tokenResp.AccessToken == "" { + return TokenExpiry{}, errors.New("token exchange response has no access_token") + } + expiresAt := subject.ExpiresAt + if tokenResp.ExpiresIn > 0 { + expiresAt = issuedAt.Add(time.Duration(tokenResp.ExpiresIn) * time.Second) + } + return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil +} diff --git a/internal/controller/tokenprovider/token_exchange_provider_test.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go similarity index 54% rename from internal/controller/tokenprovider/token_exchange_provider_test.go rename to internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go index 77a8111ee2..bd6e9c5ca1 100644 --- a/internal/controller/tokenprovider/token_exchange_provider_test.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go @@ -7,50 +7,61 @@ package tokenprovider import ( "errors" + "io" "net/http" "net/http/httptest" - "net/url" + "strings" "testing" "time" "github.com/stretchr/testify/require" + + "github.com/envoyproxy/ai-gateway/internal/json" ) -const testJWTTokenType = "urn:ietf:params:oauth:token-type:jwt" +func testOpenAIWorkloadIdentityConfig(tokenURL string) OpenAIWorkloadIdentityConfig { + return OpenAIWorkloadIdentityConfig{TokenURL: tokenURL, IdentityProviderID: "idp_123", ServiceAccountID: "sa_456"} +} -func TestNewTokenExchangeProvider(t *testing.T) { +func TestNewOpenAIWorkloadIdentityTokenProvider(t *testing.T) { subject := NewMockTokenProvider("subject", time.Now(), nil) for _, tc := range []struct { name string - config TokenExchangeConfig + config OpenAIWorkloadIdentityConfig subject TokenProvider expErr string }{ { name: "valid", - config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token", SubjectTokenType: testJWTTokenType}, + config: testOpenAIWorkloadIdentityConfig("https://auth.openai.com/oauth/token"), subject: subject, }, { name: "empty token URL", - config: TokenExchangeConfig{SubjectTokenType: testJWTTokenType}, + config: OpenAIWorkloadIdentityConfig{IdentityProviderID: "idp_123", ServiceAccountID: "sa_456"}, subject: subject, expErr: "token exchange URL must not be empty", }, { - name: "empty subject token type", - config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token"}, + name: "empty identity provider ID", + config: OpenAIWorkloadIdentityConfig{TokenURL: "https://auth.openai.com/oauth/token", ServiceAccountID: "sa_456"}, subject: subject, - expErr: "token exchange subject token type must not be empty", + expErr: "identity provider ID must not be empty", + }, + { + name: "empty service account ID", + config: OpenAIWorkloadIdentityConfig{TokenURL: "https://auth.openai.com/oauth/token", IdentityProviderID: "idp_123"}, + subject: subject, + expErr: "service account ID must not be empty", }, { name: "nil subject token provider", - config: TokenExchangeConfig{TokenURL: "https://auth.example.com/token", SubjectTokenType: testJWTTokenType}, - expErr: "token exchange subject token provider must not be nil", + config: testOpenAIWorkloadIdentityConfig("https://auth.openai.com/oauth/token"), + expErr: "subject token provider must not be nil", }, } { t.Run(tc.name, func(t *testing.T) { - provider, err := NewTokenExchangeProvider(tc.config, tc.subject) + provider, err := NewOpenAIWorkloadIdentityTokenProvider(tc.config, tc.subject) if tc.expErr != "" { require.EqualError(t, err, tc.expErr) require.Nil(t, provider) @@ -62,27 +73,24 @@ func TestNewTokenExchangeProvider(t *testing.T) { } } -func TestTokenExchangeProvider_GetToken(t *testing.T) { +func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { subjectExpiry := time.Now().Add(time.Hour).Truncate(time.Second) t.Run("success", func(t *testing.T) { - var gotForm url.Values + var gotBody map[string]string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) - require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) - require.NoError(t, r.ParseForm()) - gotForm = r.PostForm + require.Equal(t, "application/json", r.Header.Get("Content-Type")) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(body, &gotBody)) w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"access_token":"exchanged","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":600}`)) + _, _ = w.Write([]byte(`{"access_token":"exchanged","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":600,"expires_at":1789045200}`)) })) defer server.Close() - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{ - TokenURL: server.URL, - SubjectTokenType: testJWTTokenType, - Audience: "https://api.openai.com", - Scopes: []string{"a", "b"}, - }, NewMockTokenProvider("subject", subjectExpiry, nil)) + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), + NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) before := time.Now() @@ -90,31 +98,13 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { require.NoError(t, err) require.Equal(t, "exchanged", token.Token) require.WithinRange(t, token.ExpiresAt, before.Add(600*time.Second), time.Now().Add(600*time.Second)) - require.Equal(t, url.Values{ - "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, - "subject_token": {"subject"}, - "subject_token_type": {testJWTTokenType}, - "audience": {"https://api.openai.com"}, - "scope": {"a b"}, - }, gotForm) - }) - - t.Run("optional parameters omitted", func(t *testing.T) { - var gotForm url.Values - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.NoError(t, r.ParseForm()) - gotForm = r.PostForm - _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) - })) - defer server.Close() - - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, - NewMockTokenProvider("subject", subjectExpiry, nil)) - require.NoError(t, err) - _, err = provider.GetToken(t.Context()) - require.NoError(t, err) - require.NotContains(t, gotForm, "audience") - require.NotContains(t, gotForm, "scope") + require.Equal(t, map[string]string{ + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token": "subject", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + "identity_provider_id": "idp_123", + "service_account_id": "sa_456", + }, gotBody) }) t.Run("no expires_in uses subject token expiry", func(t *testing.T) { @@ -123,7 +113,7 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { })) defer server.Close() - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) token, err := provider.GetToken(t.Context()) @@ -132,7 +122,7 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { }) t.Run("subject token error", func(t *testing.T) { - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: "http://127.0.0.1:0", SubjectTokenType: testJWTTokenType}, + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig("http://127.0.0.1:0"), NewMockTokenProvider("", time.Time{}, errors.New("workload API unavailable"))) require.NoError(t, err) _, err = provider.GetToken(t.Context()) @@ -146,16 +136,22 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { expErr string }{ { - name: "oauth error response", + name: "openai error response", + status: http.StatusUnauthorized, + body: `{"error":{"message":"No identity provider found for the given provider id.","type":"invalid_request_error","param":null,"code":"invalid_grant"}}`, + expErr: "token exchange failed with status 401: invalid_grant: No identity provider found for the given provider id.", + }, + { + name: "unrecognized error response", status: http.StatusBadRequest, body: `{"error":"invalid_grant","error_description":"subject token expired"}`, - expErr: "token exchange failed with status 400: invalid_grant: subject token expired", + expErr: `token exchange failed with status 400: {"error":"invalid_grant","error_description":"subject token expired"}`, }, { name: "non-json error response", status: http.StatusBadGateway, body: `bad gateway`, - expErr: "token exchange failed with status 502", + expErr: "token exchange failed with status 502: bad gateway", }, { name: "invalid json", @@ -177,7 +173,7 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { })) defer server.Close() - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) _, err = provider.GetToken(t.Context()) @@ -185,11 +181,25 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { }) } + t.Run("long error response is truncated", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write([]byte(strings.Repeat("x", maxErrorBodyLength+100))) + })) + defer server.Close() + + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + _, err = provider.GetToken(t.Context()) + require.EqualError(t, err, "token exchange failed with status 502: "+strings.Repeat("x", maxErrorBodyLength)) + }) + t.Run("request error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) server.Close() - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: server.URL, SubjectTokenType: testJWTTokenType}, + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) _, err = provider.GetToken(t.Context()) @@ -197,7 +207,7 @@ func TestTokenExchangeProvider_GetToken(t *testing.T) { }) t.Run("invalid token URL", func(t *testing.T) { - provider, err := NewTokenExchangeProvider(TokenExchangeConfig{TokenURL: "://bad", SubjectTokenType: testJWTTokenType}, + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig("://bad"), NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) _, err = provider.GetToken(t.Context()) diff --git a/internal/controller/tokenprovider/token_exchange_provider.go b/internal/controller/tokenprovider/token_exchange_provider.go deleted file mode 100644 index 3888e05dc7..0000000000 --- a/internal/controller/tokenprovider/token_exchange_provider.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright Envoy AI Gateway Authors -// SPDX-License-Identifier: Apache-2.0 -// The full text of the Apache license is available in the LICENSE file at -// the root of the repo. - -package tokenprovider - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/envoyproxy/ai-gateway/internal/json" -) - -// tokenExchangeGrantType is the RFC 8693 grant type for token exchange requests. -const tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" // #nosec G101 - -// TokenExchangeConfig holds the parameters of an RFC 8693 token exchange request. -type TokenExchangeConfig struct { - // TokenURL is the token exchange endpoint. - TokenURL string - // SubjectTokenType is the subject_token_type parameter. - SubjectTokenType string - // Audience is the optional audience parameter. - Audience string - // Scopes are joined with spaces into the optional scope parameter. - Scopes []string -} - -// tokenExchangeProvider is a provider implements TokenProvider interface for OAuth 2.0 Token Exchange. -type tokenExchangeProvider struct { - config TokenExchangeConfig - subjectTokens TokenProvider - httpClient *http.Client -} - -// tokenExchangeResponse is the successful response of RFC 8693 section 2.2.1. -type tokenExchangeResponse struct { - AccessToken string `json:"access_token"` - ExpiresIn int64 `json:"expires_in"` -} - -// tokenExchangeErrorResponse is the error response of RFC 6749 section 5.2. -type tokenExchangeErrorResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` -} - -// NewTokenExchangeProvider creates a new TokenProvider that obtains a subject token from subjectTokens -// and exchanges it for an access token at config.TokenURL, following RFC 8693. -// -// https://datatracker.ietf.org/doc/html/rfc8693 -func NewTokenExchangeProvider(config TokenExchangeConfig, subjectTokens TokenProvider) (TokenProvider, error) { - if config.TokenURL == "" { - return nil, errors.New("token exchange URL must not be empty") - } - if config.SubjectTokenType == "" { - return nil, errors.New("token exchange subject token type must not be empty") - } - if subjectTokens == nil { - return nil, errors.New("token exchange subject token provider must not be nil") - } - return &tokenExchangeProvider{ - config: config, - subjectTokens: subjectTokens, - httpClient: &http.Client{Timeout: time.Minute}, - }, nil -} - -// GetToken implements TokenProvider.GetToken method to retrieve an exchanged access token and its expiration time. -// -// When the response has no expires_in, the access token is assumed to expire with the subject token. -func (t *tokenExchangeProvider) GetToken(ctx context.Context) (TokenExpiry, error) { - subject, err := t.subjectTokens.GetToken(ctx) - if err != nil { - return TokenExpiry{}, fmt.Errorf("failed to get subject token: %w", err) - } - - form := url.Values{ - "grant_type": {tokenExchangeGrantType}, - "subject_token": {subject.Token}, - "subject_token_type": {t.config.SubjectTokenType}, - } - if t.config.Audience != "" { - form.Set("audience", t.config.Audience) - } - if len(t.config.Scopes) > 0 { - form.Set("scope", strings.Join(t.config.Scopes, " ")) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.config.TokenURL, strings.NewReader(form.Encode())) - if err != nil { - return TokenExpiry{}, fmt.Errorf("failed to create token exchange request: %w", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Accept", "application/json") - - issuedAt := time.Now() - resp, err := t.httpClient.Do(req) - if err != nil { - return TokenExpiry{}, fmt.Errorf("failed to send token exchange request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return TokenExpiry{}, fmt.Errorf("failed to read token exchange response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - var errResp tokenExchangeErrorResponse - if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" { - return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d: %s: %s", - resp.StatusCode, errResp.Error, errResp.ErrorDescription) - } - return TokenExpiry{}, fmt.Errorf("token exchange failed with status %d", resp.StatusCode) - } - - var tokenResp tokenExchangeResponse - if err = json.Unmarshal(body, &tokenResp); err != nil { - return TokenExpiry{}, fmt.Errorf("failed to decode token exchange response: %w", err) - } - if tokenResp.AccessToken == "" { - return TokenExpiry{}, errors.New("token exchange response has no access_token") - } - expiresAt := subject.ExpiresAt - if tokenResp.ExpiresIn > 0 { - expiresAt = issuedAt.Add(time.Duration(tokenResp.ExpiresIn) * time.Second) - } - return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil -} diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index e14cfae07a..cb7943c62e 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -12828,7 +12828,8 @@ spec: openAICredentials: description: |- OpenAICredentials is a mechanism to access OpenAI backend(s) without a static API key. The controller - exchanges a subject token for a short-lived access token, which is injected into the Authorization header. + exchanges a subject token for a short-lived access token via OpenAI Workload Identity Federation, which is + injected into the Authorization header. properties: organization: description: Organization is the OpenAI organization ID. When @@ -12840,30 +12841,30 @@ spec: sent in the "OpenAI-Project" header. minLength: 1 type: string - tokenExchange: - description: TokenExchange specifies how the controller obtains - an access token via OAuth 2.0 Token Exchange. + workloadIdentityFederationConfig: + description: WorkloadIdentityFederationConfig is the configuration + for OpenAI Workload Identity Federation. properties: - audience: + identityProviderID: description: |- - Audience is the RFC 8693 audience parameter: the logical name of the service the - exchanged access token is intended for. It is unrelated to the audience of the subject token. + IdentityProviderID is the ID of the OpenAI Workload Identity Provider configured for the issuer of the + subject token, e.g. "idp_abc123". + minLength: 1 type: string - scopes: + serviceAccountID: description: |- - Scopes is the list of scopes requested for the exchanged access token. They are unrelated to - the scopes requested from the OIDC provider for the subject token. - items: - type: string - maxItems: 16 - type: array + ServiceAccountID is the ID of the OpenAI service account the access token is issued for. It must match a + service account mapping of the identity provider. + minLength: 1 + type: string subjectToken: description: SubjectToken specifies where the subject token - is obtained from. + is obtained from. The subject token must be a JWT. properties: oidcExchangeToken: - description: OIDCExchangeToken obtains the subject token - from an OIDC provider via the client-credentials flow. + description: |- + OIDCExchangeToken obtains the subject token from an OIDC provider via the client-credentials flow. + The OIDC provider must return a JWT access token. properties: aud: description: Aud defines the audience that this ID @@ -14869,7 +14870,8 @@ spec: audience: description: |- Audience is the audience ("aud" claim) of the requested JWT-SVID. The token exchange endpoint - validates it, so it must match the audience that endpoint expects. + validates it, so it must match the audience that endpoint expects, e.g. the audience configured + for the OpenAI Workload Identity Provider. minLength: 1 type: string socketPath: @@ -14887,26 +14889,20 @@ spec: must be specified rule: '[has(self.oidcExchangeToken), has(self.spiffeJWTSVID)].filter(x, x).size() == 1' - subjectTokenType: - default: urn:ietf:params:oauth:token-type:jwt - description: |- - SubjectTokenType is the RFC 8693 subject_token_type, identifying the format of the subject token. - Use "urn:ietf:params:oauth:token-type:access_token" when the OIDC provider returns an opaque access token. - minLength: 1 - type: string tokenURL: - description: TokenURL is the token exchange endpoint. - minLength: 1 + default: https://auth.openai.com/oauth/token + description: TokenURL is the OpenAI token exchange endpoint. type: string x-kubernetes-validations: - message: tokenURL must be a valid URL rule: isURL(self) required: + - identityProviderID + - serviceAccountID - subjectToken - - tokenURL type: object required: - - tokenExchange + - workloadIdentityFederationConfig type: object targetRefs: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index bab46adab7..dda25dd1f2 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -3191,8 +3191,6 @@ MCPRouteList contains a list of MCPRoute. - [BackendSecurityPolicySPIFFEJWTSVID](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffejwtsvid) - [BackendSecurityPolicySpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspec) - [BackendSecurityPolicyStatus](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicystatus) -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) -- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - [BackendSecurityPolicyType](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytype) - [CredentialOverrideFromDynamicMetadata](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromdynamicmetadata) - [CredentialOverrideFromRequestHeaders](#github-com-envoyproxy-ai-gateway-api-v1beta1-credentialoverridefromrequestheaders) @@ -3232,6 +3230,8 @@ MCPRouteList contains a list of MCPRoute. - [MCPRouteSpec](#github-com-envoyproxy-ai-gateway-api-v1beta1-mcproutespec) - [MCPRouteStatus](#github-com-envoyproxy-ai-gateway-api-v1beta1-mcproutestatus) - [MCPToolFilter](#github-com-envoyproxy-ai-gateway-api-v1beta1-mcptoolfilter) +- [OpenAISubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-openaisubjecttoken) +- [OpenAIWorkloadIdentityFederationConfig](#github-com-envoyproxy-ai-gateway-api-v1beta1-openaiworkloadidentityfederationconfig) - [ProtectedResourceMetadata](#github-com-envoyproxy-ai-gateway-api-v1beta1-protectedresourcemetadata) - [ToolCall](#github-com-envoyproxy-ai-gateway-api-v1beta1-toolcall) - [VersionedAPISchema](#github-com-envoyproxy-ai-gateway-api-v1beta1-versionedapischema) @@ -3881,8 +3881,8 @@ default service account credentials when running on GCP. **Appears in:** - [AWSOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-awsoidcexchangetoken) - [AzureOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-azureoidcexchangetoken) -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) - [GCPOIDCExchangeToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-gcpoidcexchangetoken) +- [OpenAISubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-openaisubjecttoken) BackendSecurityPolicyOIDC specifies OIDC related fields. @@ -3932,10 +3932,10 @@ BackendSecurityPolicyOpenAICredentials contains the supported authentication mec required="false" description="Project is the OpenAI project ID. When set, it is sent in the `OpenAI-Project` header." /> @@ -3944,7 +3944,7 @@ BackendSecurityPolicyOpenAICredentials contains the supported authentication mec **Appears in:** -- [BackendSecurityPolicySubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicysubjecttoken) +- [OpenAISubjectToken](#github-com-envoyproxy-ai-gateway-api-v1beta1-openaisubjecttoken) BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the SPIFFE Workload API. @@ -3956,7 +3956,7 @@ BackendSecurityPolicySPIFFEJWTSVID specifies how to obtain a JWT-SVID from the S name="audience" type="string" required="true" - description="Audience is the audience (`aud` claim) of the requested JWT-SVID. The token exchange endpoint
validates it, so it must match the audience that endpoint expects." + description="Audience is the audience (`aud` claim) of the requested JWT-SVID. The token exchange endpoint
validates it, so it must match the audience that endpoint expects, e.g. the audience configured
for the OpenAI Workload Identity Provider." /> -#### BackendSecurityPolicySubjectToken - - - -**Appears in:** -- [BackendSecurityPolicyTokenExchange](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicytokenexchange) - -BackendSecurityPolicySubjectToken specifies the source of a subject token. -Exactly one source must be configured. - -##### Fields - - - - - - -#### BackendSecurityPolicyTokenExchange - - - -**Appears in:** -- [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) - -BackendSecurityPolicyTokenExchange specifies an OAuth 2.0 Token Exchange (RFC 8693) request. -The controller obtains a subject token, exchanges it at TokenURL for an access token, -and stores the access token in a secret, rotating it before it expires. - -TokenURL, SubjectTokenType, Audience and Scopes are parameters of the exchange request and apply -to any subject token source. - -Each hop has its own audience: - - subjectToken.spiffeJWTSVID.audience: the "aud" claim of the JWT-SVID, i.e. the token exchange endpoint. - - subjectToken.oidcExchangeToken.aud: the audience requested from the OIDC provider for the subject token. - - audience: the service the exchanged access token is intended for, e.g. the OpenAI API. - -https://datatracker.ietf.org/doc/html/rfc8693 - -##### Fields - - - - - - #### BackendSecurityPolicyType **Underlying type:** string @@ -5388,6 +5307,77 @@ a tool must match an include rule AND not match any exclude rule to be allowed. /> +#### OpenAISubjectToken + + + +**Appears in:** +- [OpenAIWorkloadIdentityFederationConfig](#github-com-envoyproxy-ai-gateway-api-v1beta1-openaiworkloadidentityfederationconfig) + +OpenAISubjectToken specifies the source of the subject token exchanged for an OpenAI access token. +Exactly one source must be configured. + +##### Fields + + + + + + +#### OpenAIWorkloadIdentityFederationConfig + + + +**Appears in:** +- [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) + +OpenAIWorkloadIdentityFederationConfig specifies how the controller obtains an OpenAI access token via +OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at TokenURL for +a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. + +The identity provider and service account mapping must be configured in the OpenAI organization, with the +issuer and audience of the subject token. + +https://developers.openai.com/api/reference/workload-identity-federation + +##### Fields + + + + + + #### ProtectedResourceMetadata diff --git a/tests/controller/openai_credentials_test.go b/tests/controller/openai_credentials_test.go index 779af7afc8..b21dc691cb 100644 --- a/tests/controller/openai_credentials_test.go +++ b/tests/controller/openai_credentials_test.go @@ -34,6 +34,7 @@ import ( aigv1b1 "github.com/envoyproxy/ai-gateway/api/v1beta1" "github.com/envoyproxy/ai-gateway/internal/controller" "github.com/envoyproxy/ai-gateway/internal/controller/rotators" + "github.com/envoyproxy/ai-gateway/internal/json" internaltesting "github.com/envoyproxy/ai-gateway/internal/testing" testsinternal "github.com/envoyproxy/ai-gateway/tests/internal" ) @@ -41,7 +42,7 @@ import ( const testControllerSPIFFEID = "spiffe://example.org/ns/envoy-ai-gateway-system/sa/ai-gateway-controller" // TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE runs the BackendSecurityPolicy controller -// against a fake SPIFFE Workload API and a fake token exchange endpoint, and checks that the exchanged +// against a fake SPIFFE Workload API and a fake OpenAI token exchange endpoint, and checks that the exchanged // access token ends up in the generated secret. func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { c, cfg, k := testsinternal.NewEnvTest(t) @@ -50,18 +51,17 @@ func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { socketAddr := startFakeWorkloadAPI(t, workloadAPI) var ( - mu sync.Mutex - gotSubjectToken string - gotSubjectTokType string + mu sync.Mutex + gotBody map[string]string ) exchangeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := r.ParseForm(); err != nil { + var body map[string]string + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { w.WriteHeader(http.StatusBadRequest) return } mu.Lock() - gotSubjectToken = r.PostForm.Get("subject_token") - gotSubjectTokType = r.PostForm.Get("subject_token_type") + gotBody = body mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"access_token":"openai-access-token","token_type":"Bearer","expires_in":3600}`)) @@ -86,11 +86,13 @@ func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ Organization: "org-123", - TokenExchange: aigv1b1.BackendSecurityPolicyTokenExchange{ - TokenURL: exchangeServer.URL, - SubjectToken: aigv1b1.BackendSecurityPolicySubjectToken{ + WorkloadIdentityFederationConfig: aigv1b1.OpenAIWorkloadIdentityFederationConfig{ + IdentityProviderID: "idp_123", + ServiceAccountID: "sa_456", + TokenURL: exchangeServer.URL, + SubjectToken: aigv1b1.OpenAISubjectToken{ SPIFFEJWTSVID: &aigv1b1.BackendSecurityPolicySPIFFEJWTSVID{ - Audience: "https://auth.example.com", + Audience: "https://api.openai.com", SocketPath: socketAddr, }, }, @@ -116,12 +118,14 @@ func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { mu.Lock() defer mu.Unlock() - require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", gotSubjectTokType) + require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", gotBody["subject_token_type"]) + require.Equal(t, "idp_123", gotBody["identity_provider_id"]) + require.Equal(t, "sa_456", gotBody["service_account_id"]) claims := jwt.RegisteredClaims{} - _, err = jwt.ParseWithClaims(gotSubjectToken, &claims, func(*jwt.Token) (any, error) { return &workloadAPI.key().PublicKey, nil }) + _, err = jwt.ParseWithClaims(gotBody["subject_token"], &claims, func(*jwt.Token) (any, error) { return &workloadAPI.key().PublicKey, nil }) require.NoError(t, err) require.Equal(t, testControllerSPIFFEID, claims.Subject) - require.Equal(t, jwt.ClaimStrings{"https://auth.example.com"}, claims.Audience) + require.Equal(t, jwt.ClaimStrings{"https://api.openai.com"}, claims.Audience) } // fakeWorkloadAPI issues JWT-SVIDs for testControllerSPIFFEID. diff --git a/tests/crdcel/main_test.go b/tests/crdcel/main_test.go index 84d49a110e..3d4be4da77 100644 --- a/tests/crdcel/main_test.go +++ b/tests/crdcel/main_test.go @@ -249,13 +249,17 @@ func TestBackendSecurityPolicies(t *testing.T) { name: "openai_multiple_subject_tokens.yaml", expErr: "Exactly one of oidcExchangeToken or spiffeJWTSVID must be specified", }, + { + name: "openai_missing_identity_provider_id.yaml", + expErr: "spec.openAICredentials.workloadIdentityFederationConfig.identityProviderID in body should be at least 1 chars long", + }, { name: "openai_invalid_token_url.yaml", expErr: "tokenURL must be a valid URL", }, { name: "openai_invalid_socket_path.yaml", - expErr: "spec.openAICredentials.tokenExchange.subjectToken.spiffeJWTSVID.socketPath in body should match", + expErr: "spec.openAICredentials.workloadIdentityFederationConfig.subjectToken.spiffeJWTSVID.socketPath in body should match", }, { name: "openai_with_apikey.yaml", @@ -296,19 +300,19 @@ func TestBackendSecurityPolicies(t *testing.T) { } } -func TestBackendSecurityPolicySubjectTokenTypeDefault(t *testing.T) { +func TestBackendSecurityPolicyOpenAITokenURLDefault(t *testing.T) { c, _, _ := testsinternal.NewEnvTest(t) ctx := t.Context() - data, err := testdata.ReadFile("testdata/backendsecuritypolicies/openai_spiffe.yaml") + data, err := testdata.ReadFile("testdata/backendsecuritypolicies/openai_oidc.yaml") require.NoError(t, err) bsp := &aigv1b1.BackendSecurityPolicy{} require.NoError(t, yaml.UnmarshalStrict(data, bsp)) require.NoError(t, c.Create(ctx, bsp)) t.Cleanup(func() { _ = c.Delete(ctx, bsp) }) - require.Equal(t, "urn:ietf:params:oauth:token-type:jwt", - bsp.Spec.OpenAICredentials.TokenExchange.SubjectTokenType) + require.Equal(t, "https://auth.openai.com/oauth/token", + bsp.Spec.OpenAICredentials.WorkloadIdentityFederationConfig.TokenURL) } func TestMCPRoutes(t *testing.T) { diff --git a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml index f24bf4dbed..108014c7d2 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/apikey_with_openai_credentials.yaml @@ -14,8 +14,9 @@ spec: secretRef: name: placeholder openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml index db5dd4a6cf..a01a09972b 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_socket_path.yaml @@ -11,9 +11,10 @@ metadata: spec: type: OpenAICredentials openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com socketPath: /run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml index 77f3a5888e..bad4b6b61f 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml @@ -11,8 +11,10 @@ metadata: spec: type: OpenAICredentials openAICredentials: - tokenExchange: + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder tokenURL: not a url subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_identity_provider_id.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_identity_provider_id.yaml new file mode 100644 index 0000000000..ea5226cd37 --- /dev/null +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_identity_provider_id.yaml @@ -0,0 +1,18 @@ +# Copyright Envoy AI Gateway Authors +# SPDX-License-Identifier: Apache-2.0 +# The full text of the Apache license is available in the LICENSE file at +# the root of the repo. + +apiVersion: aigateway.envoyproxy.io/v1beta1 +kind: BackendSecurityPolicy +metadata: + name: openai-missing-identity-provider-id-policy + namespace: default +spec: + type: OpenAICredentials + openAICredentials: + workloadIdentityFederationConfig: + serviceAccountID: sa_placeholder + subjectToken: + spiffeJWTSVID: + audience: https://api.openai.com diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml index b99786f1de..386b82ace9 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_missing_subject_token.yaml @@ -11,6 +11,7 @@ metadata: spec: type: OpenAICredentials openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: {} diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml index 3fce3c3c3f..7b96a7db26 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_multiple_subject_tokens.yaml @@ -11,11 +11,12 @@ metadata: spec: type: OpenAICredentials openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com oidcExchangeToken: oidc: provider: diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml index bf39686f00..8bfdd441be 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_oidc.yaml @@ -11,9 +11,9 @@ metadata: spec: type: OpenAICredentials openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token - subjectTokenType: urn:ietf:params:oauth:token-type:access_token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: oidcExchangeToken: oidc: diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml index 9ac5c502bb..a546c8c68a 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_spiffe.yaml @@ -13,12 +13,11 @@ spec: openAICredentials: organization: org-placeholder project: proj_placeholder - tokenExchange: - tokenURL: https://auth.example.com/oauth/token - audience: https://api.openai.com - scopes: - - api.model.read + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder + tokenURL: https://auth.openai.com/oauth/token subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com socketPath: unix:///run/spire/sockets/agent.sock diff --git a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml index 31f4b61501..a0583a06af 100644 --- a/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml +++ b/tests/crdcel/testdata/backendsecuritypolicies/openai_with_apikey.yaml @@ -14,8 +14,9 @@ spec: secretRef: name: placeholder openAICredentials: - tokenExchange: - tokenURL: https://auth.example.com/oauth/token + workloadIdentityFederationConfig: + identityProviderID: idp_placeholder + serviceAccountID: sa_placeholder subjectToken: spiffeJWTSVID: - audience: https://auth.example.com + audience: https://api.openai.com From e8248e784693637355cea34ac6a35ba75627e189 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Mon, 12 Oct 2026 20:06:23 -0400 Subject: [PATCH 12/17] controller: add AI_GATEWAY_OPENAI_PROXY_URL Route OpenAI Workload Identity Federation token exchange requests through the proxy in AI_GATEWAY_OPENAI_PROXY_URL when set, like the existing AWS, Azure and GCP proxy variables. Without it, the default transport's proxy settings (HTTPS_PROXY) still apply. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- ...openai_workload_identity_token_provider.go | 25 ++++++++++++++++- ...i_workload_identity_token_provider_test.go | 27 +++++++++++++++++++ manifests/charts/ai-gateway-helm/values.yaml | 3 +++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go index 37f08b3456..747b957d21 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -12,6 +12,8 @@ import ( "fmt" "io" "net/http" + "net/url" + "os" "time" "github.com/envoyproxy/ai-gateway/internal/json" @@ -24,6 +26,9 @@ const ( jwtTokenType = "urn:ietf:params:oauth:token-type:jwt" // #nosec G101 // maxErrorBodyLength caps how much of an unrecognized error response is included in errors. maxErrorBodyLength = 512 + // OpenAIProxyEnvVar is the environment variable name for configuring the proxy URL of OpenAI token + // exchange requests. + OpenAIProxyEnvVar = "AI_GATEWAY_OPENAI_PROXY_URL" ) // OpenAIWorkloadIdentityConfig holds the parameters of an OpenAI Workload Identity Federation token exchange. @@ -84,13 +89,31 @@ func NewOpenAIWorkloadIdentityTokenProvider(config OpenAIWorkloadIdentityConfig, if subjectTokens == nil { return nil, errors.New("subject token provider must not be nil") } + transport, err := newOpenAITransport() + if err != nil { + return nil, err + } return &openAIWorkloadIdentityTokenProvider{ config: config, subjectTokens: subjectTokens, - httpClient: &http.Client{Timeout: time.Minute}, + httpClient: &http.Client{Timeout: time.Minute, Transport: transport}, }, nil } +// newOpenAITransport returns an http.RoundTripper that uses the proxy from AI_GATEWAY_OPENAI_PROXY_URL +// if set, and otherwise the default transport's proxy settings. +func newOpenAITransport() (http.RoundTripper, error) { + t := http.DefaultTransport.(*http.Transport).Clone() + if proxyURLStr := os.Getenv(OpenAIProxyEnvVar); proxyURLStr != "" { + proxyURL, err := url.Parse(proxyURLStr) + if err != nil { + return nil, fmt.Errorf("invalid %s: %w", OpenAIProxyEnvVar, err) + } + t.Proxy = http.ProxyURL(proxyURL) + } + return t, nil +} + // GetToken implements TokenProvider.GetToken method to retrieve an OpenAI access token and its expiration time. // // When the response has no expires_in, the access token is assumed to expire with the subject token. diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go index bd6e9c5ca1..af8db361b8 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go @@ -73,6 +73,33 @@ func TestNewOpenAIWorkloadIdentityTokenProvider(t *testing.T) { } } +func TestNewOpenAIWorkloadIdentityTokenProvider_InvalidProxyURL(t *testing.T) { + t.Setenv(OpenAIProxyEnvVar, "://bad") + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig("https://auth.openai.com/oauth/token"), + NewMockTokenProvider("subject", time.Now(), nil)) + require.ErrorContains(t, err, "invalid AI_GATEWAY_OPENAI_PROXY_URL") + require.Nil(t, provider) +} + +func TestOpenAIWorkloadIdentityTokenProvider_GetToken_Proxy(t *testing.T) { + var gotHost string + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // A forward proxy receives the absolute request URL of the token endpoint. + gotHost = r.URL.Host + _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) + })) + defer proxy.Close() + t.Setenv(OpenAIProxyEnvVar, proxy.URL) + + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig("http://auth.openai.invalid/oauth/token"), + NewMockTokenProvider("subject", time.Now().Add(time.Hour), nil)) + require.NoError(t, err) + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, "exchanged", token.Token) + require.Equal(t, "auth.openai.invalid", gotHost) +} + func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { subjectExpiry := time.Now().Add(time.Hour).Truncate(time.Second) diff --git a/manifests/charts/ai-gateway-helm/values.yaml b/manifests/charts/ai-gateway-helm/values.yaml index 966fe102d7..5e47ce1ccd 100644 --- a/manifests/charts/ai-gateway-helm/values.yaml +++ b/manifests/charts/ai-gateway-helm/values.yaml @@ -203,6 +203,9 @@ controller: ## # Enabled by default for troubleshooting purposes given the impact is negligible when not in use. ## - name: DISABLE_PPROF ## value: "true" + ## # OpenAI Workload Identity Federation token exchange requests will use this proxy if set. + ## - name: AI_GATEWAY_OPENAI_PROXY_URL + ## value: "http://proxy.example.com:8080" extraEnvVars: [] # Example of volumes From 7a5aad77620f611f1655b9b8fc94b0a3ed69f8b7 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Thu, 22 Oct 2026 11:30:21 -0400 Subject: [PATCH 13/17] address comments part 1 Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 8 ++++-- examples/basic/openai-spiffe.yaml | 5 ++-- internal/backendauth/openai.go | 15 ++++++++--- ...openai_workload_identity_token_provider.go | 13 +++++++--- ...i_workload_identity_token_provider_test.go | 25 ++++++++++++++++--- ...envoyproxy.io_backendsecuritypolicies.yaml | 8 ++++-- site/docs/api/api.mdx | 2 +- 7 files changed, 58 insertions(+), 18 deletions(-) diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 4bf4e28435..061e133440 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -424,8 +424,12 @@ type OpenAISubjectToken struct { OIDCExchangeToken *BackendSecurityPolicyOIDC `json:"oidcExchangeToken,omitempty"` // SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API. - // The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as - // the controller; only trusted cluster admins should be allowed to create such policies. + // + // The SPIFFE identity is owned by the controller, not by the policy: the JWT-SVID is issued for the + // controller's SPIFFE ID. This differs from OIDCExchangeToken, where the policy supplies its own + // credentials. Every policy using this source authenticates as the controller and can obtain access + // tokens for any OpenAI service account mapped to the controller's SPIFFE ID, so only trusted cluster + // admins should be allowed to create such policies. // // +optional SPIFFEJWTSVID *BackendSecurityPolicySPIFFEJWTSVID `json:"spiffeJWTSVID,omitempty"` diff --git a/examples/basic/openai-spiffe.yaml b/examples/basic/openai-spiffe.yaml index 6a52fc5ecb..ac677e2dad 100644 --- a/examples/basic/openai-spiffe.yaml +++ b/examples/basic/openai-spiffe.yaml @@ -55,8 +55,9 @@ spec: # configured in your OpenAI organization: # https://developers.openai.com/api/reference/workload-identity-federation # - # Policies run as the controller's SPIFFE identity, so only trusted cluster admins should be - # allowed to create them. + # The SPIFFE identity is owned by the controller, not by this policy (unlike oidcExchangeToken, where + # the policy supplies its own credentials). Every policy using spiffeJWTSVID authenticates as the + # controller, so only trusted cluster admins should be allowed to create them. openAICredentials: organization: OPENAI_ORGANIZATION_ID # Optional. Replace with your OpenAI organization ID, or remove. project: OPENAI_PROJECT_ID # Optional. Replace with your OpenAI project ID, or remove. diff --git a/internal/backendauth/openai.go b/internal/backendauth/openai.go index 4ab319cc09..658fa67cbf 100644 --- a/internal/backendauth/openai.go +++ b/internal/backendauth/openai.go @@ -14,6 +14,13 @@ import ( "github.com/envoyproxy/ai-gateway/internal/internalapi" ) +const ( + // openAIOrganizationHeader is the header that selects the OpenAI organization of a request. + openAIOrganizationHeader = "OpenAI-Organization" + // openAIProjectHeader is the header that selects the OpenAI project of a request. + openAIProjectHeader = "OpenAI-Project" +) + type openAIHandler struct { accessToken string applyFn applyCredentialFn @@ -42,12 +49,12 @@ func makeOpenAIApplyFn(organization, project string) applyCredentialFn { requestHeaders["Authorization"] = v headers := []internalapi.Header{{"Authorization", v}} if organization != "" { - requestHeaders["OpenAI-Organization"] = organization - headers = append(headers, internalapi.Header{"OpenAI-Organization", organization}) + requestHeaders[openAIOrganizationHeader] = organization + headers = append(headers, internalapi.Header{openAIOrganizationHeader, organization}) } if project != "" { - requestHeaders["OpenAI-Project"] = project - headers = append(headers, internalapi.Header{"OpenAI-Project", project}) + requestHeaders[openAIProjectHeader] = project + headers = append(headers, internalapi.Header{openAIProjectHeader, project}) } return headers, nil } diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go index 747b957d21..9cc82d77d9 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -61,7 +61,10 @@ type openAITokenExchangeRequest struct { // openAITokenExchangeResponse is the successful response of an OpenAI token exchange. type openAITokenExchangeResponse struct { AccessToken string `json:"access_token"` - ExpiresIn int64 `json:"expires_in"` + // ExpiresIn is the lifetime of the access token in seconds. + ExpiresIn int64 `json:"expires_in"` + // ExpiresAt is the expiration time of the access token in Unix seconds. + ExpiresAt int64 `json:"expires_at"` } // openAIErrorResponse is the error response of the OpenAI API. @@ -116,7 +119,8 @@ func newOpenAITransport() (http.RoundTripper, error) { // GetToken implements TokenProvider.GetToken method to retrieve an OpenAI access token and its expiration time. // -// When the response has no expires_in, the access token is assumed to expire with the subject token. +// The expiration time is taken from expires_at, then from expires_in. When the response has neither, the +// access token is assumed to expire with the subject token. func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (TokenExpiry, error) { subject, err := t.subjectTokens.GetToken(ctx) if err != nil { @@ -171,7 +175,10 @@ func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (Tok return TokenExpiry{}, errors.New("token exchange response has no access_token") } expiresAt := subject.ExpiresAt - if tokenResp.ExpiresIn > 0 { + switch { + case tokenResp.ExpiresAt > 0: + expiresAt = time.Unix(tokenResp.ExpiresAt, 0) + case tokenResp.ExpiresIn > 0: expiresAt = issuedAt.Add(time.Duration(tokenResp.ExpiresIn) * time.Second) } return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go index af8db361b8..c2d44abd05 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go @@ -7,6 +7,7 @@ package tokenprovider import ( "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -104,6 +105,7 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { subjectExpiry := time.Now().Add(time.Hour).Truncate(time.Second) t.Run("success", func(t *testing.T) { + expiresAt := time.Now().Add(30 * time.Minute).Truncate(time.Second) var gotBody map[string]string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) @@ -112,7 +114,7 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { require.NoError(t, err) require.NoError(t, json.Unmarshal(body, &gotBody)) w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"access_token":"exchanged","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":600,"expires_at":1789045200}`)) + _, _ = fmt.Fprintf(w, `{"access_token":"exchanged","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":600,"expires_at":%d}`, expiresAt.Unix()) })) defer server.Close() @@ -120,11 +122,10 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { NewMockTokenProvider("subject", subjectExpiry, nil)) require.NoError(t, err) - before := time.Now() token, err := provider.GetToken(t.Context()) require.NoError(t, err) require.Equal(t, "exchanged", token.Token) - require.WithinRange(t, token.ExpiresAt, before.Add(600*time.Second), time.Now().Add(600*time.Second)) + require.True(t, expiresAt.Equal(token.ExpiresAt), "expected %v, got %v", expiresAt, token.ExpiresAt) require.Equal(t, map[string]string{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "subject", @@ -134,7 +135,23 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { }, gotBody) }) - t.Run("no expires_in uses subject token expiry", func(t *testing.T) { + t.Run("no expires_at uses expires_in", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) + })) + defer server.Close() + + provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), + NewMockTokenProvider("subject", subjectExpiry, nil)) + require.NoError(t, err) + before := time.Now() + token, err := provider.GetToken(t.Context()) + require.NoError(t, err) + require.Equal(t, "exchanged", token.Token) + require.WithinRange(t, token.ExpiresAt, before.Add(600*time.Second), time.Now().Add(600*time.Second)) + }) + + t.Run("no expires_at or expires_in uses subject token expiry", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"access_token":"exchanged"}`)) })) diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index cb7943c62e..744d83de34 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -14864,8 +14864,12 @@ spec: spiffeJWTSVID: description: |- SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API. - The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as - the controller; only trusted cluster admins should be allowed to create such policies. + + The SPIFFE identity is owned by the controller, not by the policy: the JWT-SVID is issued for the + controller's SPIFFE ID. This differs from OIDCExchangeToken, where the policy supplies its own + credentials. Every policy using this source authenticates as the controller and can obtain access + tokens for any OpenAI service account mapped to the controller's SPIFFE ID, so only trusted cluster + admins should be allowed to create such policies. properties: audience: description: |- diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index dda25dd1f2..660d15d7c2 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -5330,7 +5330,7 @@ Exactly one source must be configured. name="spiffeJWTSVID" type="[BackendSecurityPolicySPIFFEJWTSVID](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyspiffejwtsvid)" required="false" - description="SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API.
The JWT-SVID is issued for the controller's SPIFFE ID, so every policy using this source acts as
the controller; only trusted cluster admins should be allowed to create such policies." + description="SPIFFEJWTSVID obtains the subject token as a JWT-SVID from the SPIFFE Workload API.
The SPIFFE identity is owned by the controller, not by the policy: the JWT-SVID is issued for the
controller's SPIFFE ID. This differs from OIDCExchangeToken, where the policy supplies its own
credentials. Every policy using this source authenticates as the controller and can obtain access
tokens for any OpenAI service account mapped to the controller's SPIFFE ID, so only trusted cluster
admins should be allowed to create such policies." /> From dfebb2b8495af30d13434e0d1feae6664292caef Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Mon, 26 Oct 2026 11:20:38 -0400 Subject: [PATCH 14/17] add log Signed-off-by: Junfei Liu --- .../openai_workload_identity_token_provider.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go index 9cc82d77d9..757a6084cf 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -16,6 +16,8 @@ import ( "os" "time" + "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/envoyproxy/ai-gateway/internal/json" ) @@ -174,12 +176,19 @@ func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (Tok if tokenResp.AccessToken == "" { return TokenExpiry{}, errors.New("token exchange response has no access_token") } - expiresAt := subject.ExpiresAt + expiresAt, expirySource := subject.ExpiresAt, "subject_token" switch { case tokenResp.ExpiresAt > 0: - expiresAt = time.Unix(tokenResp.ExpiresAt, 0) + expiresAt, expirySource = time.Unix(tokenResp.ExpiresAt, 0), "expires_at" case tokenResp.ExpiresIn > 0: - expiresAt = issuedAt.Add(time.Duration(tokenResp.ExpiresIn) * time.Second) + expiresAt, expirySource = issuedAt.Add(time.Duration(tokenResp.ExpiresIn)*time.Second), "expires_in" } + log.FromContext(ctx).Info("OpenAI token exchange expiry", + "expirySource", expirySource, + "subjectExpiresAt", subject.ExpiresAt, + "issuedAt", issuedAt, + "respExpiresAt", tokenResp.ExpiresAt, + "respExpiresIn", tokenResp.ExpiresIn, + "expiresAt", expiresAt) return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil } From f2b86b2ba843a3202e964dfab672f1a8882fe484 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Mon, 26 Oct 2026 17:12:08 -0400 Subject: [PATCH 15/17] after review part2 Signed-off-by: Junfei Liu --- api/v1beta1/backendsecurity_policy.go | 11 ++-------- .../controller/backend_security_policy.go | 2 +- .../backend_security_policy_test.go | 7 +++++-- ...openai_workload_identity_token_provider.go | 5 +++++ ...envoyproxy.io_backendsecuritypolicies.yaml | 7 ------- site/docs/api/api.mdx | 10 ++-------- tests/controller/openai_credentials_test.go | 5 ++++- tests/crdcel/main_test.go | 19 ------------------ .../openai_invalid_token_url.yaml | 20 ------------------- .../openai_spiffe.yaml | 1 - 10 files changed, 19 insertions(+), 68 deletions(-) delete mode 100644 tests/crdcel/testdata/backendsecuritypolicies/openai_invalid_token_url.yaml diff --git a/api/v1beta1/backendsecurity_policy.go b/api/v1beta1/backendsecurity_policy.go index 061e133440..014dab888c 100644 --- a/api/v1beta1/backendsecurity_policy.go +++ b/api/v1beta1/backendsecurity_policy.go @@ -377,8 +377,8 @@ type BackendSecurityPolicyOpenAICredentials struct { } // OpenAIWorkloadIdentityFederationConfig specifies how the controller obtains an OpenAI access token via -// OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at TokenURL for -// a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. +// OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at +// https://auth.openai.com/oauth/token for a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. // // The identity provider and service account mapping must be configured in the OpenAI organization, with the // issuer and audience of the subject token. @@ -399,13 +399,6 @@ type OpenAIWorkloadIdentityFederationConfig struct { // +kubebuilder:validation:MinLength=1 ServiceAccountID string `json:"serviceAccountID"` - // TokenURL is the OpenAI token exchange endpoint. - // - // +optional - // +kubebuilder:default="https://auth.openai.com/oauth/token" - // +kubebuilder:validation:XValidation:rule="isURL(self)",message="tokenURL must be a valid URL" - TokenURL string `json:"tokenURL,omitempty"` - // SubjectToken specifies where the subject token is obtained from. The subject token must be a JWT. // // +kubebuilder:validation:Required diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go index ee3b315573..8e1d83ff25 100644 --- a/internal/controller/backend_security_policy.go +++ b/internal/controller/backend_security_policy.go @@ -372,7 +372,7 @@ func (c *BackendSecurityPolicyController) newOpenAITokenProvider(ctx context.Con return nil, fmt.Errorf("failed to initialize subject token provider: %w", err) } return tokenprovider.NewOpenAIWorkloadIdentityTokenProvider(tokenprovider.OpenAIWorkloadIdentityConfig{ - TokenURL: wif.TokenURL, + TokenURL: tokenprovider.OpenAITokenExchangeURL, IdentityProviderID: wif.IdentityProviderID, ServiceAccountID: wif.ServiceAccountID, }, subjectTokens) diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index 83a6028465..cc5f3aacd0 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -40,6 +40,7 @@ import ( aigv1b1 "github.com/envoyproxy/ai-gateway/api/v1beta1" "github.com/envoyproxy/ai-gateway/internal/controller/rotators" + "github.com/envoyproxy/ai-gateway/internal/controller/tokenprovider" "github.com/envoyproxy/ai-gateway/internal/json" internaltesting "github.com/envoyproxy/ai-gateway/internal/testing" ) @@ -884,6 +885,10 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC _, _ = w.Write([]byte(`{"access_token": "openai-access-token", "token_type": "Bearer", "expires_in": 3600}`)) }) + origTokenURL := tokenprovider.OpenAITokenExchangeURL + tokenprovider.OpenAITokenExchangeURL = server.URL + "/exchange" + t.Cleanup(func() { tokenprovider.OpenAITokenExchangeURL = origTokenURL }) + eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() cl := fake.NewClientBuilder().WithScheme(Scheme).Build() c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) @@ -899,7 +904,6 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ WorkloadIdentityFederationConfig: aigv1b1.OpenAIWorkloadIdentityFederationConfig{ - TokenURL: server.URL + "/exchange", IdentityProviderID: "idp_123", ServiceAccountID: "sa_456", SubjectToken: aigv1b1.OpenAISubjectToken{ @@ -946,7 +950,6 @@ func TestBackendSecurityPolicyController_Reconcile_OpenAICredentials_SPIFFEUnava Type: aigv1b1.BackendSecurityPolicyTypeOpenAICredentials, OpenAICredentials: &aigv1b1.BackendSecurityPolicyOpenAICredentials{ WorkloadIdentityFederationConfig: aigv1b1.OpenAIWorkloadIdentityFederationConfig{ - TokenURL: "https://auth.openai.com/oauth/token", IdentityProviderID: "idp_123", ServiceAccountID: "sa_456", SubjectToken: aigv1b1.OpenAISubjectToken{ diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go index 757a6084cf..ce9e2585de 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -33,6 +33,11 @@ const ( OpenAIProxyEnvVar = "AI_GATEWAY_OPENAI_PROXY_URL" ) +// OpenAITokenExchangeURL is the OpenAI token exchange endpoint. It is not configurable through the API so that +// policies cannot direct the controller to send subject tokens to arbitrary URLs. It is a variable only so +// tests can point it at a local server. +var OpenAITokenExchangeURL = "https://auth.openai.com/oauth/token" + // OpenAIWorkloadIdentityConfig holds the parameters of an OpenAI Workload Identity Federation token exchange. type OpenAIWorkloadIdentityConfig struct { // TokenURL is the OpenAI token exchange endpoint. diff --git a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml index 744d83de34..360b89fe7c 100644 --- a/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml +++ b/manifests/charts/ai-gateway-crds-helm/templates/aigateway.envoyproxy.io_backendsecuritypolicies.yaml @@ -14893,13 +14893,6 @@ spec: must be specified rule: '[has(self.oidcExchangeToken), has(self.spiffeJWTSVID)].filter(x, x).size() == 1' - tokenURL: - default: https://auth.openai.com/oauth/token - description: TokenURL is the OpenAI token exchange endpoint. - type: string - x-kubernetes-validations: - - message: tokenURL must be a valid URL - rule: isURL(self) required: - identityProviderID - serviceAccountID diff --git a/site/docs/api/api.mdx b/site/docs/api/api.mdx index 660d15d7c2..a40cd95551 100644 --- a/site/docs/api/api.mdx +++ b/site/docs/api/api.mdx @@ -5342,8 +5342,8 @@ Exactly one source must be configured. - [BackendSecurityPolicyOpenAICredentials](#github-com-envoyproxy-ai-gateway-api-v1beta1-backendsecuritypolicyopenaicredentials) OpenAIWorkloadIdentityFederationConfig specifies how the controller obtains an OpenAI access token via -OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at TokenURL for -a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. +OpenAI Workload Identity Federation. The controller obtains a subject token, exchanges it at +https://auth.openai.com/oauth/token for a short-lived OpenAI access token, and stores the access token in a secret, rotating it before it expires. The identity provider and service account mapping must be configured in the OpenAI organization, with the issuer and audience of the subject token. @@ -5364,12 +5364,6 @@ https://developers.openai.com/api/reference/workload-identity-federation type="string" required="true" description="ServiceAccountID is the ID of the OpenAI service account the access token is issued for. It must match a
service account mapping of the identity provider." -/> Date: Mon, 26 Oct 2026 19:04:55 -0400 Subject: [PATCH 16/17] remove log Signed-off-by: Junfei Liu --- .../backend_security_policy_test.go | 2 +- ...openai_workload_identity_token_provider.go | 27 +++---------- ...i_workload_identity_token_provider_test.go | 40 ++++--------------- tests/controller/openai_credentials_test.go | 3 +- 4 files changed, 16 insertions(+), 56 deletions(-) diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index cc5f3aacd0..7214fd2c6c 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -882,7 +882,7 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC mux.HandleFunc("/exchange", func(w http.ResponseWriter, r *http.Request) { require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody)) w.Header().Add("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"access_token": "openai-access-token", "token_type": "Bearer", "expires_in": 3600}`)) + _, _ = fmt.Fprintf(w, `{"access_token": "openai-access-token", "token_type": "Bearer", "expires_in": 3600, "expires_at": %d}`, time.Now().Add(time.Hour).Unix()) }) origTokenURL := tokenprovider.OpenAITokenExchangeURL diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go index ce9e2585de..c96c12b1f1 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider.go @@ -16,8 +16,6 @@ import ( "os" "time" - "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/envoyproxy/ai-gateway/internal/json" ) @@ -36,7 +34,7 @@ const ( // OpenAITokenExchangeURL is the OpenAI token exchange endpoint. It is not configurable through the API so that // policies cannot direct the controller to send subject tokens to arbitrary URLs. It is a variable only so // tests can point it at a local server. -var OpenAITokenExchangeURL = "https://auth.openai.com/oauth/token" +var OpenAITokenExchangeURL = "https://auth.openai.com/oauth/token" // #nosec G101 // OpenAIWorkloadIdentityConfig holds the parameters of an OpenAI Workload Identity Federation token exchange. type OpenAIWorkloadIdentityConfig struct { @@ -68,8 +66,6 @@ type openAITokenExchangeRequest struct { // openAITokenExchangeResponse is the successful response of an OpenAI token exchange. type openAITokenExchangeResponse struct { AccessToken string `json:"access_token"` - // ExpiresIn is the lifetime of the access token in seconds. - ExpiresIn int64 `json:"expires_in"` // ExpiresAt is the expiration time of the access token in Unix seconds. ExpiresAt int64 `json:"expires_at"` } @@ -126,8 +122,7 @@ func newOpenAITransport() (http.RoundTripper, error) { // GetToken implements TokenProvider.GetToken method to retrieve an OpenAI access token and its expiration time. // -// The expiration time is taken from expires_at, then from expires_in. When the response has neither, the -// access token is assumed to expire with the subject token. +// The expiration time is taken from expires_at. func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (TokenExpiry, error) { subject, err := t.subjectTokens.GetToken(ctx) if err != nil { @@ -151,7 +146,6 @@ func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (Tok req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") - issuedAt := time.Now() resp, err := t.httpClient.Do(req) if err != nil { return TokenExpiry{}, fmt.Errorf("failed to send token exchange request: %w", err) @@ -181,19 +175,8 @@ func (t *openAIWorkloadIdentityTokenProvider) GetToken(ctx context.Context) (Tok if tokenResp.AccessToken == "" { return TokenExpiry{}, errors.New("token exchange response has no access_token") } - expiresAt, expirySource := subject.ExpiresAt, "subject_token" - switch { - case tokenResp.ExpiresAt > 0: - expiresAt, expirySource = time.Unix(tokenResp.ExpiresAt, 0), "expires_at" - case tokenResp.ExpiresIn > 0: - expiresAt, expirySource = issuedAt.Add(time.Duration(tokenResp.ExpiresIn)*time.Second), "expires_in" + if tokenResp.ExpiresAt <= 0 { + return TokenExpiry{}, errors.New("token exchange response has no expires_at") } - log.FromContext(ctx).Info("OpenAI token exchange expiry", - "expirySource", expirySource, - "subjectExpiresAt", subject.ExpiresAt, - "issuedAt", issuedAt, - "respExpiresAt", tokenResp.ExpiresAt, - "respExpiresIn", tokenResp.ExpiresIn, - "expiresAt", expiresAt) - return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: expiresAt}, nil + return TokenExpiry{Token: tokenResp.AccessToken, ExpiresAt: time.Unix(tokenResp.ExpiresAt, 0)}, nil } diff --git a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go index c2d44abd05..56330d132c 100644 --- a/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go +++ b/internal/controller/tokenprovider/openai_workload_identity_token_provider_test.go @@ -87,7 +87,7 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken_Proxy(t *testing.T) { proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // A forward proxy receives the absolute request URL of the token endpoint. gotHost = r.URL.Host - _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) + _, _ = fmt.Fprintf(w, `{"access_token":"exchanged","expires_in":600,"expires_at":%d}`, time.Now().Add(10*time.Minute).Unix()) })) defer proxy.Close() t.Setenv(OpenAIProxyEnvVar, proxy.URL) @@ -135,36 +135,6 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { }, gotBody) }) - t.Run("no expires_at uses expires_in", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(`{"access_token":"exchanged","expires_in":600}`)) - })) - defer server.Close() - - provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), - NewMockTokenProvider("subject", subjectExpiry, nil)) - require.NoError(t, err) - before := time.Now() - token, err := provider.GetToken(t.Context()) - require.NoError(t, err) - require.Equal(t, "exchanged", token.Token) - require.WithinRange(t, token.ExpiresAt, before.Add(600*time.Second), time.Now().Add(600*time.Second)) - }) - - t.Run("no expires_at or expires_in uses subject token expiry", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(`{"access_token":"exchanged"}`)) - })) - defer server.Close() - - provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig(server.URL), - NewMockTokenProvider("subject", subjectExpiry, nil)) - require.NoError(t, err) - token, err := provider.GetToken(t.Context()) - require.NoError(t, err) - require.Equal(t, TokenExpiry{Token: "exchanged", ExpiresAt: subjectExpiry}, token) - }) - t.Run("subject token error", func(t *testing.T) { provider, err := NewOpenAIWorkloadIdentityTokenProvider(testOpenAIWorkloadIdentityConfig("http://127.0.0.1:0"), NewMockTokenProvider("", time.Time{}, errors.New("workload API unavailable"))) @@ -206,9 +176,15 @@ func TestOpenAIWorkloadIdentityTokenProvider_GetToken(t *testing.T) { { name: "missing access token", status: http.StatusOK, - body: `{"expires_in":600}`, + body: `{"expires_in":600,"expires_at":1793053844}`, expErr: "token exchange response has no access_token", }, + { + name: "missing expires_at", + status: http.StatusOK, + body: `{"access_token":"exchanged","expires_in":600}`, + expErr: "token exchange response has no expires_at", + }, } { t.Run(tc.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { diff --git a/tests/controller/openai_credentials_test.go b/tests/controller/openai_credentials_test.go index cad7b0aa85..2a76aae012 100644 --- a/tests/controller/openai_credentials_test.go +++ b/tests/controller/openai_credentials_test.go @@ -10,6 +10,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "fmt" "net" "net/http" "net/http/httptest" @@ -65,7 +66,7 @@ func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { gotBody = body mu.Unlock() w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"access_token":"openai-access-token","token_type":"Bearer","expires_in":3600}`)) + _, _ = fmt.Fprintf(w, `{"access_token":"openai-access-token","token_type":"Bearer","expires_in":3600,"expires_at":%d}`, time.Now().Add(time.Hour).Unix()) })) t.Cleanup(exchangeServer.Close) origTokenURL := tokenprovider.OpenAITokenExchangeURL From 63a7a6f72a4a21450c79a4665916195aaa3d5ee5 Mon Sep 17 00:00:00 2001 From: Junfei Liu Date: Mon, 26 Oct 2026 19:55:12 -0400 Subject: [PATCH 17/17] controller: use configurable preRotationWindow Adapt OpenAI rotator wiring to #2886, which made the pre-rotation window a controller field. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Junfei Liu --- internal/controller/backend_security_policy.go | 2 +- internal/controller/backend_security_policy_test.go | 6 +++--- tests/controller/openai_credentials_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controller/backend_security_policy.go b/internal/controller/backend_security_policy.go index 8e1d83ff25..7f5d038b0e 100644 --- a/internal/controller/backend_security_policy.go +++ b/internal/controller/backend_security_policy.go @@ -282,7 +282,7 @@ func (c *BackendSecurityPolicyController) rotateCredential(ctx context.Context, if err != nil { return ctrl.Result{}, err } - rotator, err = rotators.NewOpenAITokenRotator(c.client, c.kube, c.logger, bsp.Namespace, bsp.Name, preRotationWindow, provider) + rotator, err = rotators.NewOpenAITokenRotator(c.client, c.kube, c.logger, bsp.Namespace, bsp.Name, c.preRotationWindow, provider) if err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/backend_security_policy_test.go b/internal/controller/backend_security_policy_test.go index 7214fd2c6c..993b9ec5ca 100644 --- a/internal/controller/backend_security_policy_test.go +++ b/internal/controller/backend_security_policy_test.go @@ -891,7 +891,7 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() cl := fake.NewClientBuilder().WithScheme(Scheme).Build() - c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil, DefaultPreRotationWindow) const bspNamespace = "default" require.NoError(t, cl.Create(t.Context(), &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: "oidc-client-secret", Namespace: bspNamespace}, @@ -942,7 +942,7 @@ func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_OIDC func TestBackendSecurityPolicyController_Reconcile_OpenAICredentials_SPIFFEUnavailable(t *testing.T) { eventCh := internaltesting.NewControllerEventChan[*aigv1b1.AIServiceBackend]() cl := requireNewFakeClientWithIndexes(t) - c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil) + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, eventCh.Ch, nil, DefaultPreRotationWindow) bsp := &aigv1b1.BackendSecurityPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: "default"}, @@ -977,7 +977,7 @@ func TestBackendSecurityPolicyController_Reconcile_OpenAICredentials_SPIFFEUnava func TestBackendSecurityPolicyController_RotateCredential_OpenAICredentials_NoSubjectToken(t *testing.T) { cl := fake.NewClientBuilder().WithScheme(Scheme).Build() - c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, nil, nil) + c := NewBackendSecurityPolicyController(cl, fake2.NewClientset(), ctrl.Log, nil, nil, DefaultPreRotationWindow) bsp := &aigv1b1.BackendSecurityPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "openai-bsp", Namespace: "default"}, Spec: aigv1b1.BackendSecurityPolicySpec{ diff --git a/tests/controller/openai_credentials_test.go b/tests/controller/openai_credentials_test.go index 2a76aae012..43a64921f0 100644 --- a/tests/controller/openai_credentials_test.go +++ b/tests/controller/openai_credentials_test.go @@ -79,7 +79,7 @@ func TestBackendSecurityPolicyController_OpenAICredentialsSPIFFE(t *testing.T) { mgr, err := ctrl.NewManager(cfg, opt) require.NoError(t, err) require.NoError(t, controller.ApplyIndexing(t.Context(), mgr.GetFieldIndexer().IndexField)) - pc := controller.NewBackendSecurityPolicyController(mgr.GetClient(), k, defaultLogger(), eventCh.Ch, eventChPool.Ch) + pc := controller.NewBackendSecurityPolicyController(mgr.GetClient(), k, defaultLogger(), eventCh.Ch, eventChPool.Ch, controller.DefaultPreRotationWindow) require.NoError(t, controller.TypedControllerBuilderForCRD(mgr, &aigv1b1.BackendSecurityPolicy{}).Complete(pc)) go func() { require.NoError(t, mgr.Start(t.Context())) }() require.True(t, mgr.GetCache().WaitForCacheSync(t.Context()))