Skip to content

Commit 1b2ebf8

Browse files
committed
implement localJWKS documented in JWT verification design
Add LocalJWKS backed by a Kubernetes Opaque Secret (secretName and key) so HTTPProxy JWT providers can supply JWKS without embedding JSON in the spec. Contour loads and validates the Secret during DAG build (type and JWKS shape), then configures Envoy jwt_authn with inline local JWKS bytes. JWKS Secrets do not use TLS certificate delegation. Includes CRD and API reference updates, DAG/cache/secret handling, listener construction, status and unit tests, xdscache expectations, and featuretests. Signed-off-by: nissy-dev <nd.12021218@gmail.com>
1 parent 3c7cb32 commit 1b2ebf8

19 files changed

Lines changed: 865 additions & 157 deletions

File tree

apis/projectcontour/v1/httpproxy.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,13 @@ type JWTProvider struct {
386386
// +optional
387387
Audiences []string `json:"audiences,omitempty"`
388388

389-
// Remote JWKS to use for verifying JWT signatures.
390-
// +kubebuilder:validation:Required
391-
RemoteJWKS RemoteJWKS `json:"remoteJWKS"`
389+
// Remote JWKS fetches signing keys from an HTTP(S) endpoint.
390+
// +optional
391+
RemoteJWKS *RemoteJWKS `json:"remoteJWKS,omitempty"`
392+
393+
// Local JWKS loads signing keys from a Kubernetes Secret
394+
// +optional
395+
LocalJWKS *LocalJWKS `json:"localJWKS,omitempty"`
392396

393397
// Whether the JWT should be forwarded to the backend
394398
// service after successful verification. By default,
@@ -397,6 +401,19 @@ type JWTProvider struct {
397401
ForwardJWT bool `json:"forwardJWT,omitempty"`
398402
}
399403

404+
// LocalJWKS defines how to fetch a JWKS from a Kubernetes secret.
405+
type LocalJWKS struct {
406+
// The name of the secret that contains the JWKS.
407+
// +kubebuilder:validation:Required
408+
// +kubebuilder:validation:MinLength=1
409+
SecretName string `json:"secretName,omitempty"`
410+
411+
// The key of the secret that contains the JWKS.
412+
// +kubebuilder:validation:Required
413+
// +kubebuilder:validation:MinLength=1
414+
Key string `json:"key,omitempty"`
415+
}
416+
400417
// RemoteJWKS defines how to fetch a JWKS from an HTTP endpoint.
401418
type RemoteJWKS struct {
402419
// The URI for the JWKS.

apis/projectcontour/v1/zz_generated.deepcopy.go

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/contour/01-crds.yaml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7882,12 +7882,31 @@ spec:
78827882
Issuer that JWTs are required to have in the "iss" field.
78837883
If not provided, JWT issuers are not checked.
78847884
type: string
7885+
localJWKS:
7886+
description: Local JWKS loads signing keys from a Kubernetes
7887+
Secret
7888+
properties:
7889+
key:
7890+
description: The key of the secret that contains the
7891+
JWKS.
7892+
minLength: 1
7893+
type: string
7894+
secretName:
7895+
description: The name of the secret that contains the
7896+
JWKS.
7897+
minLength: 1
7898+
type: string
7899+
required:
7900+
- key
7901+
- secretName
7902+
type: object
78857903
name:
78867904
description: Unique name for the provider.
78877905
minLength: 1
78887906
type: string
78897907
remoteJWKS:
7890-
description: Remote JWKS to use for verifying JWT signatures.
7908+
description: Remote JWKS fetches signing keys from an HTTP(S)
7909+
endpoint.
78917910
properties:
78927911
cacheDuration:
78937912
description: |-
@@ -7968,7 +7987,6 @@ spec:
79687987
type: object
79697988
required:
79707989
- name
7971-
- remoteJWKS
79727990
type: object
79737991
type: array
79747992
rateLimitPolicy:

examples/render/contour-deployment.yaml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8101,12 +8101,31 @@ spec:
81018101
Issuer that JWTs are required to have in the "iss" field.
81028102
If not provided, JWT issuers are not checked.
81038103
type: string
8104+
localJWKS:
8105+
description: Local JWKS loads signing keys from a Kubernetes
8106+
Secret
8107+
properties:
8108+
key:
8109+
description: The key of the secret that contains the
8110+
JWKS.
8111+
minLength: 1
8112+
type: string
8113+
secretName:
8114+
description: The name of the secret that contains the
8115+
JWKS.
8116+
minLength: 1
8117+
type: string
8118+
required:
8119+
- key
8120+
- secretName
8121+
type: object
81048122
name:
81058123
description: Unique name for the provider.
81068124
minLength: 1
81078125
type: string
81088126
remoteJWKS:
8109-
description: Remote JWKS to use for verifying JWT signatures.
8127+
description: Remote JWKS fetches signing keys from an HTTP(S)
8128+
endpoint.
81108129
properties:
81118130
cacheDuration:
81128131
description: |-
@@ -8187,7 +8206,6 @@ spec:
81878206
type: object
81888207
required:
81898208
- name
8190-
- remoteJWKS
81918209
type: object
81928210
type: array
81938211
rateLimitPolicy:

examples/render/contour-gateway-provisioner.yaml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7893,12 +7893,31 @@ spec:
78937893
Issuer that JWTs are required to have in the "iss" field.
78947894
If not provided, JWT issuers are not checked.
78957895
type: string
7896+
localJWKS:
7897+
description: Local JWKS loads signing keys from a Kubernetes
7898+
Secret
7899+
properties:
7900+
key:
7901+
description: The key of the secret that contains the
7902+
JWKS.
7903+
minLength: 1
7904+
type: string
7905+
secretName:
7906+
description: The name of the secret that contains the
7907+
JWKS.
7908+
minLength: 1
7909+
type: string
7910+
required:
7911+
- key
7912+
- secretName
7913+
type: object
78967914
name:
78977915
description: Unique name for the provider.
78987916
minLength: 1
78997917
type: string
79007918
remoteJWKS:
7901-
description: Remote JWKS to use for verifying JWT signatures.
7919+
description: Remote JWKS fetches signing keys from an HTTP(S)
7920+
endpoint.
79027921
properties:
79037922
cacheDuration:
79047923
description: |-
@@ -7979,7 +7998,6 @@ spec:
79797998
type: object
79807999
required:
79818000
- name
7982-
- remoteJWKS
79838001
type: object
79848002
type: array
79858003
rateLimitPolicy:

examples/render/contour-gateway.yaml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7918,12 +7918,31 @@ spec:
79187918
Issuer that JWTs are required to have in the "iss" field.
79197919
If not provided, JWT issuers are not checked.
79207920
type: string
7921+
localJWKS:
7922+
description: Local JWKS loads signing keys from a Kubernetes
7923+
Secret
7924+
properties:
7925+
key:
7926+
description: The key of the secret that contains the
7927+
JWKS.
7928+
minLength: 1
7929+
type: string
7930+
secretName:
7931+
description: The name of the secret that contains the
7932+
JWKS.
7933+
minLength: 1
7934+
type: string
7935+
required:
7936+
- key
7937+
- secretName
7938+
type: object
79217939
name:
79227940
description: Unique name for the provider.
79237941
minLength: 1
79247942
type: string
79257943
remoteJWKS:
7926-
description: Remote JWKS to use for verifying JWT signatures.
7944+
description: Remote JWKS fetches signing keys from an HTTP(S)
7945+
endpoint.
79277946
properties:
79287947
cacheDuration:
79297948
description: |-
@@ -8004,7 +8023,6 @@ spec:
80048023
type: object
80058024
required:
80068025
- name
8007-
- remoteJWKS
80088026
type: object
80098027
type: array
80108028
rateLimitPolicy:

examples/render/contour.yaml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8101,12 +8101,31 @@ spec:
81018101
Issuer that JWTs are required to have in the "iss" field.
81028102
If not provided, JWT issuers are not checked.
81038103
type: string
8104+
localJWKS:
8105+
description: Local JWKS loads signing keys from a Kubernetes
8106+
Secret
8107+
properties:
8108+
key:
8109+
description: The key of the secret that contains the
8110+
JWKS.
8111+
minLength: 1
8112+
type: string
8113+
secretName:
8114+
description: The name of the secret that contains the
8115+
JWKS.
8116+
minLength: 1
8117+
type: string
8118+
required:
8119+
- key
8120+
- secretName
8121+
type: object
81048122
name:
81058123
description: Unique name for the provider.
81068124
minLength: 1
81078125
type: string
81088126
remoteJWKS:
8109-
description: Remote JWKS to use for verifying JWT signatures.
8127+
description: Remote JWKS fetches signing keys from an HTTP(S)
8128+
endpoint.
81108129
properties:
81118130
cacheDuration:
81128131
description: |-
@@ -8187,7 +8206,6 @@ spec:
81878206
type: object
81888207
required:
81898208
- name
8190-
- remoteJWKS
81918209
type: object
81928210
type: array
81938211
rateLimitPolicy:

internal/dag/accessors.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,9 @@ func (d *DAG) GetDNSNameClusters() []*DNSNameCluster {
264264
for _, listener := range d.Listeners {
265265
for _, svhost := range listener.SecureVirtualHosts {
266266
for _, provider := range svhost.JWTProviders {
267-
res = append(res, &provider.RemoteJWKS.Cluster)
267+
if provider.RemoteJWKS != nil {
268+
res = append(res, &provider.RemoteJWKS.Cluster)
269+
}
268270
}
269271
}
270272
}

internal/dag/cache.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,14 @@ func (kc *KubernetesCache) secretTriggersRebuild(secretObj *core_v1.Secret) bool
563563
// not a root ingress
564564
continue
565565
}
566+
for _, jp := range vh.JWTProviders {
567+
if jp.LocalJWKS == nil || jp.LocalJWKS.SecretName == "" {
568+
continue
569+
}
570+
if secret == k8s.NamespacedNameFrom(jp.LocalJWKS.SecretName, k8s.DefaultNamespace(proxy.Namespace)) {
571+
return true
572+
}
573+
}
566574
tls := vh.TLS
567575
if tls == nil {
568576
// no tls spec
@@ -736,6 +744,27 @@ func (kc *KubernetesCache) LookupCRLSecret(name types.NamespacedName, targetName
736744
return sec, nil
737745
}
738746

747+
// LookupJWKSFromSecret returns JWKS JSON from the named Secret data entry.
748+
func (kc *KubernetesCache) LookupJWKSFromSecret(name types.NamespacedName, key string) ([]byte, error) {
749+
sec, ok := kc.secrets[name]
750+
if !ok {
751+
return nil, fmt.Errorf("Secret not found")
752+
}
753+
754+
// Compute and store the validation result if not
755+
// already stored.
756+
if sec.ValidJWKSSecret == nil {
757+
sec.ValidJWKSSecret = &SecretValidationStatus{
758+
Error: validJWKSSecret(sec.Object, key),
759+
}
760+
}
761+
762+
if err := sec.ValidJWKSSecret.Error; err != nil {
763+
return nil, err
764+
}
765+
return sec.Object.Data[key], nil
766+
}
767+
739768
// LookupUpstreamValidation constructs PeerValidationContext with CA certificate from the cache.
740769
// If name (referred Secret) is in different namespace than targetNamespace (the referring object),
741770
// then delegation check is performed.

internal/dag/dag.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,8 @@ type JWTProvider struct {
820820
Name string
821821
Issuer string
822822
Audiences []string
823-
RemoteJWKS RemoteJWKS
823+
RemoteJWKS *RemoteJWKS
824+
LocalJWKS *LocalJWKS
824825
ForwardJWT bool
825826
}
826827

@@ -831,6 +832,10 @@ type RemoteJWKS struct {
831832
CacheDuration *time.Duration
832833
}
833834

835+
type LocalJWKS struct {
836+
JWKS []byte
837+
}
838+
834839
// DNSNameCluster is a cluster that routes directly to a DNS
835840
// name (i.e. not a Kubernetes service).
836841
type DNSNameCluster struct {
@@ -1153,13 +1158,14 @@ func (s *ServiceCluster) Rebalance() {
11531158
}
11541159
}
11551160

1156-
// Secret represents a K8s Secret for TLS usage as a DAG Vertex. A Secret is
1161+
// Secret represents a K8s Secret as a DAG Vertex. A Secret is
11571162
// a leaf in the DAG.
11581163
type Secret struct {
1159-
Object *core_v1.Secret
1160-
ValidTLSSecret *SecretValidationStatus
1161-
ValidCASecret *SecretValidationStatus
1162-
ValidCRLSecret *SecretValidationStatus
1164+
Object *core_v1.Secret
1165+
ValidTLSSecret *SecretValidationStatus
1166+
ValidCASecret *SecretValidationStatus
1167+
ValidCRLSecret *SecretValidationStatus
1168+
ValidJWKSSecret *SecretValidationStatus
11631169
}
11641170

11651171
func (s *Secret) Name() string { return s.Object.Name }

0 commit comments

Comments
 (0)