From ba9bfebea9c06896b03f4a87602c7ffad18fa3f2 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 3 Jul 2025 17:29:24 +0100 Subject: [PATCH 01/12] Add SNI related options and validation --- internal/configs/version2/http.go | 14 ++++++++------ .../version2/nginx-plus.virtualserver.tmpl | 6 ++++++ internal/configs/virtualserver.go | 12 +++++++----- pkg/apis/configuration/v1/types.go | 12 +++++++----- pkg/apis/configuration/validation/policy.go | 18 +++++++++++++++++- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index 3f27badf1c..ff3ff3be76 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -428,12 +428,14 @@ func (rl LimitReqOptions) String() string { // JWTAuth holds JWT authentication configuration. type JWTAuth struct { - Key string - Secret string - Realm string - Token string - KeyCache string - JwksURI JwksURI + Key string + Secret string + Realm string + Token string + KeyCache string + JwksSNIName string + JwksSNIEnabled bool + JwksURI JwksURI } // JwksURI defines the components of a JwksURI diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index dd08f53014..4976ad6841 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -236,6 +236,12 @@ server { proxy_cache jwks_uri_{{ $s.VSName }}; proxy_cache_valid 200 12h; {{- end }} + {{- if .JwksSNIEnabled }} + proxy_ssl_server_name on; + {{- if .JwksSNIName }} + proxy_ssl_name {{ .JwksSNIName }}; + {{- end }} + {{- end }} {{- with .JwksURI }} proxy_set_header Host {{ .JwksHost }}; set $idp_backend {{ .JwksHost }}; diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index fc17a4f45c..f4725153e7 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1176,11 +1176,13 @@ func (p *policiesCfg) addJWTAuthConfig( } p.JWTAuth.Auth = &version2.JWTAuth{ - Key: polKey, - JwksURI: *JwksURI, - Realm: jwtAuth.Realm, - Token: jwtAuth.Token, - KeyCache: jwtAuth.KeyCache, + Key: polKey, + JwksURI: *JwksURI, + Realm: jwtAuth.Realm, + Token: jwtAuth.Token, + KeyCache: jwtAuth.KeyCache, + JwksSNIEnabled: jwtAuth.SNIEnabled, + JwksSNIName: jwtAuth.SNIServerName, } p.JWTAuth.JWKSEnabled = true return res diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index e700fd5dba..463ee5a576 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -652,11 +652,13 @@ type VariableCondition struct { // JWTAuth holds JWT authentication configuration. type JWTAuth struct { - Realm string `json:"realm"` - Secret string `json:"secret"` - Token string `json:"token"` - JwksURI string `json:"jwksURI"` - KeyCache string `json:"keyCache"` + Realm string `json:"realm"` + Secret string `json:"secret"` + Token string `json:"token"` + JwksURI string `json:"jwksURI"` + KeyCache string `json:"keyCache"` + SNIEnabled bool `json:"sniEnabled"` + SNIServerName string `json:"sniServerName"` } // BasicAuth holds HTTP Basic authentication configuration diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index c698a455ba..0e523106dc 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -9,6 +9,7 @@ import ( "strings" "unicode" + validation2 "github.com/nginx/kubernetes-ingress/internal/validation" v1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -213,7 +214,22 @@ func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { if jwt.KeyCache == "" { allErrs = append(allErrs, field.Required(fieldPath.Child("keyCache"), "key cache must be set, example value: 1h")) } - return allErrs + + // if SNI server name is provided, but SNI is not enabled, return an error + if jwt.SNIServerName != "" && !jwt.SNIEnabled { + allErrs = append(allErrs, field.Forbidden(fieldPath.Child("sniServerName"), "sniServerName can only be set when sniEnabled is true")) + } + + // if SNI is enabled and SNI server name is provided, make sure it's a valid URI + if jwt.SNIEnabled && jwt.SNIServerName != "" { + err := validation2.ValidateURI(jwt.SNIServerName, + validation2.WithAllowedSchemes("https"), + validation2.WithUserAllowed(false), + validation2.WithDefaultScheme("https")) + if err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("sniServerName"), jwt.SNIServerName, "sniServerName is not a valid URI")) + } + } } return allErrs } From 92f8e01caf2101e1ce8c99dd1fd6e823ab732912 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 3 Jul 2025 17:39:10 +0100 Subject: [PATCH 02/12] Add the new options to the crd definitions --- config/crd/bases/k8s.nginx.org_policies.yaml | 4 ++++ deploy/crds.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index d5a51d49e3..5a210d4833 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -141,6 +141,10 @@ spec: type: string secret: type: string + sniEnabled: + type: boolean + sniServerName: + type: string token: type: string type: object diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 01246997d7..29deab4d74 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -303,6 +303,10 @@ spec: type: string secret: type: string + sniEnabled: + type: boolean + sniServerName: + type: string token: type: string type: object From 569d4905c56645b207ac448faf768b8acee2d770 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Fri, 4 Jul 2025 14:20:53 +0100 Subject: [PATCH 03/12] Update snaps --- internal/configs/version2/__snapshots__/templates_test.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index 166c030485..ba7a51ec9c 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -1115,6 +1115,8 @@ server { proxy_set_header Content-Length ""; proxy_cache jwks_uri_cafe; proxy_cache_valid 200 12h; + proxy_ssl_server_name on; + proxy_ssl_name sni.idp.spec.example.com; proxy_set_header Host idp.spec.example.com; set $idp_backend idp.spec.example.com; proxy_pass https://$idp_backend:443/spec-keys; From 5cbb50b583425a18491410f5afdf6f9643b12a98 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Tue, 8 Jul 2025 13:47:42 +0100 Subject: [PATCH 04/12] Rename sniServerName to sniName --- config/crd/bases/k8s.nginx.org_policies.yaml | 2 +- deploy/crds.yaml | 2 +- pkg/apis/configuration/v1/types.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index 5a210d4833..d8955c11b8 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -143,7 +143,7 @@ spec: type: string sniEnabled: type: boolean - sniServerName: + sniName: type: string token: type: string diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 29deab4d74..7aa8f489ca 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -305,7 +305,7 @@ spec: type: string sniEnabled: type: boolean - sniServerName: + sniName: type: string token: type: string diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 463ee5a576..6061d59517 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -658,7 +658,7 @@ type JWTAuth struct { JwksURI string `json:"jwksURI"` KeyCache string `json:"keyCache"` SNIEnabled bool `json:"sniEnabled"` - SNIServerName string `json:"sniServerName"` + SNIServerName string `json:"sniName"` } // BasicAuth holds HTTP Basic authentication configuration From bc3b54f4c1aa06e7f2c9885d4b0e47d1d7cf3e95 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Fri, 4 Jul 2025 14:21:13 +0100 Subject: [PATCH 05/12] Add unit tests for sni --- internal/configs/version2/templates_test.go | 19 +++- internal/configs/virtualserver_test.go | 24 ++-- .../configuration/validation/policy_test.go | 104 ++++++++++++++++++ 3 files changed, 134 insertions(+), 13 deletions(-) diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 54a1a98b80..404c53017d 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -737,6 +737,15 @@ func TestExecuteVirtualServerTemplateWithJWKSWithToken(t *testing.T) { if !bytes.Contains(got, []byte("proxy_cache_valid 200 12h;")) { t.Error("want `proxy_cache_valid 200 12h;` in generated template") } + + if !bytes.Contains(got, []byte("proxy_ssl_server_name on;")) { + t.Error("want `proxy_ssl_server_name on;` in generated template") + } + + if !bytes.Contains(got, []byte("proxy_ssl_name sni.idp.spec.example.com;")) { + t.Error("want `proxy_ssl_name sni.idp.spec.example.com;` in generated template") + } + snaps.MatchSnapshot(t, string(got)) t.Log(string(got)) } @@ -2340,10 +2349,12 @@ var ( Server: Server{ JWTAuthList: map[string]*JWTAuth{ "default/jwt-policy": { - Key: "default/jwt-policy", - Realm: "Spec Realm API", - Token: "$http_token", - KeyCache: "1h", + Key: "default/jwt-policy", + Realm: "Spec Realm API", + Token: "$http_token", + KeyCache: "1h", + JwksSNIEnabled: true, + JwksSNIName: "sni.idp.spec.example.com", JwksURI: JwksURI{ JwksScheme: "https", JwksHost: "idp.spec.example.com", diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 11c7700496..b07b8111bd 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -5641,9 +5641,11 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { }, Spec: conf_v1.PolicySpec{ JWTAuth: &conf_v1.JWTAuth{ - Realm: "Spec Realm API", - JwksURI: "https://idp.spec.example.com:443/spec-keys", - KeyCache: "1h", + Realm: "Spec Realm API", + JwksURI: "https://idp.spec.example.com:443/spec-keys", + KeyCache: "1h", + SNIEnabled: true, + SNIServerName: "idp.spec.example.com", }, }, }, @@ -5709,9 +5711,11 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { Server: version2.Server{ JWTAuthList: map[string]*version2.JWTAuth{ "default/jwt-policy": { - Key: "default/jwt-policy", - Realm: "Spec Realm API", - KeyCache: "1h", + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", + JwksSNIEnabled: true, + JwksSNIName: "idp.spec.example.com", JwksURI: version2.JwksURI{ JwksScheme: "https", JwksHost: "idp.spec.example.com", @@ -5732,9 +5736,11 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { }, }, JWTAuth: &version2.JWTAuth{ - Key: "default/jwt-policy", - Realm: "Spec Realm API", - KeyCache: "1h", + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", + JwksSNIName: "idp.spec.example.com", + JwksSNIEnabled: true, JwksURI: version2.JwksURI{ JwksScheme: "https", JwksHost: "idp.spec.example.com", diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 2a723839da..738516bad5 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -95,6 +95,33 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { }, }, }, + { + name: "SNI server name passed, but SNI not enabled", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + KeyCache: "1h", + SNIServerName: "ipd.org", + }, + }, + }, + }, + { + name: "SNI server name passed, SNI enabled, bad SNI server name", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + KeyCache: "1h", + SNIEnabled: true, + SNIServerName: "msql://ipd.org", + }, + }, + }, + }, } for _, tc := range tt { @@ -164,6 +191,33 @@ func TestValidatePolicy_IsValidOnJWTPolicy(t *testing.T) { }, }, }, + { + name: "with SNI and without SNI server name", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + KeyCache: "1h", + JwksURI: "https://login.mydomain.com/keys", + SNIEnabled: true, + }, + }, + }, + }, + { + name: "with SNI and with SNI server name", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + KeyCache: "1h", + JwksURI: "https://login.mydomain.com/keys", + SNIEnabled: true, + SNIServerName: "https://example.org", + }, + }, + }, + }, } for _, tc := range tt { @@ -787,6 +841,27 @@ func TestValidateJWT_PassesOnValidInput(t *testing.T) { }, msg: "jwt with jwksURI", }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: true, + SNIServerName: "https://ipd.com:9999", + }, + msg: "SNI enabled and valid SNI server name", + }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: true, + }, + msg: "SNI enabled and no server name passed", + }, } for _, test := range tests { allErrs := validateJWT(test.jwt, field.NewPath("jwt")) @@ -890,6 +965,35 @@ func TestValidateJWT_FailsOnInvalidInput(t *testing.T) { }, msg: "invalid JwksURI", }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: true, + SNIServerName: "msql://not-\\\\a-valid-sni", + }, + msg: "invalid SNI server name", + }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: false, + SNIServerName: "https://idp.com", + }, + msg: "SNI server name passed, SNI not enabled", + }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIServerName: "https://idp.com", + }, + msg: "SNI server name passed, SNI not passed", + }, } for _, test := range tests { test := test From 72063bd596caa3796441edb8d26eff2b75634d8c Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Tue, 8 Jul 2025 17:04:11 +0100 Subject: [PATCH 06/12] Move SNI into JwksURI struct --- internal/configs/version2/http.go | 24 +++---- .../version2/nginx-plus.virtualserver.tmpl | 2 +- internal/configs/version2/templates_test.go | 20 +++--- internal/configs/virtualserver.go | 22 +++--- internal/configs/virtualserver_test.go | 46 ++++++------- pkg/apis/configuration/v1/types.go | 14 ++-- pkg/apis/configuration/validation/policy.go | 8 +-- .../configuration/validation/policy_test.go | 68 +++++++++---------- 8 files changed, 102 insertions(+), 102 deletions(-) diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index ff3ff3be76..f835656fb9 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -428,22 +428,22 @@ func (rl LimitReqOptions) String() string { // JWTAuth holds JWT authentication configuration. type JWTAuth struct { - Key string - Secret string - Realm string - Token string - KeyCache string - JwksSNIName string - JwksSNIEnabled bool - JwksURI JwksURI + Key string + Secret string + Realm string + Token string + KeyCache string + JwksURI JwksURI } // JwksURI defines the components of a JwksURI type JwksURI struct { - JwksScheme string - JwksHost string - JwksPort string - JwksPath string + JwksScheme string + JwksHost string + JwksPort string + JwksPath string + JwksSNIName string + JwksSNIEnabled bool } // BasicAuth refers to basic HTTP authentication mechanism options diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 4976ad6841..91c592cb79 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -236,13 +236,13 @@ server { proxy_cache jwks_uri_{{ $s.VSName }}; proxy_cache_valid 200 12h; {{- end }} + {{- with .JwksURI }} {{- if .JwksSNIEnabled }} proxy_ssl_server_name on; {{- if .JwksSNIName }} proxy_ssl_name {{ .JwksSNIName }}; {{- end }} {{- end }} - {{- with .JwksURI }} proxy_set_header Host {{ .JwksHost }}; set $idp_backend {{ .JwksHost }}; proxy_pass {{ .JwksScheme}}://$idp_backend{{ if .JwksPort }}:{{ .JwksPort }}{{ end }}{{ .JwksPath }}; diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 404c53017d..20b4f62896 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -2349,17 +2349,17 @@ var ( Server: Server{ JWTAuthList: map[string]*JWTAuth{ "default/jwt-policy": { - Key: "default/jwt-policy", - Realm: "Spec Realm API", - Token: "$http_token", - KeyCache: "1h", - JwksSNIEnabled: true, - JwksSNIName: "sni.idp.spec.example.com", + Key: "default/jwt-policy", + Realm: "Spec Realm API", + Token: "$http_token", + KeyCache: "1h", JwksURI: JwksURI{ - JwksScheme: "https", - JwksHost: "idp.spec.example.com", - JwksPort: "443", - JwksPath: "/spec-keys", + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + JwksSNIEnabled: true, + JwksSNIName: "sni.idp.spec.example.com", }, }, "default/jwt-policy-route": { diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index f4725153e7..e6f1808415 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1169,20 +1169,20 @@ func (p *policiesCfg) addJWTAuthConfig( uri, _ := url.Parse(jwtAuth.JwksURI) JwksURI := &version2.JwksURI{ - JwksScheme: uri.Scheme, - JwksHost: uri.Hostname(), - JwksPort: uri.Port(), - JwksPath: uri.Path, + JwksScheme: uri.Scheme, + JwksHost: uri.Hostname(), + JwksPort: uri.Port(), + JwksPath: uri.Path, + JwksSNIName: jwtAuth.SNIName, + JwksSNIEnabled: jwtAuth.SNIEnabled, } p.JWTAuth.Auth = &version2.JWTAuth{ - Key: polKey, - JwksURI: *JwksURI, - Realm: jwtAuth.Realm, - Token: jwtAuth.Token, - KeyCache: jwtAuth.KeyCache, - JwksSNIEnabled: jwtAuth.SNIEnabled, - JwksSNIName: jwtAuth.SNIServerName, + Key: polKey, + JwksURI: *JwksURI, + Realm: jwtAuth.Realm, + Token: jwtAuth.Token, + KeyCache: jwtAuth.KeyCache, } p.JWTAuth.JWKSEnabled = true return res diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index b07b8111bd..0b84c9e3f7 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -5641,11 +5641,11 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { }, Spec: conf_v1.PolicySpec{ JWTAuth: &conf_v1.JWTAuth{ - Realm: "Spec Realm API", - JwksURI: "https://idp.spec.example.com:443/spec-keys", - KeyCache: "1h", - SNIEnabled: true, - SNIServerName: "idp.spec.example.com", + Realm: "Spec Realm API", + JwksURI: "https://idp.spec.example.com:443/spec-keys", + KeyCache: "1h", + SNIEnabled: true, + SNIName: "idp.spec.example.com", }, }, }, @@ -5711,16 +5711,16 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { Server: version2.Server{ JWTAuthList: map[string]*version2.JWTAuth{ "default/jwt-policy": { - Key: "default/jwt-policy", - Realm: "Spec Realm API", - KeyCache: "1h", - JwksSNIEnabled: true, - JwksSNIName: "idp.spec.example.com", + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", JwksURI: version2.JwksURI{ - JwksScheme: "https", - JwksHost: "idp.spec.example.com", - JwksPort: "443", - JwksPath: "/spec-keys", + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + JwksSNIEnabled: true, + JwksSNIName: "idp.spec.example.com", }, }, "default/jwt-policy-route": { @@ -5736,16 +5736,16 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) { }, }, JWTAuth: &version2.JWTAuth{ - Key: "default/jwt-policy", - Realm: "Spec Realm API", - KeyCache: "1h", - JwksSNIName: "idp.spec.example.com", - JwksSNIEnabled: true, + Key: "default/jwt-policy", + Realm: "Spec Realm API", + KeyCache: "1h", JwksURI: version2.JwksURI{ - JwksScheme: "https", - JwksHost: "idp.spec.example.com", - JwksPort: "443", - JwksPath: "/spec-keys", + JwksScheme: "https", + JwksHost: "idp.spec.example.com", + JwksPort: "443", + JwksPath: "/spec-keys", + JwksSNIName: "idp.spec.example.com", + JwksSNIEnabled: true, }, }, JWKSAuthEnabled: true, diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 6061d59517..ffeb523425 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -652,13 +652,13 @@ type VariableCondition struct { // JWTAuth holds JWT authentication configuration. type JWTAuth struct { - Realm string `json:"realm"` - Secret string `json:"secret"` - Token string `json:"token"` - JwksURI string `json:"jwksURI"` - KeyCache string `json:"keyCache"` - SNIEnabled bool `json:"sniEnabled"` - SNIServerName string `json:"sniName"` + Realm string `json:"realm"` + Secret string `json:"secret"` + Token string `json:"token"` + JwksURI string `json:"jwksURI"` + KeyCache string `json:"keyCache"` + SNIEnabled bool `json:"sniEnabled"` + SNIName string `json:"sniName"` } // BasicAuth holds HTTP Basic authentication configuration diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index 0e523106dc..88d319fb7c 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -216,18 +216,18 @@ func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { } // if SNI server name is provided, but SNI is not enabled, return an error - if jwt.SNIServerName != "" && !jwt.SNIEnabled { + if jwt.SNIName != "" && !jwt.SNIEnabled { allErrs = append(allErrs, field.Forbidden(fieldPath.Child("sniServerName"), "sniServerName can only be set when sniEnabled is true")) } // if SNI is enabled and SNI server name is provided, make sure it's a valid URI - if jwt.SNIEnabled && jwt.SNIServerName != "" { - err := validation2.ValidateURI(jwt.SNIServerName, + if jwt.SNIEnabled && jwt.SNIName != "" { + err := validation2.ValidateURI(jwt.SNIName, validation2.WithAllowedSchemes("https"), validation2.WithUserAllowed(false), validation2.WithDefaultScheme("https")) if err != nil { - allErrs = append(allErrs, field.Invalid(fieldPath.Child("sniServerName"), jwt.SNIServerName, "sniServerName is not a valid URI")) + allErrs = append(allErrs, field.Invalid(fieldPath.Child("sniServerName"), jwt.SNIName, "sniServerName is not a valid URI")) } } } diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 738516bad5..a2096afe48 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -100,10 +100,10 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { policy: &v1.Policy{ Spec: v1.PolicySpec{ JWTAuth: &v1.JWTAuth{ - Realm: "My Product API", - JwksURI: "https://myjwksuri.com", - KeyCache: "1h", - SNIServerName: "ipd.org", + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + KeyCache: "1h", + SNIName: "ipd.org", }, }, }, @@ -113,11 +113,11 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { policy: &v1.Policy{ Spec: v1.PolicySpec{ JWTAuth: &v1.JWTAuth{ - Realm: "My Product API", - JwksURI: "https://myjwksuri.com", - KeyCache: "1h", - SNIEnabled: true, - SNIServerName: "msql://ipd.org", + Realm: "My Product API", + JwksURI: "https://myjwksuri.com", + KeyCache: "1h", + SNIEnabled: true, + SNIName: "msql://ipd.org", }, }, }, @@ -209,11 +209,11 @@ func TestValidatePolicy_IsValidOnJWTPolicy(t *testing.T) { policy: &v1.Policy{ Spec: v1.PolicySpec{ JWTAuth: &v1.JWTAuth{ - Realm: "My Product API", - KeyCache: "1h", - JwksURI: "https://login.mydomain.com/keys", - SNIEnabled: true, - SNIServerName: "https://example.org", + Realm: "My Product API", + KeyCache: "1h", + JwksURI: "https://login.mydomain.com/keys", + SNIEnabled: true, + SNIName: "https://example.org", }, }, }, @@ -843,12 +843,12 @@ func TestValidateJWT_PassesOnValidInput(t *testing.T) { }, { jwt: &v1.JWTAuth{ - Realm: "My Product API", - Token: "$cookie_auth_token", - JwksURI: "https://idp.com/token", - KeyCache: "1h", - SNIEnabled: true, - SNIServerName: "https://ipd.com:9999", + Realm: "My Product API", + Token: "$cookie_auth_token", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: true, + SNIName: "https://ipd.com:9999", }, msg: "SNI enabled and valid SNI server name", }, @@ -967,30 +967,30 @@ func TestValidateJWT_FailsOnInvalidInput(t *testing.T) { }, { jwt: &v1.JWTAuth{ - Realm: "My Product api", - JwksURI: "https://idp.com/token", - KeyCache: "1h", - SNIEnabled: true, - SNIServerName: "msql://not-\\\\a-valid-sni", + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: true, + SNIName: "msql://not-\\\\a-valid-sni", }, msg: "invalid SNI server name", }, { jwt: &v1.JWTAuth{ - Realm: "My Product api", - JwksURI: "https://idp.com/token", - KeyCache: "1h", - SNIEnabled: false, - SNIServerName: "https://idp.com", + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIEnabled: false, + SNIName: "https://idp.com", }, msg: "SNI server name passed, SNI not enabled", }, { jwt: &v1.JWTAuth{ - Realm: "My Product api", - JwksURI: "https://idp.com/token", - KeyCache: "1h", - SNIServerName: "https://idp.com", + Realm: "My Product api", + JwksURI: "https://idp.com/token", + KeyCache: "1h", + SNIName: "https://idp.com", }, msg: "SNI server name passed, SNI not passed", }, From 7c2c3e4f5540cbe1cbdebed95374e62c7a2df528 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Tue, 8 Jul 2025 17:11:09 +0100 Subject: [PATCH 07/12] If JwksURI is not set, SNI should not be set --- pkg/apis/configuration/validation/policy.go | 11 ++++++++ .../configuration/validation/policy_test.go | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index 88d319fb7c..fe24f736b7 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -202,6 +202,17 @@ func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { return allErrs } + if jwt.JwksURI == "" { + // If JwksURI is not set, then none of the SNI fields should be set. + if jwt.SNIEnabled { + return append(allErrs, field.Forbidden(fieldPath.Child("sniEnabled"), "sniEnabled can only be set when JwksURI is set")) + } + + if jwt.SNIName != "" { + return append(allErrs, field.Forbidden(fieldPath.Child("sniName"), "sniName can only be set when JwksURI is set")) + } + } + // Verify a case when using JWKS if jwt.JwksURI != "" { allErrs = append(allErrs, validateURL(jwt.JwksURI, fieldPath.Child("JwksURI"))...) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index a2096afe48..56b28d77bb 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -994,6 +994,31 @@ func TestValidateJWT_FailsOnInvalidInput(t *testing.T) { }, msg: "SNI server name passed, SNI not passed", }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIEnabled: true, + }, + msg: "Jwks URI not set, but SNI is enabled", + }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIName: "https://idp.com", + }, + msg: "Jwks URI not set, but SNIName is set", + }, + { + jwt: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIName: "https://idp.com", + SNIEnabled: true, + }, + msg: "Jwks URI not set, but SNIName is set and SNI is enabled", + }, } for _, test := range tests { test := test From 5cf8aa9047854341b4380d045c9078909761dbd0 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:06:38 +0100 Subject: [PATCH 08/12] add test to routes and include docs to crd Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- config/crd/bases/k8s.nginx.org_policies.yaml | 6 ++++++ deploy/crds.yaml | 6 ++++++ .../version2/__snapshots__/templates_test.snap | 2 ++ internal/configs/version2/templates_test.go | 10 ++++++---- pkg/apis/configuration/v1/types.go | 16 +++++++++------- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index d8955c11b8..46745fa740 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -142,8 +142,14 @@ spec: secret: type: string sniEnabled: + description: Enables SNI (Server Name Indication) for the JWT + policy. This is useful when the remote server requires SNI to + serve the correct certificate. type: boolean sniName: + description: The SNI name to use when connecting to the remote + server. If not set, the hostname from the ``jwksURI`` will be + used. type: string token: type: string diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 7aa8f489ca..512f816d13 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -304,8 +304,14 @@ spec: secret: type: string sniEnabled: + description: Enables SNI (Server Name Indication) for the JWT + policy. This is useful when the remote server requires SNI to + serve the correct certificate. type: boolean sniName: + description: The SNI name to use when connecting to the remote + server. If not set, the hostname from the ``jwksURI`` will be + used. type: string token: type: string diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index ba7a51ec9c..581fe23d00 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -1127,6 +1127,8 @@ server { proxy_set_header Content-Length ""; proxy_cache jwks_uri_cafe; proxy_cache_valid 200 12h; + proxy_ssl_server_name on; + proxy_ssl_name sni.idp.spec.example.com; proxy_set_header Host idp.route.example.com; set $idp_backend idp.route.example.com; proxy_pass http://$idp_backend:80/route-keys; diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 20b4f62896..beaa511dfa 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -2368,10 +2368,12 @@ var ( Token: "$http_token", KeyCache: "1h", JwksURI: JwksURI{ - JwksScheme: "http", - JwksHost: "idp.route.example.com", - JwksPort: "80", - JwksPath: "/route-keys", + JwksScheme: "http", + JwksHost: "idp.route.example.com", + JwksPort: "80", + JwksPath: "/route-keys", + JwksSNIEnabled: true, + JwksSNIName: "sni.idp.spec.example.com", }, }, }, diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index ffeb523425..6d21cc8622 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -652,13 +652,15 @@ type VariableCondition struct { // JWTAuth holds JWT authentication configuration. type JWTAuth struct { - Realm string `json:"realm"` - Secret string `json:"secret"` - Token string `json:"token"` - JwksURI string `json:"jwksURI"` - KeyCache string `json:"keyCache"` - SNIEnabled bool `json:"sniEnabled"` - SNIName string `json:"sniName"` + Realm string `json:"realm"` + Secret string `json:"secret"` + Token string `json:"token"` + JwksURI string `json:"jwksURI"` + KeyCache string `json:"keyCache"` + // Enables SNI (Server Name Indication) for the JWT policy. This is useful when the remote server requires SNI to serve the correct certificate. + SNIEnabled bool `json:"sniEnabled"` + // The SNI name to use when connecting to the remote server. If not set, the hostname from the ``jwksURI`` will be used. + SNIName string `json:"sniName"` } // BasicAuth holds HTTP Basic authentication configuration From 495e3b6150341815606b8de58326e1385dce1917 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Thu, 17 Jul 2025 02:11:56 +0100 Subject: [PATCH 09/12] add unit test for validation Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- pkg/apis/configuration/validation/policy_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 56b28d77bb..a621ed0e3d 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -95,6 +95,20 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { }, }, }, + { + name: "If JwksURI is not set, then none of the SNI fields should be set.", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Secret: "my-jwk", + KeyCache: "1h", + SNIName: "ipd.org", + SNIEnabled: true, + }, + }, + }, + }, { name: "SNI server name passed, but SNI not enabled", policy: &v1.Policy{ From e4f2e1a5a01acea755324f31cc5af858f995fae7 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:59:56 +0100 Subject: [PATCH 10/12] test codecov Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- pkg/apis/configuration/validation/policy.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index fe24f736b7..96866c4821 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -199,10 +199,7 @@ func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { if jwt.KeyCache != "" { allErrs = append(allErrs, field.Forbidden(fieldPath.Child("keyCache"), "key cache must not be used when using Secret")) } - return allErrs - } - if jwt.JwksURI == "" { // If JwksURI is not set, then none of the SNI fields should be set. if jwt.SNIEnabled { return append(allErrs, field.Forbidden(fieldPath.Child("sniEnabled"), "sniEnabled can only be set when JwksURI is set")) @@ -211,6 +208,8 @@ func validateJWT(jwt *v1.JWTAuth, fieldPath *field.Path) field.ErrorList { if jwt.SNIName != "" { return append(allErrs, field.Forbidden(fieldPath.Child("sniName"), "sniName can only be set when JwksURI is set")) } + + return allErrs } // Verify a case when using JWKS From ca1db5fd76ee49581c9d72f0436277bcba74c894 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:37:49 +0100 Subject: [PATCH 11/12] edit tests Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- .../configuration/validation/policy_test.go | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index a621ed0e3d..aa49122bce 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -136,6 +136,70 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { }, }, }, + { + name: "SNI server name passed, SNI enabled, but no JwksURI", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIEnabled: true, + }, + }, + }, + }, + { + name: "Jwks URI not set, but SNIName is set", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIName: "https://idp.com", + }, + }, + }, + }, + { + name: "Jwks URI not set, SNIName is set and SNI is enabled", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + SNIName: "https://idp.com", + SNIEnabled: true, + }, + }, + }, + }, + { + name: "Jwks URI not set, Secret set, but SNIName is set and SNI is enabled", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + Secret: "my-jwk", + SNIName: "https://idp.com", + SNIEnabled: true, + }, + }, + }, + }, + { + name: "Jwks URI not set, SNIName set, but SNI is not enabled", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + JWTAuth: &v1.JWTAuth{ + Realm: "My Product API", + Token: "$cookie_auth_token", + Secret: "my-jwk", + SNIName: "https://idp.com", + }, + }, + }, + }, } for _, tc := range tt { @@ -1008,31 +1072,6 @@ func TestValidateJWT_FailsOnInvalidInput(t *testing.T) { }, msg: "SNI server name passed, SNI not passed", }, - { - jwt: &v1.JWTAuth{ - Realm: "My Product API", - Token: "$cookie_auth_token", - SNIEnabled: true, - }, - msg: "Jwks URI not set, but SNI is enabled", - }, - { - jwt: &v1.JWTAuth{ - Realm: "My Product API", - Token: "$cookie_auth_token", - SNIName: "https://idp.com", - }, - msg: "Jwks URI not set, but SNIName is set", - }, - { - jwt: &v1.JWTAuth{ - Realm: "My Product API", - Token: "$cookie_auth_token", - SNIName: "https://idp.com", - SNIEnabled: true, - }, - msg: "Jwks URI not set, but SNIName is set and SNI is enabled", - }, } for _, test := range tests { test := test From 1de6c837f689292e25c9cc3975d03f6b4603d26b Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Mon, 21 Jul 2025 03:01:58 +0100 Subject: [PATCH 12/12] remove redundant tests Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- pkg/apis/configuration/validation/policy_test.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index aa49122bce..4dc5b2b739 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -137,7 +137,7 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { }, }, { - name: "SNI server name passed, SNI enabled, but no JwksURI", + name: "SNI enabled, but no JwksURI", policy: &v1.Policy{ Spec: v1.PolicySpec{ JWTAuth: &v1.JWTAuth{ @@ -160,19 +160,6 @@ func TestValidatePolicy_JWTIsNotValidOn(t *testing.T) { }, }, }, - { - name: "Jwks URI not set, SNIName is set and SNI is enabled", - policy: &v1.Policy{ - Spec: v1.PolicySpec{ - JWTAuth: &v1.JWTAuth{ - Realm: "My Product API", - Token: "$cookie_auth_token", - SNIName: "https://idp.com", - SNIEnabled: true, - }, - }, - }, - }, { name: "Jwks URI not set, Secret set, but SNIName is set and SNI is enabled", policy: &v1.Policy{