Skip to content

Commit 75f0850

Browse files
authored
add sslVerify for jwksUri (#8292)
1 parent f5ce4b4 commit 75f0850

File tree

14 files changed

+644
-22
lines changed

14 files changed

+644
-22
lines changed

config/crd/bases/k8s.nginx.org_policies.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,13 +304,32 @@ spec:
304304
server. If not set, the hostname from the ``jwksURI`` will be
305305
used.
306306
type: string
307+
sslVerify:
308+
default: false
309+
description: Enables verification of the JWKS server SSL certificate.
310+
Default is false.
311+
type: boolean
312+
sslVerifyDepth:
313+
default: 1
314+
description: Sets the verification depth in the JWKS server certificates
315+
chain. The default is 1.
316+
minimum: 0
317+
type: integer
307318
token:
308319
description: 'The token specifies a variable that contains the
309320
JSON Web Token. By default the JWT is passed in the Authorization
310321
header as a Bearer Token. JWT may be also passed as a cookie
311322
or a part of a query string, for example: $cookie_auth_token.
312323
Accepted variables are $http_, $arg_, $cookie_.'
313324
type: string
325+
trustedCertSecret:
326+
description: The name of the Kubernetes secret that stores the
327+
CA certificate for JWKS server verification. It must be in the
328+
same namespace as the Policy resource. The secret must be of
329+
the type nginx.org/ca, and the certificate must be stored in
330+
the secret under the key ca.crt.
331+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
332+
type: string
314333
type: object
315334
oidc:
316335
description: The OpenID Connect policy configures NGINX to authenticate

deploy/crds.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,13 +475,32 @@ spec:
475475
server. If not set, the hostname from the ``jwksURI`` will be
476476
used.
477477
type: string
478+
sslVerify:
479+
default: false
480+
description: Enables verification of the JWKS server SSL certificate.
481+
Default is false.
482+
type: boolean
483+
sslVerifyDepth:
484+
default: 1
485+
description: Sets the verification depth in the JWKS server certificates
486+
chain. The default is 1.
487+
minimum: 0
488+
type: integer
478489
token:
479490
description: 'The token specifies a variable that contains the
480491
JSON Web Token. By default the JWT is passed in the Authorization
481492
header as a Bearer Token. JWT may be also passed as a cookie
482493
or a part of a query string, for example: $cookie_auth_token.
483494
Accepted variables are $http_, $arg_, $cookie_.'
484495
type: string
496+
trustedCertSecret:
497+
description: The name of the Kubernetes secret that stores the
498+
CA certificate for JWKS server verification. It must be in the
499+
same namespace as the Policy resource. The secret must be of
500+
the type nginx.org/ca, and the certificate must be stored in
501+
the secret under the key ca.crt.
502+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
503+
type: string
485504
type: object
486505
oidc:
487506
description: The OpenID Connect policy configures NGINX to authenticate

docs/crd/k8s.nginx.org_policies.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ The `.spec` object supports the following fields:
5858
| `jwt.secret` | `string` | The name of the Kubernetes secret that stores the Htpasswd configuration. It must be in the same namespace as the Policy resource. The secret must be of the type nginx.org/htpasswd, and the config must be stored in the secret under the key htpasswd, otherwise the secret will be rejected as invalid. |
5959
| `jwt.sniEnabled` | `boolean` | Enables SNI (Server Name Indication) for the JWT policy. This is useful when the remote server requires SNI to serve the correct certificate. |
6060
| `jwt.sniName` | `string` | The SNI name to use when connecting to the remote server. If not set, the hostname from the ``jwksURI`` will be used. |
61+
| `jwt.sslVerify` | `boolean` | Enables verification of the JWKS server SSL certificate. Default is false. |
62+
| `jwt.sslVerifyDepth` | `integer` | Sets the verification depth in the JWKS server certificates chain. The default is 1. |
6163
| `jwt.token` | `string` | The token specifies a variable that contains the JSON Web Token. By default the JWT is passed in the Authorization header as a Bearer Token. JWT may be also passed as a cookie or a part of a query string, for example: $cookie_auth_token. Accepted variables are $http_, $arg_, $cookie_. |
64+
| `jwt.trustedCertSecret` | `string` | The name of the Kubernetes secret that stores the CA certificate for JWKS server verification. It must be in the same namespace as the Policy resource. The secret must be of the type nginx.org/ca, and the certificate must be stored in the secret under the key ca.crt. |
6265
| `oidc` | `object` | The OpenID Connect policy configures NGINX to authenticate client requests by validating a JWT token against an OAuth2/OIDC token provider, such as Auth0 or Keycloak. |
6366
| `oidc.accessTokenEnable` | `boolean` | Option of whether Bearer token is used to authorize NGINX to access protected backend. |
6467
| `oidc.authEndpoint` | `string` | URL for the authorization endpoint provided by your OpenID Connect provider. |

internal/configs/version2/__snapshots__/templates_test.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,7 @@ server {
12591259
proxy_set_header Content-Length "";
12601260
proxy_ssl_server_name on;
12611261
proxy_ssl_name sni.idp.spec.example.com;
1262+
proxy_ssl_verify off;
12621263
proxy_pass_request_headers off;
12631264
proxy_pass_request_body off;
12641265
proxy_set_header Host idp.spec.example.com;
@@ -1271,6 +1272,7 @@ server {
12711272
proxy_set_header Content-Length "";
12721273
proxy_ssl_server_name on;
12731274
proxy_ssl_name sni.idp.spec.example.com;
1275+
proxy_ssl_verify off;
12741276
proxy_pass_request_headers off;
12751277
proxy_pass_request_body off;
12761278
proxy_set_header Host idp.route.example.com;
@@ -1380,6 +1382,7 @@ server {
13801382
internal;
13811383
proxy_method GET;
13821384
proxy_set_header Content-Length "";
1385+
proxy_ssl_verify off;
13831386
proxy_pass_request_headers off;
13841387
proxy_pass_request_body off;
13851388
proxy_set_header Host idp.spec.example.com;
@@ -1390,6 +1393,7 @@ server {
13901393
internal;
13911394
proxy_method GET;
13921395
proxy_set_header Content-Length "";
1396+
proxy_ssl_verify off;
13931397
proxy_pass_request_headers off;
13941398
proxy_pass_request_body off;
13951399
proxy_set_header Host idp.route.example.com;

internal/configs/version2/http.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ type JwksURI struct {
448448
JwksPath string
449449
JwksSNIName string
450450
JwksSNIEnabled bool
451+
SSLVerify bool
452+
TrustedCert string
453+
SSLVerifyDepth int
451454
}
452455

453456
// BasicAuth refers to basic HTTP authentication mechanism options

internal/configs/version2/nginx-plus.virtualserver.tmpl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,17 @@ server {
293293
proxy_ssl_name {{ .JwksSNIName }};
294294
{{- end }}
295295
{{- end }}
296+
{{- if .SSLVerify }}
297+
proxy_ssl_verify on;
298+
proxy_ssl_verify_depth {{ .SSLVerifyDepth }};
299+
{{- if .TrustedCert }}
300+
proxy_ssl_trusted_certificate {{ .TrustedCert }};
301+
{{- else }}
302+
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
303+
{{- end }}
304+
{{- else }}
305+
proxy_ssl_verify off;
306+
{{- end }}
296307
proxy_pass_request_headers off;
297308
proxy_pass_request_body off;
298309
proxy_set_header Host {{ .JwksHost }};

internal/configs/version2/templates_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,4 +3031,161 @@ var (
30313031
},
30323032
},
30333033
}
3034+
3035+
// JWT SSL Verification test configs
3036+
virtualServerCfgWithJWTSSLDefaultCert = VirtualServerConfig{
3037+
Upstreams: []Upstream{
3038+
{
3039+
Name: "vs_default_cafe_tea",
3040+
Servers: []UpstreamServer{
3041+
{Address: "10.0.0.20:80"},
3042+
},
3043+
},
3044+
},
3045+
Server: Server{
3046+
JWTAuthList: map[string]*JWTAuth{
3047+
"default/jwt-ssl-policy": {
3048+
Key: "default/jwt-ssl-policy",
3049+
Realm: "SSL API",
3050+
KeyCache: "1h",
3051+
JwksURI: JwksURI{
3052+
JwksScheme: "https",
3053+
JwksHost: "idp.example.com",
3054+
JwksPath: "/keys",
3055+
SSLVerify: true,
3056+
SSLVerifyDepth: 1,
3057+
},
3058+
},
3059+
},
3060+
Locations: []Location{
3061+
{
3062+
Path: "/",
3063+
ProxyPass: "http://vs_default_cafe_tea",
3064+
JWTAuth: &JWTAuth{
3065+
Key: "default/jwt-ssl-policy",
3066+
},
3067+
},
3068+
},
3069+
},
3070+
}
3071+
3072+
virtualServerCfgWithJWTSSLSecretCert = VirtualServerConfig{
3073+
Upstreams: []Upstream{
3074+
{
3075+
Name: "vs_default_cafe_tea",
3076+
Servers: []UpstreamServer{
3077+
{Address: "10.0.0.20:80"},
3078+
},
3079+
},
3080+
},
3081+
Server: Server{
3082+
JWTAuthList: map[string]*JWTAuth{
3083+
"default/jwt-ssl-policy": {
3084+
Key: "default/jwt-ssl-policy",
3085+
Realm: "SSL API",
3086+
KeyCache: "1h",
3087+
JwksURI: JwksURI{
3088+
JwksScheme: "https",
3089+
JwksHost: "idp.example.com",
3090+
JwksPath: "/keys",
3091+
SSLVerify: true,
3092+
TrustedCert: "/etc/nginx/secrets/default-my-ca-secret",
3093+
SSLVerifyDepth: 2,
3094+
},
3095+
},
3096+
},
3097+
Locations: []Location{
3098+
{
3099+
Path: "/",
3100+
ProxyPass: "http://vs_default_cafe_tea",
3101+
JWTAuth: &JWTAuth{
3102+
Key: "default/jwt-ssl-policy",
3103+
},
3104+
},
3105+
},
3106+
},
3107+
}
3108+
3109+
virtualServerCfgWithJWTNoSSL = VirtualServerConfig{
3110+
Upstreams: []Upstream{
3111+
{
3112+
Name: "vs_default_cafe_tea",
3113+
Servers: []UpstreamServer{
3114+
{Address: "10.0.0.20:80"},
3115+
},
3116+
},
3117+
},
3118+
Server: Server{
3119+
JWTAuthList: map[string]*JWTAuth{
3120+
"default/jwt-no-ssl-policy": {
3121+
Key: "default/jwt-no-ssl-policy",
3122+
Realm: "No SSL API",
3123+
KeyCache: "1h",
3124+
JwksURI: JwksURI{
3125+
JwksScheme: "https",
3126+
JwksHost: "idp.example.com",
3127+
JwksPath: "/keys",
3128+
SSLVerify: false,
3129+
},
3130+
},
3131+
},
3132+
Locations: []Location{
3133+
{
3134+
Path: "/",
3135+
ProxyPass: "http://vs_default_cafe_tea",
3136+
JWTAuth: &JWTAuth{
3137+
Key: "default/jwt-no-ssl-policy",
3138+
},
3139+
},
3140+
},
3141+
},
3142+
}
30343143
)
3144+
3145+
func TestJWTSSLVerificationDefaultCert(t *testing.T) {
3146+
t.Parallel()
3147+
executor := newTmplExecutorNGINXPlus(t)
3148+
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTSSLDefaultCert)
3149+
if err != nil {
3150+
t.Error(err)
3151+
}
3152+
if !bytes.Contains(got, []byte("proxy_ssl_verify on;")) {
3153+
t.Error("want `proxy_ssl_verify on;` in generated template")
3154+
}
3155+
if !bytes.Contains(got, []byte("proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;")) {
3156+
t.Error("want system CA bundle path in generated template")
3157+
}
3158+
}
3159+
3160+
func TestJWTSSLVerificationSecretCert(t *testing.T) {
3161+
t.Parallel()
3162+
executor := newTmplExecutorNGINXPlus(t)
3163+
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTSSLSecretCert)
3164+
if err != nil {
3165+
t.Error(err)
3166+
}
3167+
if !bytes.Contains(got, []byte("proxy_ssl_verify on;")) {
3168+
t.Error("want `proxy_ssl_verify on;` in generated template")
3169+
}
3170+
if !bytes.Contains(got, []byte("proxy_ssl_trusted_certificate /etc/nginx/secrets/default-my-ca-secret;")) {
3171+
t.Error("want custom CA secret path in generated template")
3172+
}
3173+
if !bytes.Contains(got, []byte("proxy_ssl_verify_depth 2;")) {
3174+
t.Error("want custom SSL verify depth in generated template")
3175+
}
3176+
}
3177+
3178+
func TestJWTNoSSLVerification(t *testing.T) {
3179+
t.Parallel()
3180+
executor := newTmplExecutorNGINXPlus(t)
3181+
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTNoSSL)
3182+
if err != nil {
3183+
t.Error(err)
3184+
}
3185+
if !bytes.Contains(got, []byte("proxy_ssl_verify off;")) {
3186+
t.Error("want `proxy_ssl_verify off;` in generated template")
3187+
}
3188+
if bytes.Contains(got, []byte("proxy_ssl_trusted_certificate")) {
3189+
t.Error("want no SSL trusted certificate directive in generated template")
3190+
}
3191+
}

internal/configs/virtualserver.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,13 +1188,54 @@ func (p *policiesCfg) addJWTAuthConfig(
11881188
} else if jwtAuth.JwksURI != "" {
11891189
uri, _ := url.Parse(jwtAuth.JwksURI)
11901190

1191+
// Handle SSL verification for JWKS
1192+
var trustedCertPath string
1193+
if jwtAuth.SSLVerify && jwtAuth.TrustedCertSecret != "" {
1194+
trustedCertSecretKey := fmt.Sprintf("%s/%s", polNamespace, jwtAuth.TrustedCertSecret)
1195+
trustedCertSecretRef := secretRefs[trustedCertSecretKey]
1196+
1197+
// Check if secret reference exists
1198+
if trustedCertSecretRef == nil {
1199+
res.addWarningf("JWT policy %s references a non-existent trusted cert secret %s", polKey, trustedCertSecretKey)
1200+
res.isError = true
1201+
return res
1202+
}
1203+
1204+
var secretType api_v1.SecretType
1205+
if trustedCertSecretRef.Secret != nil {
1206+
secretType = trustedCertSecretRef.Secret.Type
1207+
}
1208+
if secretType != "" && secretType != secrets.SecretTypeCA {
1209+
res.addWarningf("JWT policy %s references a secret %s of a wrong type '%s', must be '%s'", polKey, trustedCertSecretKey, secretType, secrets.SecretTypeCA)
1210+
res.isError = true
1211+
return res
1212+
} else if trustedCertSecretRef.Error != nil {
1213+
res.addWarningf("JWT policy %s references an invalid trusted cert secret %s: %v", polKey, trustedCertSecretKey, trustedCertSecretRef.Error)
1214+
res.isError = true
1215+
return res
1216+
}
1217+
1218+
caFields := strings.Fields(trustedCertSecretRef.Path)
1219+
if len(caFields) > 0 {
1220+
trustedCertPath = caFields[0]
1221+
}
1222+
}
1223+
1224+
sslVerifyDepth := 1
1225+
if jwtAuth.SSLVerifyDepth != nil {
1226+
sslVerifyDepth = *jwtAuth.SSLVerifyDepth
1227+
}
1228+
11911229
JwksURI := &version2.JwksURI{
11921230
JwksScheme: uri.Scheme,
11931231
JwksHost: uri.Hostname(),
11941232
JwksPort: uri.Port(),
11951233
JwksPath: uri.Path,
11961234
JwksSNIName: jwtAuth.SNIName,
11971235
JwksSNIEnabled: jwtAuth.SNIEnabled,
1236+
SSLVerify: jwtAuth.SSLVerify,
1237+
TrustedCert: trustedCertPath,
1238+
SSLVerifyDepth: sslVerifyDepth,
11981239
}
11991240

12001241
p.JWTAuth.Auth = &version2.JWTAuth{

0 commit comments

Comments
 (0)