added new auth-tls-match-cn annotation (#8434)
* added new auth-tls-match-cn annotation * added few more tests
This commit is contained in:
parent
81c2afd975
commit
f9372aa495
5 changed files with 116 additions and 0 deletions
|
@ -28,6 +28,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
||||||
|[nginx.ingress.kubernetes.io/auth-tls-verify-client](#client-certificate-authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-tls-verify-client](#client-certificate-authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-tls-error-page](#client-certificate-authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-tls-error-page](#client-certificate-authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream](#client-certificate-authentication)|"true" or "false"|
|
|[nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream](#client-certificate-authentication)|"true" or "false"|
|
||||||
|
|[nginx.ingress.kubernetes.io/auth-tls-match-cn](#client-certificate-authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-url](#external-authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-url](#external-authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-cache-key](#external-authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-cache-key](#external-authentication)|string|
|
||||||
|[nginx.ingress.kubernetes.io/auth-cache-duration](#external-authentication)|string|
|
|[nginx.ingress.kubernetes.io/auth-cache-duration](#external-authentication)|string|
|
||||||
|
@ -264,6 +265,7 @@ You can further customize client certificate authentication and behavior with th
|
||||||
* `optional_no_ca`: Do optional client certificate validation, but do not fail the request when the client certificate is not signed by the CAs from `auth-tls-secret`. Certificate verification result is sent to the upstream service.
|
* `optional_no_ca`: Do optional client certificate validation, but do not fail the request when the client certificate is not signed by the CAs from `auth-tls-secret`. Certificate verification result is sent to the upstream service.
|
||||||
* `nginx.ingress.kubernetes.io/auth-tls-error-page`: The URL/Page that user should be redirected in case of a Certificate Authentication Error
|
* `nginx.ingress.kubernetes.io/auth-tls-error-page`: The URL/Page that user should be redirected in case of a Certificate Authentication Error
|
||||||
* `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream`: Indicates if the received certificates should be passed or not to the upstream server in the header `ssl-client-cert`. Possible values are "true" or "false" (default).
|
* `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream`: Indicates if the received certificates should be passed or not to the upstream server in the header `ssl-client-cert`. Possible values are "true" or "false" (default).
|
||||||
|
* `nginx.ingress.kubernetes.io/auth-tls-match-cn`: Adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN=", example: `"CN=myvalidclient"`. If the certificate CN sent during mTLS does not match your string / regex it will fail with status code 403. Another way of using this is by adding multiple options in your regex, example: `"CN=(option1|option2|myvalidclient)"`. In this case, as long as one of the options in the brackets matches the certificate CN then you will receive a 200 status code.
|
||||||
|
|
||||||
The following headers are sent to the upstream service according to the `auth-tls-*` annotations:
|
The following headers are sent to the upstream service according to the `auth-tls-*` annotations:
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ package authtls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
networking "k8s.io/api/networking/v1"
|
networking "k8s.io/api/networking/v1"
|
||||||
|
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -35,6 +36,7 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||||
|
commonNameRegex = regexp.MustCompile(`CN=`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config contains the AuthSSLCert used for mutual authentication
|
// Config contains the AuthSSLCert used for mutual authentication
|
||||||
|
@ -45,6 +47,7 @@ type Config struct {
|
||||||
ValidationDepth int `json:"validationDepth"`
|
ValidationDepth int `json:"validationDepth"`
|
||||||
ErrorPage string `json:"errorPage"`
|
ErrorPage string `json:"errorPage"`
|
||||||
PassCertToUpstream bool `json:"passCertToUpstream"`
|
PassCertToUpstream bool `json:"passCertToUpstream"`
|
||||||
|
MatchCN string `json:"matchCN"`
|
||||||
AuthTLSError string
|
AuthTLSError string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,5 +130,10 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||||
config.PassCertToUpstream = false
|
config.PassCertToUpstream = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing)
|
||||||
|
if err != nil || !commonNameRegex.MatchString(config.MatchCN) {
|
||||||
|
config.MatchCN = ""
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,11 +128,15 @@ func TestAnnotations(t *testing.T) {
|
||||||
if u.PassCertToUpstream != false {
|
if u.PassCertToUpstream != false {
|
||||||
t.Errorf("expected %v but got %v", false, u.PassCertToUpstream)
|
t.Errorf("expected %v but got %v", false, u.PassCertToUpstream)
|
||||||
}
|
}
|
||||||
|
if u.MatchCN != "" {
|
||||||
|
t.Errorf("expected empty string, but got %v", u.MatchCN)
|
||||||
|
}
|
||||||
|
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error"
|
data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error"
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true"
|
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true"
|
||||||
|
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "CN=hello-app"
|
||||||
|
|
||||||
ing.SetAnnotations(data)
|
ing.SetAnnotations(data)
|
||||||
|
|
||||||
|
@ -161,6 +165,9 @@ func TestAnnotations(t *testing.T) {
|
||||||
if u.PassCertToUpstream != true {
|
if u.PassCertToUpstream != true {
|
||||||
t.Errorf("expected %v but got %v", true, u.PassCertToUpstream)
|
t.Errorf("expected %v but got %v", true, u.PassCertToUpstream)
|
||||||
}
|
}
|
||||||
|
if u.MatchCN != "CN=hello-app" {
|
||||||
|
t.Errorf("expected %v but got %v", "CN=hello-app", u.MatchCN)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidAnnotations(t *testing.T) {
|
func TestInvalidAnnotations(t *testing.T) {
|
||||||
|
@ -195,6 +202,7 @@ func TestInvalidAnnotations(t *testing.T) {
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t"
|
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t"
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
||||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
||||||
|
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "<script>nope</script>"
|
||||||
ing.SetAnnotations(data)
|
ing.SetAnnotations(data)
|
||||||
|
|
||||||
i, err := NewParser(fakeSecret).Parse(ing)
|
i, err := NewParser(fakeSecret).Parse(ing)
|
||||||
|
@ -215,6 +223,9 @@ func TestInvalidAnnotations(t *testing.T) {
|
||||||
if u.PassCertToUpstream != false {
|
if u.PassCertToUpstream != false {
|
||||||
t.Errorf("expected %v but got %v", false, u.PassCertToUpstream)
|
t.Errorf("expected %v but got %v", false, u.PassCertToUpstream)
|
||||||
}
|
}
|
||||||
|
if u.MatchCN != "" {
|
||||||
|
t.Errorf("expected empty string but got %v", u.MatchCN)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -946,6 +946,14 @@ stream {
|
||||||
|
|
||||||
set $proxy_upstream_name "-";
|
set $proxy_upstream_name "-";
|
||||||
|
|
||||||
|
{{ if not ( empty $server.CertificateAuth.MatchCN ) }}
|
||||||
|
{{ if gt (len $server.CertificateAuth.MatchCN) 0 }}
|
||||||
|
if ( $ssl_client_s_dn !~ {{ $server.CertificateAuth.MatchCN }} ) {
|
||||||
|
return 403 "client certificate unauthorized";
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ if eq $server.Hostname "_" }}
|
{{ if eq $server.Hostname "_" }}
|
||||||
ssl_reject_handshake {{ if $all.Cfg.SSLRejectHandshake }}on{{ else }}off{{ end }};
|
ssl_reject_handshake {{ if $all.Cfg.SSLRejectHandshake }}on{{ else }}off{{ end }};
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -262,6 +262,93 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
||||||
Status(http.StatusOK)
|
Status(http.StatusOK)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should return 403 using auth-tls-match-cn with no matching CN from client", func() {
|
||||||
|
host := "authtls.foo.com"
|
||||||
|
nameSpace := f.Namespace
|
||||||
|
|
||||||
|
clientConfig, err := framework.CreateIngressMASecret(
|
||||||
|
f.KubeClientSet,
|
||||||
|
host,
|
||||||
|
host,
|
||||||
|
nameSpace)
|
||||||
|
assert.Nil(ginkgo.GinkgoT(), err)
|
||||||
|
|
||||||
|
annotations := map[string]string{
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host,
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=notgonnamatch",
|
||||||
|
}
|
||||||
|
|
||||||
|
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||||
|
|
||||||
|
assertSslClientCertificateConfig(f, host, "on", "1")
|
||||||
|
|
||||||
|
f.HTTPTestClientWithTLSConfig(clientConfig).
|
||||||
|
GET("/").
|
||||||
|
WithURL(f.GetURL(framework.HTTPS)).
|
||||||
|
WithHeader("Host", host).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusForbidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should return 200 using auth-tls-match-cn with matching CN from client", func() {
|
||||||
|
host := "authtls.foo.com"
|
||||||
|
nameSpace := f.Namespace
|
||||||
|
|
||||||
|
clientConfig, err := framework.CreateIngressMASecret(
|
||||||
|
f.KubeClientSet,
|
||||||
|
host,
|
||||||
|
host,
|
||||||
|
nameSpace)
|
||||||
|
assert.Nil(ginkgo.GinkgoT(), err)
|
||||||
|
|
||||||
|
annotations := map[string]string{
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host,
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=authtls",
|
||||||
|
}
|
||||||
|
|
||||||
|
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||||
|
|
||||||
|
assertSslClientCertificateConfig(f, host, "on", "1")
|
||||||
|
|
||||||
|
f.HTTPTestClientWithTLSConfig(clientConfig).
|
||||||
|
GET("/").
|
||||||
|
WithURL(f.GetURL(framework.HTTPS)).
|
||||||
|
WithHeader("Host", host).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should return 200 using auth-tls-match-cn where atleast one of the regex options matches CN from client", func() {
|
||||||
|
host := "authtls.foo.com"
|
||||||
|
nameSpace := f.Namespace
|
||||||
|
|
||||||
|
clientConfig, err := framework.CreateIngressMASecret(
|
||||||
|
f.KubeClientSet,
|
||||||
|
host,
|
||||||
|
host,
|
||||||
|
nameSpace)
|
||||||
|
assert.Nil(ginkgo.GinkgoT(), err)
|
||||||
|
|
||||||
|
annotations := map[string]string{
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host,
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
|
||||||
|
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "CN=(itwillmatch|withthenextoption|authtls)",
|
||||||
|
}
|
||||||
|
|
||||||
|
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||||
|
|
||||||
|
assertSslClientCertificateConfig(f, host, "on", "1")
|
||||||
|
|
||||||
|
f.HTTPTestClientWithTLSConfig(clientConfig).
|
||||||
|
GET("/").
|
||||||
|
WithURL(f.GetURL(framework.HTTPS)).
|
||||||
|
WithHeader("Host", host).
|
||||||
|
Expect().
|
||||||
|
Status(http.StatusOK)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
func assertSslClientCertificateConfig(f *framework.Framework, host string, verifyClient string, verifyDepth string) {
|
func assertSslClientCertificateConfig(f *framework.Framework, host string, verifyClient string, verifyDepth string) {
|
||||||
|
|
Loading…
Reference in a new issue