added new auth-tls-match-cn annotation (#8434)

* added new auth-tls-match-cn annotation

* added few more tests
This commit is contained in:
Chris Shino 2022-04-15 15:59:10 -04:00 committed by GitHub
parent 81c2afd975
commit f9372aa495
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 0 deletions

View file

@ -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:

View file

@ -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
} }

View file

@ -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)
}
} }

View file

@ -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 }}

View file

@ -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) {