Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ spec:
type: string
secret:
type: string
sniEnabled:
type: boolean
sniName:
type: string
token:
type: string
type: object
Expand Down
4 changes: 4 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ spec:
type: string
secret:
type: string
sniEnabled:
type: boolean
sniName:
type: string
token:
type: string
type: object
Expand Down
2 changes: 2 additions & 0 deletions internal/configs/version2/__snapshots__/templates_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 6 additions & 4 deletions internal/configs/version2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,10 +438,12 @@ type JWTAuth struct {

// 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
Expand Down
6 changes: 6 additions & 0 deletions internal/configs/version2/nginx-plus.virtualserver.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ server {
proxy_cache_valid 200 12h;
{{- end }}
{{- with .JwksURI }}
{{- if .JwksSNIEnabled }}
proxy_ssl_server_name on;
{{- if .JwksSNIName }}
proxy_ssl_name {{ .JwksSNIName }};
{{- end }}
{{- end }}
proxy_set_header Host {{ .JwksHost }};
set $idp_backend {{ .JwksHost }};
proxy_pass {{ .JwksScheme}}://$idp_backend{{ if .JwksPort }}:{{ .JwksPort }}{{ end }}{{ .JwksPath }};
Expand Down
19 changes: 15 additions & 4 deletions internal/configs/version2/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -2345,10 +2354,12 @@ var (
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": {
Expand Down
10 changes: 6 additions & 4 deletions internal/configs/virtualserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1169,10 +1169,12 @@ 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{
Expand Down
28 changes: 17 additions & 11 deletions internal/configs/virtualserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
SNIName: "idp.spec.example.com",
},
},
},
Expand Down Expand Up @@ -5713,10 +5715,12 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) {
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": {
Expand All @@ -5736,10 +5740,12 @@ func TestGenerateVirtualServerConfigJWKSPolicy(t *testing.T) {
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,
Expand Down
12 changes: 7 additions & 5 deletions pkg/apis/configuration/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
SNIName string `json:"sniName"`
}

// BasicAuth holds HTTP Basic authentication configuration
Expand Down
29 changes: 28 additions & 1 deletion pkg/apis/configuration/validation/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"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"
Expand Down Expand Up @@ -201,6 +202,17 @@
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"))
}

Check warning on line 209 in pkg/apis/configuration/validation/policy.go

View check run for this annotation

Codecov / codecov/patch

pkg/apis/configuration/validation/policy.go#L206-L209

Added lines #L206 - L209 were not covered by tests

if jwt.SNIName != "" {
return append(allErrs, field.Forbidden(fieldPath.Child("sniName"), "sniName can only be set when JwksURI is set"))
}

Check warning on line 213 in pkg/apis/configuration/validation/policy.go

View check run for this annotation

Codecov / codecov/patch

pkg/apis/configuration/validation/policy.go#L211-L213

Added lines #L211 - L213 were not covered by tests
}

// Verify a case when using JWKS
if jwt.JwksURI != "" {
allErrs = append(allErrs, validateURL(jwt.JwksURI, fieldPath.Child("JwksURI"))...)
Expand All @@ -213,7 +225,22 @@
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.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.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.SNIName, "sniServerName is not a valid URI"))
}
}
}
return allErrs
}
Expand Down
129 changes: 129 additions & 0 deletions pkg/apis/configuration/validation/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
SNIName: "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,
SNIName: "msql://ipd.org",
},
},
},
},
}

for _, tc := range tt {
Expand Down Expand Up @@ -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,
SNIName: "https://example.org",
},
},
},
},
}

for _, tc := range tt {
Expand Down Expand Up @@ -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,
SNIName: "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"))
Expand Down Expand Up @@ -890,6 +965,60 @@ 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,
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,
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",
SNIName: "https://idp.com",
},
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
Expand Down
Loading