Skip to content
15 changes: 15 additions & 0 deletions api/v1alpha1/agentgateway/agentgateway_backend_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package agentgateway

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand Down Expand Up @@ -288,6 +289,20 @@ type BedrockConfig struct {
// If not specified, the AWS Guardrail policy will not be used.
// +optional
Guardrail *AWSGuardrailConfig `json:"guardrail,omitempty"`

// Auth specifies an explicit AWS authentication method for the backend.
// When omitted, we will try to use the default AWS SDK authentication methods.
//
// +optional
Auth *AwsAuth `json:"auth,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm not sure this belongs here vs policies.backend.auth.aws. That would be consistent. On the other hand, this is currently only usable for bedrock. But in the future probably we would support lambda, etc, which would necessitate putting it in a more broad area.

I think for consistency and future proofing we need to move this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll try but it will be slightly annoying to do - as if there is no policy, i'll want to implicitly attach the implicit one inline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm - agentgw will default to the implicit one, so doing this is not annoying

}

// AwsAuth specifies the authentication method to use for the backend.
type AwsAuth struct {
// SecretRef references a Kubernetes Secret containing the AWS credentials.
// The Secret must have keys "accessKey", "secretKey", and optionally "sessionToken".
// +required
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be required. If not set, we use the implciit workload identity

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, missed the 'if not set, use implicit' at the higher level. But if wer move it, we will need to make it optional

Copy link
Contributor Author

@yuval-k yuval-k Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved it to BackendAuth and made optional. the secret is still required - if in the future we will add more options, we can make it optional then - it is not a breaking change

SecretRef corev1.LocalObjectReference `json:"secretRef"`
}

type AWSGuardrailConfig struct {
Expand Down
21 changes: 21 additions & 0 deletions api/v1alpha1/agentgateway/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,30 @@ spec:
bedrock:
description: Bedrock provider
properties:
auth:
description: |-
Auth specifies an explicit AWS authentication method for the backend.
When omitted, we will try to use the default AWS SDK authentication methods.
properties:
secretRef:
description: |-
SecretRef references a Kubernetes Secret containing the AWS credentials.
The Secret must have keys "accessKey", "secretKey", and optionally "sessionToken".
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
required:
- secretRef
type: object
guardrail:
description: |-
Guardrail configures the Guardrail policy to use for the backend. See <https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html>
Expand Down Expand Up @@ -1545,6 +1569,30 @@ spec:
bedrock:
description: Bedrock provider
properties:
auth:
description: |-
Auth specifies an explicit AWS authentication method for the backend.
When omitted, we will try to use the default AWS SDK authentication methods.
properties:
secretRef:
description: |-
SecretRef references a Kubernetes Secret containing the AWS credentials.
The Secret must have keys "accessKey", "secretKey", and optionally "sessionToken".
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
required:
- secretRef
type: object
guardrail:
description: |-
Guardrail configures the Guardrail policy to use for the backend. See <https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ inlinePolicies:
/v1/messages/count_tokens: ANTHROPIC_TOKEN_COUNT
/v1/models: MODELS
/v1/responses: RESPONSES
- auth:
aws:
implicit: {}
key: test-ns/bedrock-with-new-routes
name:
name: bedrock-with-new-routes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ai:
providerGroups:
- providers:
- bedrock:
region: us-east-1
name: backend
inlinePolicies:
- auth:
aws:
explicitConfig:
accessKeyId: secret-accessKey
region: us-east-1
secretAccessKey: secret-secretKey
sessionToken: secret-sessionToken
key: test-ns/bedrock-with-secret-ref
name:
name: bedrock-with-secret-ref
namespace: test-ns
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ ai:
model: anthropic.claude-3-haiku-20240307-v1:0
region: eu-west-1
name: backend
inlinePolicies:
- auth:
aws:
implicit: {}
key: test-ns/bedrock-backend-custom
name:
name: bedrock-backend-custom
Expand Down
99 changes: 93 additions & 6 deletions pkg/kgateway/agentgatewaysyncer/backend/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway"
"github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins"
"github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/utils"
"github.com/kgateway-dev/kgateway/v2/pkg/kgateway/wellknown"
"github.com/kgateway-dev/kgateway/v2/pkg/logging"
"github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils"
)
Expand Down Expand Up @@ -224,11 +225,17 @@ func translateAIBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBac

aiBackend := &api.AIBackend{}
if llm := ai.LLM; llm != nil {
provider, err := translateLLMProvider(llm, utils.SingularLLMProviderSubBackendName)
provider, auth, err := translateLLMProvider(ctx, llm, utils.SingularLLMProviderSubBackendName, be.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to translate LLM provider: %w", err)
}

if auth != nil {
inlinePolicies = append(inlinePolicies, &api.BackendPolicySpec{
Kind: &api.BackendPolicySpec_Auth{
Auth: auth,
},
})
}
aiBackend.ProviderGroups = []*api.AIBackend_ProviderGroup{{
Providers: []*api.AIBackend_Provider{provider},
}}
Expand All @@ -237,7 +244,7 @@ func translateAIBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBac
providerGroup := &api.AIBackend_ProviderGroup{}

for _, provider := range group.Providers {
tp, err := translateLLMProvider(&provider.LLMProvider, string(provider.Name))
tp, auth, err := translateLLMProvider(ctx, &provider.LLMProvider, string(provider.Name), be.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to translate LLM provider: %w", err)
}
Expand All @@ -247,6 +254,13 @@ func translateAIBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBac
logger.Warn("failed to translate AI backend policies", "err", err)
}
tp.InlinePolicies = pol
if auth != nil {
tp.InlinePolicies = append(tp.InlinePolicies, &api.BackendPolicySpec{
Kind: &api.BackendPolicySpec_Auth{
Auth: auth,
},
})
}

providerGroup.Providers = append(providerGroup.Providers, tp)
}
Expand Down Expand Up @@ -303,7 +317,7 @@ func translateAIBackendPolicies(
})
}

func translateLLMProvider(llm *agentgateway.LLMProvider, providerName string) (*api.AIBackend_Provider, error) {
func translateLLMProvider(ctx plugins.PolicyCtx, llm *agentgateway.LLMProvider, providerName, namespace string) (*api.AIBackend_Provider, *api.BackendAuthPolicy, error) {
provider := &api.AIBackend_Provider{
Name: providerName,
}
Expand All @@ -318,6 +332,7 @@ func translateLLMProvider(llm *agentgateway.LLMProvider, providerName string) (*
if llm.Path != "" {
provider.PathOverride = &llm.Path
}
var auth *api.BackendAuthPolicy

// Extract auth token and model based on provider
if llm.OpenAI != nil {
Expand Down Expand Up @@ -363,6 +378,12 @@ func translateLLMProvider(llm *agentgateway.LLMProvider, providerName string) (*
guardrailVersion = &llm.Bedrock.Guardrail.GuardrailVersion
}

var err error
auth, err = buildBedrockAuthPolicy(ctx.Krt, region, llm.Bedrock.Auth, ctx.Collections.Secrets, namespace)
if err != nil {
return nil, nil, err
}

provider.Provider = &api.AIBackend_Provider_Bedrock{
Bedrock: &api.AIBackend_Bedrock{
Model: llm.Bedrock.Model,
Expand All @@ -372,10 +393,10 @@ func translateLLMProvider(llm *agentgateway.LLMProvider, providerName string) (*
},
}
} else {
return nil, fmt.Errorf("no supported LLM provider configured")
return nil, nil, fmt.Errorf("no supported LLM provider configured")
}

return provider, nil
return provider, auth, nil
}

func toMCPProtocol(appProtocol string) api.MCPTarget_Protocol {
Expand All @@ -391,3 +412,69 @@ func toMCPProtocol(appProtocol string) api.MCPTarget_Protocol {
return api.MCPTarget_UNDEFINED
}
}

func buildBedrockAuthPolicy(krtctx krt.HandlerContext, region string, auth *agentgateway.AwsAuth, secrets krt.Collection[*corev1.Secret], namespace string) (*api.BackendAuthPolicy, error) {
var errs []error
if auth == nil {
logger.Warn("using implicit AWS auth for AI backend")
return &api.BackendAuthPolicy{
Kind: &api.BackendAuthPolicy_Aws{
Aws: &api.Aws{
Kind: &api.Aws_Implicit{
Implicit: &api.AwsImplicit{},
},
},
},
}, nil
}

if auth.SecretRef.Name == "" {
logger.Warn("not using any auth for AWS - it's most likely not what you want")
return nil, nil
}

// Get secret using the SecretIndex
secret, err := kubeutils.GetSecret(secrets, krtctx, auth.SecretRef.Name, namespace)
if err != nil {
// Return nil auth policy if secret not found - this will be handled upstream
// TODO(npolshak): Add backend status errors https://github.com/kgateway-dev/kgateway/issues/11966
return nil, err
}

var accessKeyId, secretAccessKey string
var sessionToken *string

// Extract access key
if value, exists := kubeutils.GetSecretValue(secret, wellknown.AccessKey); !exists {
errs = append(errs, errors.New("accessKey is missing or not a valid string"))
} else {
accessKeyId = value
}

// Extract secret key
if value, exists := kubeutils.GetSecretValue(secret, wellknown.SecretKey); !exists {
errs = append(errs, errors.New("secretKey is missing or not a valid string"))
} else {
secretAccessKey = value
}

Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages for missing secret keys don't include context about which secret is being accessed. Consider including the secret name and namespace in the error messages to help users identify which secret needs to be fixed. For example: "accessKey is missing in secret test-ns/bedrock-secret".

Copilot uses AI. Check for mistakes.
// Extract session token (optional)
if value, exists := kubeutils.GetSecretValue(secret, wellknown.SessionToken); exists {
sessionToken = ptr.Of(value)
}

return &api.BackendAuthPolicy{
Kind: &api.BackendAuthPolicy_Aws{
Aws: &api.Aws{
Kind: &api.Aws_ExplicitConfig{
ExplicitConfig: &api.AwsExplicitConfig{
AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey,
SessionToken: sessionToken,
Region: region,
},
},
},
},
}, errors.Join(errs...)
}
30 changes: 30 additions & 0 deletions pkg/kgateway/agentgatewaysyncer/backend/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,36 @@ func TestBuildAIBackend(t *testing.T) {
},
},
},
{
name: "Bedrock backend with new secret ref",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backend: &agentgateway.AgentgatewayBackend{
ObjectMeta: metav1.ObjectMeta{
Name: "bedrock-with-secret-ref",
Namespace: "test-ns",
},
Spec: agentgateway.AgentgatewayBackendSpec{
AI: &agentgateway.AIBackend{
LLM: &agentgateway.LLMProvider{
Bedrock: &agentgateway.BedrockConfig{
Region: "us-east-1",
Auth: &agentgateway.AwsAuth{
SecretRef: corev1.LocalObjectReference{
Name: "bedrock-secret",
},
},
},
},
},
},
},
inputs: []any{
createMockSecret("test-ns", "bedrock-secret", map[string]string{
"accessKey": "secret-accessKey",
"secretKey": "secret-secretKey",
"sessionToken": "secret-sessionToken",
}),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Backends:
model: anthropic.claude-3-5-haiku-20241022-v1:0
region: us-west-2
name: backend
inlinePolicies:
- auth:
aws:
implicit: {}
key: default/bedrock
name:
name: bedrock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Backends:
- bedrock:
model: anthropic.claude-3-haiku-20240307-v1:0
region: us-east-1
inlinePolicies:
- auth:
aws:
implicit: {}
name: bedrock
key: default/multipool-priority
name:
Expand Down