Skip to content
Merged
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
19 changes: 19 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,13 +304,32 @@ spec:
server. If not set, the hostname from the ``jwksURI`` will be
used.
type: string
sslVerify:
default: false
description: Enables verification of the JWKS server SSL certificate.
Default is false.
type: boolean
sslVerifyDepth:
default: 1
description: Sets the verification depth in the JWKS server certificates
chain. The default is 1.
minimum: 0
type: integer
token:
description: '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_.'
type: string
trustedCertSecret:
description: 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.
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
type: object
oidc:
description: The OpenID Connect policy configures NGINX to authenticate
Expand Down
19 changes: 19 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,32 @@ spec:
server. If not set, the hostname from the ``jwksURI`` will be
used.
type: string
sslVerify:
default: false
description: Enables verification of the JWKS server SSL certificate.
Default is false.
type: boolean
sslVerifyDepth:
default: 1
description: Sets the verification depth in the JWKS server certificates
chain. The default is 1.
minimum: 0
type: integer
token:
description: '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_.'
type: string
trustedCertSecret:
description: 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.
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
type: object
oidc:
description: The OpenID Connect policy configures NGINX to authenticate
Expand Down
3 changes: 3 additions & 0 deletions docs/crd/k8s.nginx.org_policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ The `.spec` object supports the following fields:
| `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. |
| `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. |
| `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. |
| `jwt.sslVerify` | `boolean` | Enables verification of the JWKS server SSL certificate. Default is false. |
| `jwt.sslVerifyDepth` | `integer` | Sets the verification depth in the JWKS server certificates chain. The default is 1. |
| `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_. |
| `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. |
| `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. |
| `oidc.accessTokenEnable` | `boolean` | Option of whether Bearer token is used to authorize NGINX to access protected backend. |
| `oidc.authEndpoint` | `string` | URL for the authorization endpoint provided by your OpenID Connect provider. |
Expand Down
4 changes: 4 additions & 0 deletions internal/configs/version2/__snapshots__/templates_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ server {
proxy_set_header Content-Length "";
proxy_ssl_server_name on;
proxy_ssl_name sni.idp.spec.example.com;
proxy_ssl_verify off;
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_set_header Host idp.spec.example.com;
Expand All @@ -1271,6 +1272,7 @@ server {
proxy_set_header Content-Length "";
proxy_ssl_server_name on;
proxy_ssl_name sni.idp.spec.example.com;
proxy_ssl_verify off;
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_set_header Host idp.route.example.com;
Expand Down Expand Up @@ -1380,6 +1382,7 @@ server {
internal;
proxy_method GET;
proxy_set_header Content-Length "";
proxy_ssl_verify off;
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_set_header Host idp.spec.example.com;
Expand All @@ -1390,6 +1393,7 @@ server {
internal;
proxy_method GET;
proxy_set_header Content-Length "";
proxy_ssl_verify off;
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_set_header Host idp.route.example.com;
Expand Down
3 changes: 3 additions & 0 deletions internal/configs/version2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,9 @@ type JwksURI struct {
JwksPath string
JwksSNIName string
JwksSNIEnabled bool
SSLVerify bool
TrustedCert string
SSLVerifyDepth int
}

// BasicAuth refers to basic HTTP authentication mechanism options
Expand Down
11 changes: 11 additions & 0 deletions internal/configs/version2/nginx-plus.virtualserver.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,17 @@ server {
proxy_ssl_name {{ .JwksSNIName }};
{{- end }}
{{- end }}
{{- if .SSLVerify }}
proxy_ssl_verify on;
proxy_ssl_verify_depth {{ .SSLVerifyDepth }};
{{- if .TrustedCert }}
proxy_ssl_trusted_certificate {{ .TrustedCert }};
{{- else }}
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
{{- end }}
{{- else }}
proxy_ssl_verify off;
{{- end }}
proxy_pass_request_headers off;
proxy_pass_request_body off;
proxy_set_header Host {{ .JwksHost }};
Expand Down
157 changes: 157 additions & 0 deletions internal/configs/version2/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3031,4 +3031,161 @@ var (
},
},
}

// JWT SSL Verification test configs
virtualServerCfgWithJWTSSLDefaultCert = VirtualServerConfig{
Upstreams: []Upstream{
{
Name: "vs_default_cafe_tea",
Servers: []UpstreamServer{
{Address: "10.0.0.20:80"},
},
},
},
Server: Server{
JWTAuthList: map[string]*JWTAuth{
"default/jwt-ssl-policy": {
Key: "default/jwt-ssl-policy",
Realm: "SSL API",
KeyCache: "1h",
JwksURI: JwksURI{
JwksScheme: "https",
JwksHost: "idp.example.com",
JwksPath: "/keys",
SSLVerify: true,
SSLVerifyDepth: 1,
},
},
},
Locations: []Location{
{
Path: "/",
ProxyPass: "http://vs_default_cafe_tea",
JWTAuth: &JWTAuth{
Key: "default/jwt-ssl-policy",
},
},
},
},
}

virtualServerCfgWithJWTSSLSecretCert = VirtualServerConfig{
Upstreams: []Upstream{
{
Name: "vs_default_cafe_tea",
Servers: []UpstreamServer{
{Address: "10.0.0.20:80"},
},
},
},
Server: Server{
JWTAuthList: map[string]*JWTAuth{
"default/jwt-ssl-policy": {
Key: "default/jwt-ssl-policy",
Realm: "SSL API",
KeyCache: "1h",
JwksURI: JwksURI{
JwksScheme: "https",
JwksHost: "idp.example.com",
JwksPath: "/keys",
SSLVerify: true,
TrustedCert: "/etc/nginx/secrets/default-my-ca-secret",
SSLVerifyDepth: 2,
},
},
},
Locations: []Location{
{
Path: "/",
ProxyPass: "http://vs_default_cafe_tea",
JWTAuth: &JWTAuth{
Key: "default/jwt-ssl-policy",
},
},
},
},
}

virtualServerCfgWithJWTNoSSL = VirtualServerConfig{
Upstreams: []Upstream{
{
Name: "vs_default_cafe_tea",
Servers: []UpstreamServer{
{Address: "10.0.0.20:80"},
},
},
},
Server: Server{
JWTAuthList: map[string]*JWTAuth{
"default/jwt-no-ssl-policy": {
Key: "default/jwt-no-ssl-policy",
Realm: "No SSL API",
KeyCache: "1h",
JwksURI: JwksURI{
JwksScheme: "https",
JwksHost: "idp.example.com",
JwksPath: "/keys",
SSLVerify: false,
},
},
},
Locations: []Location{
{
Path: "/",
ProxyPass: "http://vs_default_cafe_tea",
JWTAuth: &JWTAuth{
Key: "default/jwt-no-ssl-policy",
},
},
},
},
}
)

func TestJWTSSLVerificationDefaultCert(t *testing.T) {
t.Parallel()
executor := newTmplExecutorNGINXPlus(t)
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTSSLDefaultCert)
if err != nil {
t.Error(err)
}
if !bytes.Contains(got, []byte("proxy_ssl_verify on;")) {
t.Error("want `proxy_ssl_verify on;` in generated template")
}
if !bytes.Contains(got, []byte("proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;")) {
t.Error("want system CA bundle path in generated template")
}
}

func TestJWTSSLVerificationSecretCert(t *testing.T) {
t.Parallel()
executor := newTmplExecutorNGINXPlus(t)
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTSSLSecretCert)
if err != nil {
t.Error(err)
}
if !bytes.Contains(got, []byte("proxy_ssl_verify on;")) {
t.Error("want `proxy_ssl_verify on;` in generated template")
}
if !bytes.Contains(got, []byte("proxy_ssl_trusted_certificate /etc/nginx/secrets/default-my-ca-secret;")) {
t.Error("want custom CA secret path in generated template")
}
if !bytes.Contains(got, []byte("proxy_ssl_verify_depth 2;")) {
t.Error("want custom SSL verify depth in generated template")
}
}

func TestJWTNoSSLVerification(t *testing.T) {
t.Parallel()
executor := newTmplExecutorNGINXPlus(t)
got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithJWTNoSSL)
if err != nil {
t.Error(err)
}
if !bytes.Contains(got, []byte("proxy_ssl_verify off;")) {
t.Error("want `proxy_ssl_verify off;` in generated template")
}
if bytes.Contains(got, []byte("proxy_ssl_trusted_certificate")) {
t.Error("want no SSL trusted certificate directive in generated template")
}
}
41 changes: 41 additions & 0 deletions internal/configs/virtualserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1188,13 +1188,54 @@ func (p *policiesCfg) addJWTAuthConfig(
} else if jwtAuth.JwksURI != "" {
uri, _ := url.Parse(jwtAuth.JwksURI)

// Handle SSL verification for JWKS
var trustedCertPath string
if jwtAuth.SSLVerify && jwtAuth.TrustedCertSecret != "" {
trustedCertSecretKey := fmt.Sprintf("%s/%s", polNamespace, jwtAuth.TrustedCertSecret)
trustedCertSecretRef := secretRefs[trustedCertSecretKey]

// Check if secret reference exists
if trustedCertSecretRef == nil {
res.addWarningf("JWT policy %s references a non-existent trusted cert secret %s", polKey, trustedCertSecretKey)
res.isError = true
return res
}

var secretType api_v1.SecretType
if trustedCertSecretRef.Secret != nil {
secretType = trustedCertSecretRef.Secret.Type
}
if secretType != "" && secretType != secrets.SecretTypeCA {
res.addWarningf("JWT policy %s references a secret %s of a wrong type '%s', must be '%s'", polKey, trustedCertSecretKey, secretType, secrets.SecretTypeCA)
res.isError = true
return res
} else if trustedCertSecretRef.Error != nil {
res.addWarningf("JWT policy %s references an invalid trusted cert secret %s: %v", polKey, trustedCertSecretKey, trustedCertSecretRef.Error)
res.isError = true
return res
}

caFields := strings.Fields(trustedCertSecretRef.Path)
if len(caFields) > 0 {
trustedCertPath = caFields[0]
}
}

sslVerifyDepth := 1
if jwtAuth.SSLVerifyDepth != nil {
sslVerifyDepth = *jwtAuth.SSLVerifyDepth
}

JwksURI := &version2.JwksURI{
JwksScheme: uri.Scheme,
JwksHost: uri.Hostname(),
JwksPort: uri.Port(),
JwksPath: uri.Path,
JwksSNIName: jwtAuth.SNIName,
JwksSNIEnabled: jwtAuth.SNIEnabled,
SSLVerify: jwtAuth.SSLVerify,
TrustedCert: trustedCertPath,
SSLVerifyDepth: sslVerifyDepth,
}

p.JWTAuth.Auth = &version2.JWTAuth{
Expand Down
Loading
Loading