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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 113 additions & 13 deletions api/v1beta1/backendsecurity_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -99,6 +101,13 @@ 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 via OpenAI Workload Identity Federation, 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.
//
Expand Down Expand Up @@ -347,6 +356,96 @@ 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"`

// WorkloadIdentityFederationConfig is the configuration for OpenAI Workload Identity Federation.
//
// +kubebuilder:validation:Required
WorkloadIdentityFederationConfig OpenAIWorkloadIdentityFederationConfig `json:"workloadIdentityFederationConfig"`
}

// OpenAIWorkloadIdentityFederationConfig specifies how the controller obtains an OpenAI access token via
// 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.
//
// 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
IdentityProviderID string `json:"identityProviderID"`

// 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.
//
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
ServiceAccountID string `json:"serviceAccountID"`

// SubjectToken specifies where the subject token is obtained from. The subject token must be a JWT.
//
// +kubebuilder:validation:Required
SubjectToken OpenAISubjectToken `json:"subjectToken"`
}

// 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 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"`

// 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.
//
// +optional
SPIFFEJWTSVID *BackendSecurityPolicySPIFFEJWTSVID `json:"spiffeJWTSVID,omitempty"`
}

// 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, e.g. the audience configured
// for the OpenAI Workload Identity Provider.
//
// +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.
Expand Down Expand Up @@ -379,6 +478,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:
Expand Down
77 changes: 77 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/basic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading