Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implementation of AWS OIDC #232

Closed
wants to merge 15 commits into from
Closed
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
2 changes: 1 addition & 1 deletion filterapi/filterconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ type BackendAuth struct {
// AWSAuth defines the credentials needed to access AWS.
type AWSAuth struct {
CredentialFileName string `json:"credentialFileName,omitempty"`
Region string `json:"region"`
Region string `json:"region,omitempty"`
}

// APIKeyAuth defines the file that will be mounted to the external proc.
Expand Down
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ require (
github.com/aws/aws-sdk-go-v2 v1.35.0
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8
github.com/aws/aws-sdk-go-v2/config v1.29.3
github.com/aws/aws-sdk-go-v2/credentials v1.17.56
github.com/aws/aws-sdk-go-v2/service/sts v1.33.11
github.com/coreos/go-oidc/v3 v3.12.0
github.com/envoyproxy/gateway v1.3.0
github.com/envoyproxy/go-control-plane/envoy v1.32.3
github.com/go-logr/logr v1.4.2
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/cel-go v0.23.1
github.com/google/go-cmp v0.6.0
github.com/openai/openai-go v0.1.0-alpha.50
github.com/stretchr/testify v1.10.0
go.uber.org/goleak v1.3.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c
golang.org/x/oauth2 v0.25.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
k8s.io/api v0.32.1
Expand All @@ -32,7 +37,6 @@ require (
require (
cel.dev/expr v0.19.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.56 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.30 // indirect
Expand All @@ -41,7 +45,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.11 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand All @@ -53,6 +56,7 @@ require (
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
Expand Down Expand Up @@ -86,8 +90,8 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand All @@ -56,6 +58,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
Expand All @@ -72,6 +76,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
Expand Down Expand Up @@ -184,6 +190,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
Expand Down
13 changes: 12 additions & 1 deletion internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"

aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
"github.com/envoyproxy/ai-gateway/internal/controller/oidc"
)

func init() { MustInitializeScheme(scheme) }
Expand Down Expand Up @@ -115,6 +116,15 @@ func StartControllers(ctx context.Context, config *rest.Config, logger logr.Logg
if err = mgr.Start(ctx); err != nil { // This blocks until the manager is stopped.
return fmt.Errorf("failed to start controller manager: %w", err)
}

// Have a token exchange handler per provider type.
handler, err := oidc.NewOIDCTokenExchange(&logger, c, aigv1a1.BackendSecurityPolicyTypeAWSCredentials)
if err != nil {
return fmt.Errorf("failed to create OIDC handler: %w", err)
}

go handler.RefreshCredentials(ctx)

Comment on lines +120 to +127
Copy link
Member

Choose a reason for hiding this comment

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

these code are not called since mgr.Start is blocking.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about we write a reconciler for BackendSecurityPolicyTypeAWSCredentials. The reconcile loop can be trigger can time tick event. In the reconcile loop you can fresh the tokens.

  • write a event ticket soruce
  • only watch generic event
, err := ctrl.NewControllerManagedBy(mgr).
		For(&igv1a1.BackendSecurityPolicyTypeAWSCredentials{}}).
		//we filter out create/delete/update event
		//reconciliation is only triggered by a GenericEvent generated by a time ticker
		WithEventFilter(predicate.Funcs
			GenericFunc: func(event.GenericEvent) bool { return true },
  • refresh the token in the reconcile loop

If we move the auth logic in the controller, maybe let us follow controller-runtime convention ?

return nil
}

Expand Down Expand Up @@ -181,8 +191,9 @@ func backendSecurityPolicyIndexFunc(o client.Object) []string {
awsCreds := backendSecurityPolicy.Spec.AWSCredentials
if awsCreds.CredentialsFile != nil {
key = getSecretNameAndNamespace(awsCreds.CredentialsFile.SecretRef, backendSecurityPolicy.Namespace)
} else if awsCreds.OIDCExchangeToken != nil {
key = getSecretNameAndNamespace(&awsCreds.OIDCExchangeToken.OIDC.ClientSecret, backendSecurityPolicy.Namespace)
}
// TODO: OIDC.
}
return []string{key}
}
Expand Down
155 changes: 155 additions & 0 deletions internal/controller/oidc/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package oidc

import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

aigv1a1 "github.com/envoyproxy/ai-gateway/api/v1alpha1"
)

const OidcAwsPrefix = "oidc-aws-"

type AWSSpec struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

This name is a bit unclear of the purpose, maybe we should name the other struct AWSCredentialHandler and this one AWSCredentialExchange

region string
expiredTime time.Time
roleArn string
namespace string
credentials aws.Credentials
oidc egv1a1.OIDC
aud string
}

type AWSCredentialExchange struct {
logger *logr.Logger
k8sClient client.Client
awsSpecs map[string]*AWSSpec
}

func newAWSCredentialExchange(logger *logr.Logger, k8sClient client.Client) *AWSCredentialExchange {
return &AWSCredentialExchange{
logger: logger,
k8sClient: k8sClient,
awsSpecs: make(map[string]*AWSSpec),
}
}

func (a *AWSCredentialExchange) isOIDCBackendSecurityPolicy(policy aigv1a1.BackendSecurityPolicy) bool {
if policy.Spec.Type != aigv1a1.BackendSecurityPolicyTypeAWSCredentials {
a.logger.Info(fmt.Sprintf("Skipping credentials refresh for type %s", policy.Spec.Type))
return false
}
if policy.Spec.AWSCredentials.CredentialsFile != nil {
a.logger.Info(fmt.Sprintf("Skiping due to credential file being set for %s", policy.Name))
return false
}
return true
}

func (a *AWSCredentialExchange) createSpecIfNew(policy aigv1a1.BackendSecurityPolicy) {
_, ok := a.awsSpecs[fmt.Sprintf("%s.%s", policy.Name, policy.Namespace)]
if !ok {
a.awsSpecs[fmt.Sprintf("%s.%s", policy.Name, policy.Namespace)] = &AWSSpec{
region: policy.Spec.AWSCredentials.Region,
expiredTime: time.Time{},
roleArn: policy.Spec.AWSCredentials.OIDCExchangeToken.AwsRoleArn,
namespace: policy.Namespace,
credentials: aws.Credentials{},
oidc: policy.Spec.AWSCredentials.OIDCExchangeToken.OIDC,
aud: policy.Spec.AWSCredentials.OIDCExchangeToken.Aud,
}
}
}

func (a *AWSCredentialExchange) getAud(cacheKey string) string {
return a.awsSpecs[cacheKey].aud
}

func (a *AWSCredentialExchange) getOIDC(cacheKey string) egv1a1.OIDC {
return a.awsSpecs[cacheKey].oidc
}

func (a *AWSCredentialExchange) updateCredentials(accessToken, cacheKey string) error {
awsSpec, ok := a.awsSpecs[cacheKey]
if !ok {
return fmt.Errorf("no AWS spec found for %s", cacheKey)
}

// create sts client
stsCfg := aws.Config{
Region: awsSpec.region,
}

if proxyURL := os.Getenv("AI_GATEWY_STS_PROXY_URL"); proxyURL != "" {
stsCfg.HTTPClient = &http.Client{
Transport: &http.Transport{
Proxy: func(*http.Request) (*url.URL, error) {
return url.Parse(proxyURL)
},
},
}
}
stsClient := sts.NewFromConfig(stsCfg)
credentialsCache := aws.NewCredentialsCache(stscreds.NewWebIdentityRoleProvider(
stsClient,
awsSpec.roleArn,
IdentityTokenValue(accessToken),
))
credentials, err := credentialsCache.Retrieve(context.TODO())
awsSpec.credentials = credentials
return err
}

func (a *AWSCredentialExchange) updateSecret(cacheKey string) error {
namespaceName := types.NamespacedName{
Namespace: a.awsSpecs[cacheKey].namespace,
Name: fmt.Sprintf("%s%s", OidcAwsPrefix, cacheKey),
}
credentialSecret := corev1.Secret{}
err := a.k8sClient.Get(context.TODO(), namespaceName, &credentialSecret)
if err != nil {
if client.IgnoreNotFound(err) != nil {
return fmt.Errorf("fail to get secret for backend security policy %w", err)
}
err = a.k8sClient.Create(context.Background(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName.Name,
Namespace: namespaceName.Namespace,
},
})
if err != nil {
return err
}
}
if credentialSecret.StringData == nil {
credentialSecret.StringData = make(map[string]string)
}
credentialSecret.StringData["credentials"] = fmt.Sprintf("[default]\n"+
"aws_access_key_id = %s\n"+
"aws_secret_access_key = %s\n"+
"aws_session_token = %s\n",
a.awsSpecs[cacheKey].credentials.AccessKeyID, a.awsSpecs[cacheKey].credentials.SecretAccessKey, a.awsSpecs[cacheKey].credentials.SessionToken)

err = a.k8sClient.Update(context.TODO(), &credentialSecret)
if err != nil {
return fmt.Errorf("fail to refresh find secret for backend security policy %w", err)
}
return nil
}

func (a *AWSCredentialExchange) needsCredentialRefresh(cacheKey string) bool {
return time.Now().After(a.awsSpecs[cacheKey].expiredTime.Add(timeBeforeExpired))
}
1 change: 1 addition & 0 deletions internal/controller/oidc/aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package oidc
Loading
Loading