diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index 84dacda11..4f3540de3 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -139,35 +139,39 @@ tls: minVersion: VersionTLS13 ``` -### Mutual Authentication +### Client Authentication (mTLS) -Traefik supports both optional and strict (which is the default) mutual authentication, though the `ClientCA.files` section. -If present, connections from clients without a certificate will be rejected. +Traefik supports mutual authentication, through the `ClientAuth` section. -For clients with a certificate, the `optional` option governs the behaviour as follows: +For authentication policies that require verification of the client certificate, the certificate authority for the certificate should be set in `ClientAuth.caFiles`. + +The `ClientAuth.clientAuthType` option governs the behaviour as follows: -- When `optional = false`, Traefik accepts connections only from clients presenting a certificate signed by a CA listed in `ClientCA.files`. -- When `optional = true`, Traefik authorizes connections from clients presenting a certificate signed by an unknown CA. +- `NoClientCert`: disregards any client certificate. +- `RequestClientCert`: asks for a certificate but proceeds anyway if none is provided. +- `RequireAnyClientCert`: requires a certificate but does not verify if it is signed by a CA listed in `ClientAuth.caFiles`. +- `VerifyClientCertIfGiven`: if a certificate is provided, verifies if it is signed by a CA listed in `ClientAuth.caFiles`. Otherwise proceeds without any certificate. +- `RequireAndVerifyClientCert`: requires a certificate, which must be signed by a CA listed in `ClientAuth.caFiles`. ```toml tab="TOML" [tls.options] [tls.options.default] - [tls.options.default.clientCA] + [tls.options.default.clientAuth] # in PEM format. each file can contain multiple CAs. - files = ["tests/clientca1.crt", "tests/clientca2.crt"] - optional = false + caFiles = ["tests/clientca1.crt", "tests/clientca2.crt"] + clientAuthType = "RequireAndVerifyClientCert" ``` ```yaml tab="YAML" tls: options: default: - clientCA: + clientAuth: # in PEM format. each file can contain multiple CAs. - files: + caFiles: - tests/clientca1.crt - tests/clientca2.crt - optional: false + clientAuthType: RequireAndVerifyClientCert ``` ### Cipher Suites diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index cf6c0b79c..d074d41dc 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -296,7 +296,7 @@ metadata: namespace: default spec: - minversion: VersionTLS12 + minVersion: VersionTLS12 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 9c378e0ef..bf59ba1e1 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -275,16 +275,16 @@ minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true - [tls.options.Options0.clientCA] - files = ["foobar", "foobar"] - optional = true + [tls.options.Options0.clientAuth] + caFiles = ["foobar", "foobar"] + clientAuthType = "VerifyClientCertIfGiven" [tls.options.Options1] minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true - [tls.options.Options1.clientCA] - files = ["foobar", "foobar"] - optional = true + [tls.options.Options1.clientAuth] + caFiles = ["foobar", "foobar"] + clientAuthType = "VerifyClientCertIfGiven" [tls.stores] [tls.stores.Store0] [tls.stores.Store0.defaultCertificate] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 0ea19a867..083b79c7c 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -303,22 +303,22 @@ tls: cipherSuites: - foobar - foobar - clientCA: - files: + clientAuth: + caFiles: - foobar - foobar - optional: true + clientAuthType: VerifyClientCertIfGiven sniStrict: true Options1: minVersion: foobar cipherSuites: - foobar - foobar - clientCA: - files: + clientAuth: + caFiles: - foobar - foobar - optional: true + clientAuthType: VerifyClientCertIfGiven sniStrict: true stores: Store0: diff --git a/integration/fixtures/https/clientca/https_1ca1config.toml b/integration/fixtures/https/clientca/https_1ca1config.toml index ba62c4fd0..e149185b3 100644 --- a/integration/fixtures/https/clientca/https_1ca1config.toml +++ b/integration/fixtures/https/clientca/https_1ca1config.toml @@ -47,6 +47,6 @@ keyFile = "fixtures/https/snitest.org.key" [tls.options] - [tls.options.default.ClientCA] - files = ["fixtures/https/clientca/ca1.crt"] - optional = true + [tls.options.default.clientAuth] + caFiles = ["fixtures/https/clientca/ca1.crt"] + clientAuthType = "VerifyClientCertIfGiven" diff --git a/integration/fixtures/https/clientca/https_2ca1config.toml b/integration/fixtures/https/clientca/https_2ca1config.toml index 848b4ace9..d12915e85 100644 --- a/integration/fixtures/https/clientca/https_2ca1config.toml +++ b/integration/fixtures/https/clientca/https_2ca1config.toml @@ -47,5 +47,5 @@ keyFile = "fixtures/https/snitest.org.key" [tls.options] - [tls.options.default.clientCA] - files = ["fixtures/https/clientca/ca1and2.crt"] \ No newline at end of file + [tls.options.default.clientAuth] + caFiles = ["fixtures/https/clientca/ca1and2.crt"] \ No newline at end of file diff --git a/integration/fixtures/https/clientca/https_2ca2config.toml b/integration/fixtures/https/clientca/https_2ca2config.toml index 6340cac13..5a15e574f 100644 --- a/integration/fixtures/https/clientca/https_2ca2config.toml +++ b/integration/fixtures/https/clientca/https_2ca2config.toml @@ -46,6 +46,6 @@ keyFile = "fixtures/https/snitest.org.key" [tls.options] - [tls.options.default.clientCA] - files = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] - optional = false + [tls.options.default.clientAuth] + caFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] + clientAuthType = "RequireAndVerifyClientCert" diff --git a/integration/fixtures/https/https_tls_options.toml b/integration/fixtures/https/https_tls_options.toml index 10bf6cadf..d81ac1a65 100644 --- a/integration/fixtures/https/https_tls_options.toml +++ b/integration/fixtures/https/https_tls_options.toml @@ -69,13 +69,13 @@ [tls.options] [tls.options.foo] - minversion = "VersionTLS11" + minVersion = "VersionTLS11" [tls.options.baz] - minversion = "VersionTLS11" + minVersion = "VersionTLS11" [tls.options.bar] - minversion = "VersionTLS12" + minVersion = "VersionTLS12" [tls.options.default] - minversion = "VersionTLS12" + minVersion = "VersionTLS12" diff --git a/integration/fixtures/k8s/03-tlsoption.yml b/integration/fixtures/k8s/03-tlsoption.yml index dea75d7e9..5c00a3973 100644 --- a/integration/fixtures/k8s/03-tlsoption.yml +++ b/integration/fixtures/k8s/03-tlsoption.yml @@ -5,8 +5,8 @@ metadata: namespace: default spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 diff --git a/integration/fixtures/tcp/multi-tls-options.toml b/integration/fixtures/tcp/multi-tls-options.toml index 517fdeddc..884924365 100644 --- a/integration/fixtures/tcp/multi-tls-options.toml +++ b/integration/fixtures/tcp/multi-tls-options.toml @@ -40,7 +40,7 @@ [tls.options] [tls.options.foo] - minversion = "VersionTLS11" + minVersion = "VersionTLS11" [tls.options.bar] - minversion = "VersionTLS12" + minVersion = "VersionTLS12" diff --git a/integration/fixtures/tlsclientheaders/simple.toml b/integration/fixtures/tlsclientheaders/simple.toml index 90fed2166..9469bb91f 100644 --- a/integration/fixtures/tlsclientheaders/simple.toml +++ b/integration/fixtures/tlsclientheaders/simple.toml @@ -23,9 +23,9 @@ ## dynamic configuration ## [tls.options] - [tls.options.default.clientCA] - files = [ """{{ .RootCertContent }}""" ] - optional = false + [tls.options.default.clientAuth] + caFiles = [ """{{ .RootCertContent }}""" ] + clientAuthType = "RequireAndVerifyClientCert" [tls.stores] [tls.stores.default.defaultCertificate] diff --git a/pkg/config/file/fixtures/sample.toml b/pkg/config/file/fixtures/sample.toml index 0a288c010..4459c506a 100644 --- a/pkg/config/file/fixtures/sample.toml +++ b/pkg/config/file/fixtures/sample.toml @@ -460,16 +460,16 @@ minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true - [tls.options.TLS0.clientCA] - files = ["foobar", "foobar"] - optional = true + [tls.options.TLS0.clientAuth] + caFiles = ["foobar", "foobar"] + clientAuthType = "VerifyClientCertIfGiven" [tls.options.TLS1] minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true - [tls.options.TLS1.clientCA] - files = ["foobar", "foobar"] - optional = true + [tls.options.TLS1.clientAuth] + caFiles = ["foobar", "foobar"] + clientAuthType = "VerifyClientCertIfGiven" [tls.stores] [tls.stores.Store0] [tls.stores.Store0.defaultCertificate] diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go index 9512854ef..22002ef26 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go @@ -222,7 +222,7 @@ func (p *passTLSClientCert) modifyRequestHeaders(logger logrus.FieldLogger, r *h if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { r.Header.Set(xForwardedTLSClientCert, getXForwardedTLSClientCert(logger, r.TLS.PeerCertificates)) } else { - logger.Warn("Try to extract certificate on a request without TLS") + logger.Warn("Tried to extract a certificate on a request without mutual TLS") } } @@ -231,7 +231,7 @@ func (p *passTLSClientCert) modifyRequestHeaders(logger logrus.FieldLogger, r *h headerContent := p.getXForwardedTLSClientCertInfo(r.TLS.PeerCertificates) r.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent)) } else { - logger.Warn("Try to extract certificate on a request without TLS") + logger.Warn("Tried to extract a certificate on a request without mutual TLS") } } } diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_tls_options.yml index 27915bb20..9c2a82f42 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_bad_tls_options.yml @@ -25,17 +25,17 @@ metadata: namespace: default spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretUnknown - emptySecret - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: v1 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options.yml index 5788e2f6b..c68d3900a 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options.yml @@ -25,16 +25,16 @@ metadata: namespace: default spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretCA2 - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: v1 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_and_specific_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_and_specific_namespace.yml index 4c5ba7844..518d8e1b8 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_and_specific_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_tls_options_and_specific_namespace.yml @@ -25,16 +25,16 @@ metadata: namespace: myns spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretCA2 - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: v1 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options.yml index 6c8e2491e..5798a0719 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options.yml @@ -6,7 +6,7 @@ metadata: namespace: default spec: - minversion: VersionTLS12 + minVersion: VersionTLS12 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options_namespace.yml index bd444c505..ed34abdf6 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_unknown_tls_options_namespace.yml @@ -6,7 +6,7 @@ metadata: namespace: default spec: - minversion: VersionTLS12 + minVersion: VersionTLS12 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_bad_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/with_bad_tls_options.yml index 0a51178ca..94ea22e5f 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_bad_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_bad_tls_options.yml @@ -25,17 +25,17 @@ metadata: namespace: default spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretUnknown - emptySecret - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml index 5d9bd31a7..93ec20239 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml @@ -25,16 +25,16 @@ metadata: namespace: default spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretCA2 - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml index 2fd7aa390..b870b3389 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_options_and_specific_namespace.yml @@ -25,16 +25,16 @@ metadata: namespace: myns spec: - minversion: VersionTLS12 - snistrict: true - ciphersuites: + minVersion: VersionTLS12 + sniStrict: true + cipherSuites: - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 - TLS_RSA_WITH_AES_256_GCM_SHA384 - clientca: - secretnames: + clientAuth: + secretNames: - secretCA1 - secretCA2 - optional: true + clientAuthType: VerifyClientCertIfGiven --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options.yml index 1ef9d2344..543851e37 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options.yml @@ -5,7 +5,7 @@ metadata: namespace: default spec: - minversion: VersionTLS12 + minVersion: VersionTLS12 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml index 022b88c98..69606c2db 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_unknown_tls_options_namespace.yml @@ -5,7 +5,7 @@ metadata: namespace: default spec: - minversion: VersionTLS12 + minVersion: VersionTLS12 --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 9de6cdf9c..8f4a463d8 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -313,7 +313,7 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options logger := log.FromContext(log.With(ctx, log.Str("tlsOption", tlsOption.Name), log.Str("namespace", tlsOption.Namespace))) var clientCAs []tls.FileOrContent - for _, secretName := range tlsOption.Spec.ClientCA.SecretNames { + for _, secretName := range tlsOption.Spec.ClientAuth.SecretNames { secret, exists, err := client.GetSecret(tlsOption.Namespace, secretName) if err != nil { logger.Errorf("Failed to fetch secret %s/%s: %v", tlsOption.Namespace, secretName, err) @@ -337,9 +337,9 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options tlsOptions[makeID(tlsOption.Namespace, tlsOption.Name)] = tls.Options{ MinVersion: tlsOption.Spec.MinVersion, CipherSuites: tlsOption.Spec.CipherSuites, - ClientCA: tls.ClientCA{ - Files: clientCAs, - Optional: tlsOption.Spec.ClientCA.Optional, + ClientAuth: tls.ClientAuth{ + CAFiles: clientCAs, + ClientAuthType: tlsOption.Spec.ClientAuth.ClientAuthType, }, SniStrict: tlsOption.Spec.SniStrict, } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 9ece08d30..25557beb1 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -319,12 +319,12 @@ func TestLoadIngressRouteTCPs(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, @@ -377,12 +377,12 @@ func TestLoadIngressRouteTCPs(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, @@ -435,11 +435,11 @@ func TestLoadIngressRouteTCPs(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, @@ -1009,12 +1009,12 @@ func TestLoadIngressRoutes(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, @@ -1067,12 +1067,12 @@ func TestLoadIngressRoutes(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, @@ -1125,11 +1125,11 @@ func TestLoadIngressRoutes(t *testing.T) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", }, - ClientCA: tls.ClientCA{ - Files: []tls.FileOrContent{ + ClientAuth: tls.ClientAuth{ + CAFiles: []tls.FileOrContent{ tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), }, - Optional: true, + ClientAuthType: "VerifyClientCertIfGiven", }, SniStrict: true, }, diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go index 22e04e3e5..e06fdeead 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/tlsoption.go @@ -19,22 +19,22 @@ type TLSOption struct { // TLSOptionSpec configures TLS for an entry point type TLSOptionSpec struct { - MinVersion string `json:"minversion"` - CipherSuites []string `json:"ciphersuites"` - ClientCA ClientCA `json:"clientca"` - SniStrict bool `json:"snistrict"` + MinVersion string `json:"minVersion,omitempty"` + CipherSuites []string `json:"cipherSuites,omitempty"` + ClientAuth ClientAuth `json:"clientAuth,omitempty"` + SniStrict bool `json:"sniStrict,omitempty"` } // +k8s:deepcopy-gen=true -// ClientCA defines traefik CA files for an entryPoint -// and it indicates if they are mandatory or have just to be analyzed if provided -type ClientCA struct { +// ClientAuth defines the parameters of the client authentication part of the TLS connection, if any. +type ClientAuth struct { // SecretName is the name of the referenced Kubernetes Secret to specify the // certificate details. - SecretNames []string `json:"secretnames"` - // Optional indicates if ClientCA are mandatory or have just to be analyzed if provided - Optional bool `json:"optional"` + SecretNames []string `json:"secretNames"` + // ClientAuthType defines the client authentication type to apply. + // The available values are: "NoClientCert", "RequestClientCert", "VerifyClientCertIfGiven" and "RequireAndVerifyClientCert". + ClientAuthType string `json:"clientAuthType"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index e1d756c35..0d603a047 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClientCA) DeepCopyInto(out *ClientCA) { +func (in *ClientAuth) DeepCopyInto(out *ClientAuth) { *out = *in if in.SecretNames != nil { in, out := &in.SecretNames, &out.SecretNames @@ -43,12 +43,12 @@ func (in *ClientCA) DeepCopyInto(out *ClientCA) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCA. -func (in *ClientCA) DeepCopy() *ClientCA { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuth. +func (in *ClientAuth) DeepCopy() *ClientAuth { if in == nil { return nil } - out := new(ClientCA) + out := new(ClientAuth) in.DeepCopyInto(out) return out } @@ -529,7 +529,7 @@ func (in *TLSOptionSpec) DeepCopyInto(out *TLSOptionSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - in.ClientCA.DeepCopyInto(&out.ClientCA) + in.ClientAuth.DeepCopyInto(&out.ClientAuth) return } diff --git a/pkg/tls/tls.go b/pkg/tls/tls.go index 7193a5220..49d18176f 100644 --- a/pkg/tls/tls.go +++ b/pkg/tls/tls.go @@ -4,21 +4,22 @@ const certificateHeader = "-----BEGIN CERTIFICATE-----\n" // +k8s:deepcopy-gen=true -// ClientCA defines traefik CA files for a entryPoint -// and it indicates if they are mandatory or have just to be analyzed if provided. -type ClientCA struct { - Files []FileOrContent `json:"files,omitempty" toml:"files,omitempty" yaml:"files,omitempty"` - Optional bool `json:"optional,omitempty" toml:"optional,omitempty" yaml:"optional,omitempty"` +// ClientAuth defines the parameters of the client authentication part of the TLS connection, if any. +type ClientAuth struct { + CAFiles []FileOrContent `json:"caFiles,omitempty" toml:"caFiles,omitempty" yaml:"caFiles,omitempty"` + // ClientAuthType defines the client authentication type to apply. + // The available values are: "NoClientCert", "RequestClientCert", "VerifyClientCertIfGiven" and "RequireAndVerifyClientCert". + ClientAuthType string `json:"clientAuthType,omitempty" toml:"clientAuthType,omitempty" yaml:"clientAuthType,omitempty"` } // +k8s:deepcopy-gen=true // Options configures TLS for an entry point type Options struct { - MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"` - CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"` - ClientCA ClientCA `json:"clientCA,omitempty" toml:"clientCA,omitempty" yaml:"clientCA,omitempty"` - SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"` + MinVersion string `json:"minVersion,omitempty" toml:"minVersion,omitempty" yaml:"minVersion,omitempty" export:"true"` + CipherSuites []string `json:"cipherSuites,omitempty" toml:"cipherSuites,omitempty" yaml:"cipherSuites,omitempty"` + ClientAuth ClientAuth `json:"clientAuth,omitempty" toml:"clientAuth,omitempty" yaml:"clientAuth,omitempty"` + SniStrict bool `json:"sniStrict,omitempty" toml:"sniStrict,omitempty" yaml:"sniStrict,omitempty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index c1599b7d8..1873cfe58 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -3,6 +3,7 @@ package tls import ( "crypto/tls" "crypto/x509" + "errors" "fmt" "sync" @@ -159,23 +160,45 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) { // ensure http2 enabled conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol} - if len(tlsOption.ClientCA.Files) > 0 { + if len(tlsOption.ClientAuth.CAFiles) > 0 { pool := x509.NewCertPool() - for _, caFile := range tlsOption.ClientCA.Files { + for _, caFile := range tlsOption.ClientAuth.CAFiles { data, err := caFile.Read() if err != nil { return nil, err } ok := pool.AppendCertsFromPEM(data) if !ok { - return nil, fmt.Errorf("invalid certificate(s) in %s", caFile) + if caFile.IsPath() { + return nil, fmt.Errorf("invalid certificate(s) in %s", caFile) + } + return nil, errors.New("invalid certificate(s) content") } } conf.ClientCAs = pool - if tlsOption.ClientCA.Optional { + conf.ClientAuth = tls.RequireAndVerifyClientCert + } + + clientAuthType := tlsOption.ClientAuth.ClientAuthType + if len(clientAuthType) > 0 { + if conf.ClientCAs == nil && (clientAuthType == "VerifyClientCertIfGiven" || + clientAuthType == "RequireAndVerifyClientCert") { + return nil, fmt.Errorf("invalid clientAuthType: %s, CAFiles is required", clientAuthType) + } + + switch clientAuthType { + case "NoClientCert": + conf.ClientAuth = tls.NoClientCert + case "RequestClientCert": + conf.ClientAuth = tls.RequestClientCert + case "RequireAnyClientCert": + conf.ClientAuth = tls.RequireAnyClientCert + case "VerifyClientCertIfGiven": conf.ClientAuth = tls.VerifyClientCertIfGiven - } else { + case "RequireAndVerifyClientCert": conf.ClientAuth = tls.RequireAndVerifyClientCert + default: + return nil, fmt.Errorf("unknown client auth type %q", clientAuthType) } } diff --git a/pkg/tls/tlsmanager_test.go b/pkg/tls/tlsmanager_test.go index 963f548ee..a176bfe11 100644 --- a/pkg/tls/tlsmanager_test.go +++ b/pkg/tls/tlsmanager_test.go @@ -2,9 +2,12 @@ package tls import ( "crypto/tls" + "crypto/x509" + "encoding/pem" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // LocalhostCert is a PEM-encoded TLS cert with SAN IPs @@ -146,3 +149,125 @@ func TestManager_Get(t *testing.T) { }) } } + +func TestClientAuth(t *testing.T) { + tlsConfigs := map[string]Options{ + "eca": {ClientAuth: ClientAuth{}}, + "ecat": {ClientAuth: ClientAuth{ClientAuthType: ""}}, + "ncc": {ClientAuth: ClientAuth{ClientAuthType: "NoClientCert"}}, + "rcc": {ClientAuth: ClientAuth{ClientAuthType: "RequestClientCert"}}, + "racc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAnyClientCert"}}, + "vccig": { + ClientAuth: ClientAuth{ + CAFiles: []FileOrContent{localhostCert}, + ClientAuthType: "VerifyClientCertIfGiven", + }, + }, + "vccigwca": { + ClientAuth: ClientAuth{ClientAuthType: "VerifyClientCertIfGiven"}, + }, + "ravcc": {ClientAuth: ClientAuth{ClientAuthType: "RequireAndVerifyClientCert"}}, + "ravccwca": { + ClientAuth: ClientAuth{ + CAFiles: []FileOrContent{localhostCert}, + ClientAuthType: "RequireAndVerifyClientCert", + }, + }, + "ravccwbca": { + ClientAuth: ClientAuth{ + CAFiles: []FileOrContent{"Bad content"}, + ClientAuthType: "RequireAndVerifyClientCert", + }, + }, + "ucat": {ClientAuth: ClientAuth{ClientAuthType: "Unknown"}}, + } + + block, _ := pem.Decode([]byte(localhostCert)) + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + testCases := []struct { + desc string + tlsOptionsName string + expectedClientAuth tls.ClientAuthType + expectedRawSubject []byte + }{ + { + desc: "Empty ClientAuth option should get a tls.NoClientCert (default value)", + tlsOptionsName: "eca", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "Empty ClientAuthType option should get a tls.NoClientCert (default value)", + tlsOptionsName: "ecat", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "NoClientCert option should get a tls.NoClientCert as ClientAuthType", + tlsOptionsName: "ncc", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "RequestClientCert option should get a tls.RequestClientCert as ClientAuthType", + tlsOptionsName: "rcc", + expectedClientAuth: tls.RequestClientCert, + }, + { + desc: "RequireAnyClientCert option should get a tls.RequireAnyClientCert as ClientAuthType", + tlsOptionsName: "racc", + expectedClientAuth: tls.RequireAnyClientCert, + }, + { + desc: "VerifyClientCertIfGiven option should get a tls.VerifyClientCertIfGiven as ClientAuthType", + tlsOptionsName: "vccig", + expectedClientAuth: tls.VerifyClientCertIfGiven, + }, + { + desc: "VerifyClientCertIfGiven option without CAFiles yields a default ClientAuthType (NoClientCert)", + tlsOptionsName: "vccigwca", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "RequireAndVerifyClientCert option without CAFiles yields a default ClientAuthType (NoClientCert)", + tlsOptionsName: "ravcc", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "RequireAndVerifyClientCert option should get a tls.RequireAndVerifyClientCert as ClientAuthType with CA files", + tlsOptionsName: "ravccwca", + expectedClientAuth: tls.RequireAndVerifyClientCert, + expectedRawSubject: cert.RawSubject, + }, + { + desc: "Unknown option yields a default ClientAuthType (NoClientCert)", + tlsOptionsName: "ucat", + expectedClientAuth: tls.NoClientCert, + }, + { + desc: "Bad CA certificate content yields a default ClientAuthType (NoClientCert)", + tlsOptionsName: "ravccwbca", + expectedClientAuth: tls.NoClientCert, + }, + } + + tlsManager := NewManager() + tlsManager.UpdateConfigs(nil, tlsConfigs, nil) + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + config, err := tlsManager.Get("default", test.tlsOptionsName) + assert.NoError(t, err) + + if test.expectedRawSubject != nil { + subjects := config.ClientCAs.Subjects() + assert.Len(t, subjects, 1) + assert.Equal(t, subjects[0], test.expectedRawSubject) + } + + assert.Equal(t, config.ClientAuth, test.expectedClientAuth) + }) + } +} diff --git a/pkg/tls/zz_generated.deepcopy.go b/pkg/tls/zz_generated.deepcopy.go index 9823a1354..e259da2ca 100644 --- a/pkg/tls/zz_generated.deepcopy.go +++ b/pkg/tls/zz_generated.deepcopy.go @@ -51,22 +51,22 @@ func (in *CertAndStores) DeepCopy() *CertAndStores { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClientCA) DeepCopyInto(out *ClientCA) { +func (in *ClientAuth) DeepCopyInto(out *ClientAuth) { *out = *in - if in.Files != nil { - in, out := &in.Files, &out.Files + if in.CAFiles != nil { + in, out := &in.CAFiles, &out.CAFiles *out = make([]FileOrContent, len(*in)) copy(*out, *in) } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientCA. -func (in *ClientCA) DeepCopy() *ClientCA { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuth. +func (in *ClientAuth) DeepCopy() *ClientAuth { if in == nil { return nil } - out := new(ClientCA) + out := new(ClientAuth) in.DeepCopyInto(out) return out } @@ -79,7 +79,7 @@ func (in *Options) DeepCopyInto(out *Options) { *out = make([]string, len(*in)) copy(*out, *in) } - in.ClientCA.DeepCopyInto(&out.ClientCA) + in.ClientAuth.DeepCopyInto(&out.ClientAuth) return }