Merge 2c0f3f7b32
into 5628f765fe
This commit is contained in:
commit
c4c85ba306
5 changed files with 60 additions and 18 deletions
|
@ -29,6 +29,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|
|||
|[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-match-cn](#client-certificate-authentication)|string|
|
||||
|[nginx.ingress.kubernetes.io/auth-tls-exact-match-cn](#client-certificate-authentication)|"true" or "false"|
|
||||
|[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-duration](#external-authentication)|string|
|
||||
|
@ -270,7 +271,9 @@ 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.
|
||||
* `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-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.
|
||||
* `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 regex. If the certificate CN sent during mTLS does not match your regex, it will fail with status code 403. Another way of using this is by adding multiple options in your regex, example: your actual CN is `"myvalidclient.com"` and the annotation regex you pass in is: `"(option1|option2|myvalid)"` then myvalid would give you a match. In this case, as long as one of the options in the brackets regex matches the certificate CN then you will receive a 200 status code.
|
||||
* `nginx.ingress.kubernetes.io/auth-tls-exact-match-cn`: Adds on to the strictness of the auth-tls-match-cn check. Adding a `"true"` flag makes the check an exact match for the CN of the client certificate that is sent over. Example: `"myvalidclient.com"`. If the certificate CN sent during mTLS does not match your string exactly, it will fail with status code 403. With exact match set to true, if you were to pass `"myvalidclient.co"`, it would not match because it is missing the `"m"` at the end. This defaults to false when using auth-tls-match-cn.
|
||||
|
||||
|
||||
The following headers are sent to the upstream service according to the `auth-tls-*` annotations:
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ const (
|
|||
|
||||
var (
|
||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||
commonNameRegex = regexp.MustCompile(`CN=`)
|
||||
matchCNSanitizer = regexp.MustCompile(`^[a-zA-Z0-9\.\-\(\)\|]+$`)
|
||||
)
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
|
@ -48,6 +48,7 @@ type Config struct {
|
|||
ErrorPage string `json:"errorPage"`
|
||||
PassCertToUpstream bool `json:"passCertToUpstream"`
|
||||
MatchCN string `json:"matchCN"`
|
||||
ExactMatchCN bool `json:"exactMatchCN"`
|
||||
AuthTLSError string
|
||||
}
|
||||
|
||||
|
@ -131,9 +132,14 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
|
||||
config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing)
|
||||
if err != nil || !commonNameRegex.MatchString(config.MatchCN) {
|
||||
if err != nil || !matchCNSanitizer.MatchString(config.MatchCN) {
|
||||
config.MatchCN = ""
|
||||
}
|
||||
|
||||
config.ExactMatchCN, err = parser.GetBoolAnnotation("auth-tls-exact-match-cn", ing)
|
||||
if err != nil {
|
||||
config.ExactMatchCN = false
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package authtls
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
|
@ -131,12 +132,16 @@ func TestAnnotations(t *testing.T) {
|
|||
if u.MatchCN != "" {
|
||||
t.Errorf("expected empty string, but got %v", u.MatchCN)
|
||||
}
|
||||
if u.ExactMatchCN != false {
|
||||
t.Errorf("expected empty string, but got %v", u.ExactMatchCN)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
||||
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-match-cn")] = "CN=hello-app"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "he"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-exact-match-cn")] = "true"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -165,8 +170,15 @@ func TestAnnotations(t *testing.T) {
|
|||
if u.PassCertToUpstream != true {
|
||||
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)
|
||||
match, err := regexp.MatchString(u.MatchCN, "hello")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error matching CN %v", err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("expected %v but got %v", "hello", u.MatchCN)
|
||||
}
|
||||
if u.ExactMatchCN != true {
|
||||
t.Errorf("expected %v but got %v", true, u.ExactMatchCN)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,6 +215,7 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "<script>nope</script>"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-exact-match-cn")] = "<script>definitely nope</script>"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(fakeSecret).Parse(ing)
|
||||
|
@ -226,6 +239,9 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
if u.MatchCN != "" {
|
||||
t.Errorf("expected empty string but got %v", u.MatchCN)
|
||||
}
|
||||
if u.ExactMatchCN != false {
|
||||
t.Errorf("expected %v but got %v", false, u.ExactMatchCN)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -962,13 +962,29 @@ stream {
|
|||
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 }} ) {
|
||||
{{ if gt (len $server.CertificateAuth.MatchCN) 0 }}
|
||||
|
||||
if ( $ssl_client_s_dn ~ CN=(?<CN>[^,]+) ) {
|
||||
set $ssl_client_s_dn_cn $CN;
|
||||
}
|
||||
|
||||
{{ if ( $server.CertificateAuth.ExactMatchCN )}}
|
||||
|
||||
|
||||
if ( $ssl_client_s_dn_cn != {{ $server.CertificateAuth.MatchCN }} ) {
|
||||
return 403 "client certificate unauthorized";
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
|
||||
if ( $ssl_client_s_dn_cn !~ {{ $server.CertificateAuth.MatchCN }} ) {
|
||||
return 403 "client certificate unauthorized";
|
||||
}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{ if eq $server.Hostname "_" }}
|
||||
ssl_reject_handshake {{ if $all.Cfg.SSLRejectHandshake }}on{{ else }}off{{ end }};
|
||||
{{ end }}
|
||||
|
|
|
@ -263,7 +263,7 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
|||
|
||||
})
|
||||
|
||||
ginkgo.It("should return 403 using auth-tls-match-cn with no matching CN from client", func() {
|
||||
ginkgo.It("should return 403 using auth-tls-exact-match-cn with no matching CN from client", func() {
|
||||
host := "authtls.foo.com"
|
||||
nameSpace := f.Namespace
|
||||
|
||||
|
@ -275,9 +275,10 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
|||
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",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host,
|
||||
"nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-exact-match-cn": "true",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "notgonnamatch",
|
||||
}
|
||||
|
||||
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||
|
@ -292,7 +293,7 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
|||
Status(http.StatusForbidden)
|
||||
})
|
||||
|
||||
ginkgo.It("should return 200 using auth-tls-match-cn with matching CN from client", func() {
|
||||
ginkgo.It("should return 200 using auth-tls-exact-match-cn with matching CN from client", func() {
|
||||
host := "authtls.foo.com"
|
||||
nameSpace := f.Namespace
|
||||
|
||||
|
@ -304,13 +305,13 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
|||
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",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host,
|
||||
"nginx.ingress.kubernetes.io/auth-tls-verify-client": "on",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-exact-match-cn": "true",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "authtls.foo.com-client",
|
||||
}
|
||||
|
||||
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||
|
||||
assertSslClientCertificateConfig(f, host, "on", "1")
|
||||
|
||||
f.HTTPTestClientWithTLSConfig(clientConfig).
|
||||
|
@ -335,7 +336,7 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() {
|
|||
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)",
|
||||
"nginx.ingress.kubernetes.io/auth-tls-match-cn": "(itwillmatch|withthenextoption|authtls)",
|
||||
}
|
||||
|
||||
f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations))
|
||||
|
|
Loading…
Reference in a new issue