Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4eb5267
only reconcile jwks for policies that have changed
dmitri-d Nov 21, 2025
3ed1725
use a controller to sync jwks store to ConfigMaps
dmitri-d Nov 24, 2025
04a8cef
fixed lost event in jwks source collection
dmitri-d Nov 27, 2025
cca6e30
parse BackendTLSPolicy to use with remote jwks
dmitri-d Nov 27, 2025
519e877
an initial implementation for supporting of backendRefs in Agentgatew…
dmitri-d Nov 28, 2025
c87df28
added support for setting of InsecureSkipVerify in tls options
dmitri-d Nov 28, 2025
19f87bd
Merge remote-tracking branch 'upstream/main' into remote-jwks-with-cm…
dmitri-d Dec 1, 2025
91fc675
fixed spelling mistakes and such
dmitri-d Dec 1, 2025
c7b4a54
fix tests
dmitri-d Dec 1, 2025
c3d2cd5
make fmt
dmitri-d Dec 1, 2025
3d60d4d
fixed liniting issues
dmitri-d Dec 1, 2025
04d038d
Merge remote-tracking branch 'upstream/main' into remote-jwks-with-cm…
dmitri-d Dec 2, 2025
1ffdf58
Merge remote-tracking branch 'upstream/main' into remote-jwks-with-cm…
dmitri-d Dec 2, 2025
1c37386
Merge remote-tracking branch 'upstream/main' into remote-jwks-with-cm…
dmitri-d Dec 10, 2025
5e70df0
cleaned up reusable labels
dmitri-d Dec 10, 2025
a6931ae
small fixes
dmitri-d Dec 10, 2025
2937ceb
Merge branch 'remote-jwks-with-cm-controller' into remote-jwks-with-refs
dmitri-d Dec 11, 2025
7b034fa
post-merge fixes
dmitri-d Dec 11, 2025
ef548f1
use BackendTLS
dmitri-d Dec 11, 2025
2f2b513
broke up jwksUri into jwksPath + fqdn from backend/service
dmitri-d Dec 11, 2025
9fbd9a9
Merge remote-tracking branch 'upstream/main' into remote-jwks-with-refs
dmitri-d Dec 11, 2025
84880b2
added comments
dmitri-d Dec 11, 2025
427f5ce
make linter happy
dmitri-d Dec 11, 2025
2fbae93
make fmt
dmitri-d Dec 11, 2025
0cd442c
added missing String() func to TargetRefIndexKey
dmitri-d Dec 11, 2025
e7b6aa4
handle missing AgentgatewayPolicy when computing jwks url
dmitri-d Dec 12, 2025
86cf818
updated existing tests and added more
dmitri-d Dec 12, 2025
3daf6e9
added missing rbac tests
dmitri-d Dec 12, 2025
48e2830
updated mcp backend resource configs used in tests
dmitri-d Dec 12, 2025
4340373
moar tests
dmitri-d Dec 12, 2025
be91d1d
make linter happy
dmitri-d Dec 12, 2025
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
6 changes: 2 additions & 4 deletions api/v1alpha1/agentgateway_policy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,22 +514,20 @@ type AgentJWKS struct {
Inline *string `json:"inline,omitempty"`
}

// +kubebuilder:validation:ExactlyOneOf=jwksUri;backendRef
type AgentRemoteJWKS struct {
// IdP jwks endpoint. Default tls settings are used to connect to this url.
// +kubebuilder:validation:Pattern=`^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$`
// +optional
// +required
JwksUri string `json:"jwksUri,omitempty"`
// +optional
// +kubebuilder:validation:XValidation:rule="matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$')",message="invalid duration value"
// +kubebuilder:validation:XValidation:rule="duration(self) >= duration('5m')",message="cacheDuration must be at least 5m."
// +kubebuilder:default="5m"
CacheDuration *metav1.Duration `json:"cacheDuration,omitempty"`
// backendRef references the remote JWKS server to reach.
// Not implemented yet, only jwksUri is currently supported.
// Supported types: Service and Backend.
// +optional
BackendRef gwv1.BackendObjectReference `json:"backendRef,omitempty"`
BackendRef *gwv1.BackendObjectReference `json:"backendRef,omitempty"`
}

// +kubebuilder:validation:Enum=Strict;Optional
Expand Down
6 changes: 5 additions & 1 deletion api/v1alpha1/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 @@ -2853,7 +2853,6 @@ spec:
backendRef:
description: |-
backendRef references the remote JWKS server to reach.
Not implemented yet, only jwksUri is currently supported.
Supported types: Service and Backend.
properties:
group:
Expand Down Expand Up @@ -2936,12 +2935,9 @@ spec:
settings are used to connect to this url.
pattern: ^(https|http):\/\/[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:\d+)?\/.*$
type: string
required:
- jwksUri
type: object
x-kubernetes-validations:
- message: exactly one of the fields in [jwksUri
backendRef] must be set
rule: '[has(self.jwksUri),has(self.backendRef)].filter(x,x==true).size()
== 1'
type: object
x-kubernetes-validations:
- message: exactly one of the fields in [remote inline]
Expand Down
154 changes: 154 additions & 0 deletions internal/kgateway/agentjwksstore/cm_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package agentjwksstore

import (
"context"
"math"
"time"

"golang.org/x/time/rate"
"istio.io/istio/pkg/kube/controllers"
"istio.io/istio/pkg/kube/kclient"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kgateway-dev/kgateway/v2/internal/kgateway/jwks"
"github.com/kgateway-dev/kgateway/v2/pkg/apiclient"
"github.com/kgateway-dev/kgateway/v2/pkg/logging"
)

var cmLogger = logging.New("jwks_store_config_map_controller")

const JwksStoreConfigMapName = "jwks-store"

type JwksStoreConfigMapsController struct {
apiClient apiclient.Client
cmClient kclient.Client[*corev1.ConfigMap]
eventQueue controllers.Queue
jwksUpdates chan map[string]string
jwksStore *jwks.JwksStore
deploymentNamespace string
waitForSync []cache.InformerSynced
}

var (
rateLimiter = workqueue.NewTypedMaxOfRateLimiter(
workqueue.NewTypedItemExponentialFailureRateLimiter[any](500*time.Millisecond, 10*time.Second),
// 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
&workqueue.TypedBucketRateLimiter[any]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)
)

func NewJWKSStoreConfigMapsController(apiClient apiclient.Client, deploymentNamespace string, jwksStore *jwks.JwksStore) *JwksStoreConfigMapsController {
cmLogger.Info("creating jwks store ConfigMap controller")
return &JwksStoreConfigMapsController{
apiClient: apiClient,
deploymentNamespace: deploymentNamespace,
jwksStore: jwksStore,
}
}

func (jcm *JwksStoreConfigMapsController) Init(ctx context.Context) {
jcm.cmClient = kclient.NewFiltered[*corev1.ConfigMap](jcm.apiClient,
kclient.Filter{
ObjectFilter: jcm.apiClient.ObjectFilter(),
Namespace: jcm.deploymentNamespace,
LabelSelector: jwks.JwksStoreLabelString})

jcm.waitForSync = []cache.InformerSynced{
jcm.cmClient.HasSynced,
}

jcm.jwksUpdates = jcm.jwksStore.SubscribeToUpdates()
jcm.eventQueue = controllers.NewQueue("JwksStoreController", controllers.WithReconciler(jcm.Reconcile), controllers.WithMaxAttempts(math.MaxInt), controllers.WithRateLimiter(rateLimiter))
}

func (jcm *JwksStoreConfigMapsController) Start(ctx context.Context) error {
cmLogger.Info("waiting for cache to sync")
jcm.apiClient.Core().WaitForCacheSync(
"kube jwks store ConfigMap syncer",
ctx.Done(),
jcm.waitForSync...,
)

cmLogger.Info("starting jwks store ConfigMap controller")
jcm.cmClient.AddEventHandler(
controllers.FromEventHandler(
func(o controllers.Event) {
jcm.eventQueue.AddObject(o.Latest())
}))

go func() {
for {
select {
case u := <-jcm.jwksUpdates:
for uri := range u {
jcm.eventQueue.AddObject(jcm.newJwksStoreConfigMap(jwks.JwksConfigMapName(uri)))
}
case <-ctx.Done():
return
}
}
}()
go jcm.eventQueue.Run(ctx.Done())

<-ctx.Done()
return nil
}

func (jcm *JwksStoreConfigMapsController) Reconcile(req types.NamespacedName) error {
cmLogger.Debug("syncing jwks store to ConfigMap(s)")
ctx := context.Background()

uri, storedJwks, ok := jcm.jwksStore.JwksByConfigMapName(req.Name)
if !ok {
cmLogger.Debug("deleting ConfigMap", "name", req.Name)
return client.IgnoreNotFound(jcm.apiClient.Kube().CoreV1().ConfigMaps(req.Namespace).Delete(ctx, req.Name, metav1.DeleteOptions{}))
}

existingCm := jcm.cmClient.Get(req.Name, req.Namespace)
if existingCm == nil {
newCm := jcm.newJwksStoreConfigMap(jwks.JwksConfigMapName(uri))
if err := jwks.SetJwksInConfigMap(newCm, uri, storedJwks); err != nil {
cmLogger.Error("error updating ConfigMap", "error", err)
return err // no retries?
}

_, err := jcm.apiClient.Kube().CoreV1().ConfigMaps(req.Namespace).Create(ctx, newCm, metav1.CreateOptions{})
if err != nil {
cmLogger.Error("error creating ConfigMap", "error", err)
return err
}
} else {
if err := jwks.SetJwksInConfigMap(existingCm, uri, storedJwks); err != nil {
cmLogger.Error("error updating ConfigMap", "error", err)
return err // no retries?
}
_, err := jcm.apiClient.Kube().CoreV1().ConfigMaps(req.Namespace).Update(ctx, existingCm, metav1.UpdateOptions{})
if err != nil {
cmLogger.Error("error updating jwks ConfigMap", "error", err)
return err
}
}

return nil
}

// runs on the leader only
func (jcm *JwksStoreConfigMapsController) NeedLeaderElection() bool {
return true
}

func (jcm *JwksStoreConfigMapsController) newJwksStoreConfigMap(name string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: jcm.deploymentNamespace,
Labels: jwks.JwksStoreLabelMap,
},
Data: make(map[string]string),
}
}
95 changes: 0 additions & 95 deletions internal/kgateway/agentjwksstore/jwks_store_controller.go

This file was deleted.

Loading
Loading