diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index e9fefebf1..1fb883596 100755 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -244,23 +244,18 @@ It is possible to enable Client Certificate Authentication using additional anno Client Certificate Authentication is applied per host and it is not possible to specify rules that differ for individual paths. -The annotations are: +To enable, add the annotation `nginx.ingress.kubernetes.io/auth-tls-secret: namespace/secretName`. This secret must have a file named `ca.crt` containing the full Certificate Authority chain `ca.crt` that is enabled to authenticate against this Ingress. -* `nginx.ingress.kubernetes.io/auth-tls-secret: secretName`: - The name of the Secret that contains the full Certificate Authority chain `ca.crt` that is enabled to authenticate against this Ingress. - This annotation expects the Secret name in the form "namespace/secretName". -* `nginx.ingress.kubernetes.io/auth-tls-verify-depth`: - The validation depth between the provided client certificate and the Certification Authority chain. -* `nginx.ingress.kubernetes.io/auth-tls-verify-client`: - Enables verification of client certificates. Possible values are: - * `off`: Don't request client certificates and don't do client certificate verification. (default) - * `on`: Request a client certificate that must be signed by a certificate that is included in the secret key `ca.crt` of the secret specified by `nginx.ingress.kubernetes.io/auth-tls-secret: secretName`. Failed certificate verification will result in a status code 400 (Bad Request). - * `optional`: Do optional client certificate validation against the CAs from `auth-tls-secret`. The request fails with status code 400 (Bad Request) when a certificate is provided that is not signed by the CA. When no or an otherwise invalid certificate is provided, the request does not fail, but instead the 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-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). +You can further customize client certificate authentication and behaviour with these annotations: + +* `nginx.ingress.kubernetes.io/auth-tls-verify-depth`: The validation depth between the provided client certificate and the Certification Authority chain. (default: 1) +* `nginx.ingress.kubernetes.io/auth-tls-verify-client`: Enables verification of client certificates. Possible values are: + * `on`: Request a client certificate that must be signed by a certificate that is included in the secret key `ca.crt` of the secret specified by `nginx.ingress.kubernetes.io/auth-tls-secret: namespace/secretName`. Failed certificate verification will result in a status code 400 (Bad Request) (default) + * `off`: Don't request client certificates and don't do client certificate verification. + * `optional`: Do optional client certificate validation against the CAs from `auth-tls-secret`. The request fails with status code 400 (Bad Request) when a certificate is provided that is not signed by the CA. When no or an otherwise invalid certificate is provided, the request does not fail, but instead the 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-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). The following headers are sent to the upstream service according to the `auth-tls-*` annotations: @@ -333,39 +328,43 @@ location enabling this functionality. CORS can be controlled with the following annotations: -* `nginx.ingress.kubernetes.io/cors-allow-methods` - controls which methods are accepted. This is a multi-valued field, separated by ',' and - accepts only letters (upper and lower case). - - Default: `GET, PUT, POST, DELETE, PATCH, OPTIONS` - - Example: `nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"` +* `nginx.ingress.kubernetes.io/cors-allow-methods`: Controls which methods are accepted. -* `nginx.ingress.kubernetes.io/cors-allow-headers` - controls which headers are accepted. This is a multi-valued field, separated by ',' and accepts letters, - numbers, _ and -. - - Default: `DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization` - - Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"` + This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case). -* `nginx.ingress.kubernetes.io/cors-expose-headers` - controls which headers are exposed to response. This is a multi-valued field, separated by ',' and accepts - letters, numbers, _, - and *. - - Default: *empty* - - Example: `nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"` + - Default: `GET, PUT, POST, DELETE, PATCH, OPTIONS` + - Example: `nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"` -* `nginx.ingress.kubernetes.io/cors-allow-origin` - controls what's the accepted Origin for CORS. - This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` - - Default: `*` - - Example: `nginx.ingress.kubernetes.io/cors-allow-origin: "https://origin-site.com:4443"` +* `nginx.ingress.kubernetes.io/cors-allow-headers`: Controls which headers are accepted. -* `nginx.ingress.kubernetes.io/cors-allow-credentials` - controls if credentials can be passed during CORS operations. - - Default: `true` - - Example: `nginx.ingress.kubernetes.io/cors-allow-credentials: "false"` + This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -. -* `nginx.ingress.kubernetes.io/cors-max-age` - controls how long preflight requests can be cached. - Default: `1728000` - Example: `nginx.ingress.kubernetes.io/cors-max-age: 600` + - Default: `DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization` + - Example: `nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"` + +* `nginx.ingress.kubernetes.io/cors-expose-headers`: Controls which headers are exposed to response. + + This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *. + + - Default: *empty* + - Example: `nginx.ingress.kubernetes.io/cors-expose-headers: "*, X-CustomResponseHeader"` + +* `nginx.ingress.kubernetes.io/cors-allow-origin`: Controls what's the accepted Origin for CORS. + + This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` + + - Default: `*` + - Example: `nginx.ingress.kubernetes.io/cors-allow-origin: "https://origin-site.com:4443"` + +* `nginx.ingress.kubernetes.io/cors-allow-credentials`: Controls if credentials can be passed during CORS operations. + + - Default: `true` + - Example: `nginx.ingress.kubernetes.io/cors-allow-credentials: "false"` + +* `nginx.ingress.kubernetes.io/cors-max-age`: Controls how long preflight requests can be cached. + + - Default: `1728000` + - Example: `nginx.ingress.kubernetes.io/cors-max-age: 600` !!! note For more information please see [https://enable-cors.org](https://enable-cors.org/server_nginx.html) diff --git a/internal/ingress/annotations/authtls/main_test.go b/internal/ingress/annotations/authtls/main_test.go index b2c915d18..f7649fe1c 100644 --- a/internal/ingress/annotations/authtls/main_test.go +++ b/internal/ingress/annotations/authtls/main_test.go @@ -94,10 +94,6 @@ func TestAnnotations(t *testing.T) { data := map[string]string{} data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret" - data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off" - data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "1" - data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error" - data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true" ing.SetAnnotations(data) @@ -120,12 +116,45 @@ func TestAnnotations(t *testing.T) { if u.AuthSSLCert.Secret != secret.Secret { t.Errorf("expected %v but got %v", secret.Secret, u.AuthSSLCert.Secret) } - if u.VerifyClient != "off" { - t.Errorf("expected %v but got %v", "off", u.VerifyClient) + if u.VerifyClient != "on" { + t.Errorf("expected %v but got %v", "on", u.VerifyClient) } if u.ValidationDepth != 1 { t.Errorf("expected %v but got %v", 1, u.ValidationDepth) } + if u.ErrorPage != "" { + t.Errorf("expected %v but got %v", "", u.ErrorPage) + } + if u.PassCertToUpstream != false { + t.Errorf("expected %v but got %v", false, u.PassCertToUpstream) + } + + 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" + + ing.SetAnnotations(data) + + i, err = NewParser(fakeSecret).Parse(ing) + if err != nil { + t.Errorf("Unexpected error with ingress: %v", err) + } + + u, ok = i.(*Config) + if !ok { + t.Errorf("expected *Config but got %v", u) + } + + if u.AuthSSLCert.Secret != secret.Secret { + t.Errorf("expected %v but got %v", secret.Secret, u.AuthSSLCert.Secret) + } + if u.VerifyClient != "off" { + t.Errorf("expected %v but got %v", "off", u.VerifyClient) + } + if u.ValidationDepth != 2 { + t.Errorf("expected %v but got %v", 2, u.ValidationDepth) + } if u.ErrorPage != "ok.com/error" { t.Errorf("expected %v but got %v", "ok.com/error", u.ErrorPage) } diff --git a/test/e2e/annotations/authtls.go b/test/e2e/annotations/authtls.go index 86c5d9ec5..093afe14e 100644 --- a/test/e2e/annotations/authtls.go +++ b/test/e2e/annotations/authtls.go @@ -17,7 +17,6 @@ limitations under the License. package annotations import ( - "crypto/tls" "fmt" "net/http" "strings" @@ -34,7 +33,7 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() { f.NewEchoDeploymentWithReplicas(2) }) - ginkgo.It("should set valid auth-tls-secret", func() { + ginkgo.It("should set sslClientCertificate, sslVerifyClient and sslVerifyDepth with auth-tls-secret", func() { host := "authtls.foo.com" nameSpace := f.Namespace @@ -45,16 +44,28 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() { nameSpace) assert.Nil(ginkgo.GinkgoT(), err) - annotations := map[string]string{ + annotations := map[string]string{} + + ing := f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations)) + + f.WaitForNginxServer(host, + func(server string) bool { + return !strings.Contains(server, "ssl_client_certificate") && + !strings.Contains(server, "ssl_verify_client") && + !strings.Contains(server, "ssl_verify_depth") + }) + + annotations = map[string]string{ "nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host, } - f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations)) + ing.SetAnnotations(annotations) + f.UpdateIngress(ing) assertSslClientCertificateConfig(f, host, "on", "1") // Send Request without Client Certs - f.HTTPTestClientWithTLSConfig(&tls.Config{ServerName: host, InsecureSkipVerify: true}). + f.HTTPTestClient(). GET("/"). WithURL(f.GetURL(framework.HTTPS)). WithHeader("Host", host). @@ -100,7 +111,7 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() { Status(http.StatusOK) }) - ginkgo.It("should set valid auth-tls-secret, pass certificate to upstream, and error page", func() { + ginkgo.It("should 302 redirect to error page instead of 400 when auth-tls-error-page is set", func() { host := "authtls.foo.com" nameSpace := f.Namespace @@ -114,9 +125,8 @@ 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-error-page": f.GetURL(framework.HTTP) + errorPath, - "nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream": "true", + "nginx.ingress.kubernetes.io/auth-tls-secret": nameSpace + "/" + host, + "nginx.ingress.kubernetes.io/auth-tls-error-page": f.GetURL(framework.HTTP) + errorPath, } f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations)) @@ -124,12 +134,10 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() { assertSslClientCertificateConfig(f, host, "on", "1") sslErrorPage := fmt.Sprintf("error_page 495 496 = %s;", f.GetURL(framework.HTTP)+errorPath) - sslUpstreamClientCert := "proxy_set_header ssl-client-cert $ssl_client_escaped_cert;" f.WaitForNginxServer(host, func(server string) bool { - return strings.Contains(server, sslErrorPage) && - strings.Contains(server, sslUpstreamClientCert) + return strings.Contains(server, sslErrorPage) }) // Send Request without Client Certs @@ -150,6 +158,51 @@ var _ = framework.DescribeAnnotation("auth-tls-*", func() { Status(http.StatusOK) }) + ginkgo.It("should pass URL-encoded certificate to upstream", 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-pass-certificate-to-upstream": "true", + } + + f.EnsureIngress(framework.NewSingleIngressWithTLS(host, "/", host, []string{host}, nameSpace, framework.EchoService, 80, annotations)) + + assertSslClientCertificateConfig(f, host, "on", "1") + + sslUpstreamClientCert := "proxy_set_header ssl-client-cert $ssl_client_escaped_cert;" + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, sslUpstreamClientCert) + }) + + // Send Request without Client Certs + f.HTTPTestClient(). + GET("/"). + WithURL(f.GetURL(framework.HTTPS)). + WithHeader("Host", host). + Expect(). + Status(http.StatusBadRequest) + + // Send Request Passing the Client Certs + f.HTTPTestClientWithTLSConfig(clientConfig). + GET("/"). + WithURL(f.GetURL(framework.HTTPS)). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK). + Body().Contains("ssl-client-cert=-----BEGIN%20CERTIFICATE-----%0A") + }) + ginkgo.It("should validate auth-tls-verify-client", func() { host := "authtls.foo.com" nameSpace := f.Namespace