diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index 61c2fd576..cf27291d9 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -52,7 +52,10 @@ func TestDo_globalConfiguration(t *testing.T) { {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, }, - ClientCAFiles: []string{"foo ClientCAFiles 1", "foo ClientCAFiles 2", "foo ClientCAFiles 3"}, + ClientCA: traefikTls.ClientCA{ + Files: []string{"foo ClientCAFiles 1", "foo ClientCAFiles 2", "foo ClientCAFiles 3"}, + Optional: false, + }, }, Redirect: &configuration.Redirect{ Replacement: "foo Replacement", @@ -95,7 +98,10 @@ func TestDo_globalConfiguration(t *testing.T) { {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, }, - ClientCAFiles: []string{"fii ClientCAFiles 1", "fii ClientCAFiles 2", "fii ClientCAFiles 3"}, + ClientCA: traefikTls.ClientCA{ + Files: []string{"fii ClientCAFiles 1", "fii ClientCAFiles 2", "fii ClientCAFiles 3"}, + Optional: false, + }, }, Redirect: &configuration.Redirect{ Replacement: "fii Replacement", diff --git a/configuration/configuration.go b/configuration/configuration.go index 929f3fb32..0eac04901 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -308,7 +308,11 @@ func (ep *EntryPoints) Set(value string) error { } if len(result["ca"]) > 0 { files := strings.Split(result["ca"], ",") - configTLS.ClientCAFiles = files + optional := toBool(result, "ca_optional") + configTLS.ClientCA = tls.ClientCA{ + Files: files, + Optional: optional, + } } var redirect *Redirect if len(result["redirect_entrypoint"]) > 0 || len(result["redirect_regex"]) > 0 || len(result["redirect_replacement"]) > 0 { diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index fc0e95480..9d2f4783c 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -134,7 +134,7 @@ func TestEntryPoints_Set(t *testing.T) { }{ { name: "all parameters camelcase", - expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1 ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", + expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car CA.Optional:false Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1 ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -152,7 +152,10 @@ func TestEntryPoints_Set(t *testing.T) { }, WhitelistSourceRange: []string{"Range"}, TLS: &tls.TLS{ - ClientCAFiles: []string{"car"}, + ClientCA: tls.ClientCA{ + Files: []string{"car"}, + Optional: false, + }, Certificates: tls.Certificates{ { CertFile: tls.FileOrContent("goo"), @@ -164,7 +167,7 @@ func TestEntryPoints_Set(t *testing.T) { }, { name: "all parameters lowercase", - expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.trustedIPs:192.168.0.1 forwardedHeaders.trustedIPs:10.0.0.3/24,20.0.0.3/24", + expression: "name:foo address::8000 tls:goo,gii tls ca:car ca.optional:true redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.trustedIPs:192.168.0.1 forwardedHeaders.trustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -182,7 +185,10 @@ func TestEntryPoints_Set(t *testing.T) { }, WhitelistSourceRange: []string{"Range"}, TLS: &tls.TLS{ - ClientCAFiles: []string{"car"}, + ClientCA: tls.ClientCA{ + Files: []string{"car"}, + Optional: true, + }, Certificates: tls.Certificates{ { CertFile: tls.FileOrContent("goo"), diff --git a/docs/basics.md b/docs/basics.md index 70b32370e..95beac7f1 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -62,10 +62,13 @@ And here is another example with client certificate authentication: [entryPoints.https] address = ":443" [entryPoints.https.tls] - clientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"] - [[entryPoints.https.tls.certificates]] - certFile = "tests/traefik.crt" - keyFile = "tests/traefik.key" + [entryPoints.https.tls] + [entryPoints.https.tls.ClientCA] + files = ["tests/clientca1.crt", "tests/clientca2.crt"] + optional = false + [[entryPoints.https.tls.certificates]] + certFile = "tests/traefik.crt" + keyFile = "tests/traefik.key" ``` - We enable SSL on `https` by giving a certificate and a key. diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 47f017baa..5001fa95d 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -72,11 +72,13 @@ Define an entrypoint with SNI support. ## TLS Mutual Authentication -Only accept clients that present a certificate signed by a specified Certificate Authority (CA). +TLS Mutual Authentication can be `optional` or not. +If it's `optional`, Træfik will authorize connection with certificates not signed by a specified Certificate Authority (CA). +Otherwise, Træfik will only accept clients that present a certificate signed by a specified Certificate Authority (CA). `ClientCAFiles` can be configured with multiple `CA:s` in the same file or use multiple files containing one or several `CA:s`. The `CA:s` has to be in PEM format. -All clients will be required to present a valid cert. +By default, `ClientCAFiles` is not optional, all clients will be required to present a valid cert. The requirement will apply to all server certs in the entrypoint. In the example below both `snitest.com` and `snitest.org` will require client certs @@ -86,7 +88,9 @@ In the example below both `snitest.com` and `snitest.org` will require client ce [entryPoints.https] address = ":443" [entryPoints.https.tls] - ClientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"] + [entryPoints.https.tls.ClientCA] + files = ["tests/clientca1.crt", "tests/clientca2.crt"] + optional = false [[entryPoints.https.tls.certificates]] certFile = "integration/fixtures/https/snitest.com.cert" keyFile = "integration/fixtures/https/snitest.com.key" @@ -95,6 +99,11 @@ In the example below both `snitest.com` and `snitest.org` will require client ce keyFile = "integration/fixtures/https/snitest.org.key" ``` +!!! note + +The deprecated argument `ClientCAFiles` allows adding Client CA files which are mandatory. +If this parameter exists, the new ones are not checked. + ## Authentication ### Basic Authentication diff --git a/integration/fixtures/https/clientca/https_1ca1config.toml b/integration/fixtures/https/clientca/https_1ca1config.toml index 5370ed080..320796be1 100644 --- a/integration/fixtures/https/clientca/https_1ca1config.toml +++ b/integration/fixtures/https/clientca/https_1ca1config.toml @@ -6,7 +6,9 @@ defaultEntryPoints = ["https"] [entryPoints.https] address = ":4443" [entryPoints.https.tls] - ClientCAFiles = ["fixtures/https/clientca/ca1.crt"] + [entryPoints.https.tls.ClientCA] + files = ["fixtures/https/clientca/ca1.crt"] + optional = true [[entryPoints.https.tls.certificates]] certFile = "fixtures/https/snitest.com.cert" keyFile = "fixtures/https/snitest.com.key" diff --git a/integration/fixtures/https/clientca/https_2ca1config.toml b/integration/fixtures/https/clientca/https_2ca1config.toml index 5d1e4e9e9..741554de7 100644 --- a/integration/fixtures/https/clientca/https_2ca1config.toml +++ b/integration/fixtures/https/clientca/https_2ca1config.toml @@ -6,7 +6,8 @@ defaultEntryPoints = ["https"] [entryPoints.https] address = ":4443" [entryPoints.https.tls] - ClientCAFiles = ["fixtures/https/clientca/ca1and2.crt"] + [entryPoints.https.tls.ClientCA] + files = ["fixtures/https/clientca/ca1and2.crt"] [[entryPoints.https.tls.certificates]] certFile = "fixtures/https/snitest.com.cert" keyFile = "fixtures/https/snitest.com.key" diff --git a/integration/fixtures/https/clientca/https_2ca2config.toml b/integration/fixtures/https/clientca/https_2ca2config.toml index 66bdbdb54..fe904e245 100644 --- a/integration/fixtures/https/clientca/https_2ca2config.toml +++ b/integration/fixtures/https/clientca/https_2ca2config.toml @@ -6,7 +6,9 @@ defaultEntryPoints = ["https"] [entryPoints.https] address = ":4443" [entryPoints.https.tls] - ClientCAFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] + [entryPoints.https.tls.ClientCA] + files = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] + optional = false [[entryPoints.https.tls.certificates]] certFile = "fixtures/https/snitest.com.cert" keyFile = "fixtures/https/snitest.com.key" diff --git a/integration/https_test.go b/integration/https_test.go index cb5907f1e..38c673bba 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -116,7 +116,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { } // TestWithClientCertificateAuthentication -// The client has to send a certificate signed by a CA trusted by the server +// The client can send a certificate signed by a CA trusted by the server but it's optional func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { cmd, display := s.traefikCmd(withConfigFile("fixtures/https/clientca/https_1ca1config.toml")) defer display(c) @@ -135,7 +135,7 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { } // Connection without client certificate should fail _, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) - c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server")) + c.Assert(err, checker.IsNil, check.Commentf("should be allowed to connect to server")) // Connect with client certificate signed by ca1 cert, err := tls.LoadX509KeyPair("fixtures/https/clientca/client1.crt", "fixtures/https/clientca/client1.key") @@ -147,6 +147,16 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { conn.Close() + // Connect with client certificate not signed by ca1 + cert, err = tls.LoadX509KeyPair("fixtures/https/snitest.org.cert", "fixtures/https/snitest.org.key") + c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key")) + tlsConfig.Certificates = append(tlsConfig.Certificates, cert) + + conn, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + conn.Close() + // Connect with client signed by ca2 should fail tlsConfig = &tls.Config{ InsecureSkipVerify: true, @@ -158,8 +168,7 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { tlsConfig.Certificates = append(tlsConfig.Certificates, cert) _, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) - c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server")) - + c.Assert(err, checker.IsNil, check.Commentf("should be allowed to connect to server")) } // TestWithClientCertificateAuthentication diff --git a/server/server.go b/server/server.go index de620b58b..f29271745 100644 --- a/server/server.go +++ b/server/server.go @@ -574,8 +574,13 @@ func createClientTLSConfig(entryPointName string, tlsOption *traefikTls.TLS) (*t } if len(tlsOption.ClientCAFiles) > 0 { + log.Warnf("Deprecated configuration found during client TLS configuration creation: %s. Please use %s (which allows to make the CA Files optional).", "tls.ClientCAFiles", "tls.ClientCA.files") + tlsOption.ClientCA.Files = tlsOption.ClientCAFiles + tlsOption.ClientCA.Optional = false + } + if len(tlsOption.ClientCA.Files) > 0 { pool := x509.NewCertPool() - for _, caFile := range tlsOption.ClientCAFiles { + for _, caFile := range tlsOption.ClientCA.Files { data, err := ioutil.ReadFile(caFile) if err != nil { return nil, err @@ -611,8 +616,13 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *traefikT config.NextProtos = []string{"h2", "http/1.1"} if len(tlsOption.ClientCAFiles) > 0 { + log.Warnf("Deprecated configuration found during TLS configuration creation: %s. Please use %s (which allows to make the CA Files optional).", "tls.ClientCAFiles", "tls.ClientCA.files") + tlsOption.ClientCA.Files = tlsOption.ClientCAFiles + tlsOption.ClientCA.Optional = false + } + if len(tlsOption.ClientCA.Files) > 0 { pool := x509.NewCertPool() - for _, caFile := range tlsOption.ClientCAFiles { + for _, caFile := range tlsOption.ClientCA.Files { data, err := ioutil.ReadFile(caFile) if err != nil { return nil, err @@ -623,7 +633,11 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *traefikT } } config.ClientCAs = pool - config.ClientAuth = tls.RequireAndVerifyClientCert + if tlsOption.ClientCA.Optional { + config.ClientAuth = tls.VerifyClientCertIfGiven + } else { + config.ClientAuth = tls.RequireAndVerifyClientCert + } } if server.globalConfiguration.ACME != nil { diff --git a/tls/tls.go b/tls/tls.go index bfe9c8c1a..1798da12d 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -6,12 +6,20 @@ import ( "strings" ) +// 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 []string + Optional bool +} + // TLS configures TLS for an entry point type TLS struct { MinVersion string `export:"true"` CipherSuites []string Certificates Certificates - ClientCAFiles []string + ClientCAFiles []string // Deprecated + ClientCA ClientCA } // RootCAs hold the CA we want to have in root diff --git a/types/types.go b/types/types.go index 1335132dd..3cabd236c 100644 --- a/types/types.go +++ b/types/types.go @@ -445,6 +445,7 @@ type AccessLog struct { // CA, Cert and Key can be either path or file contents type ClientTLS struct { CA string `description:"TLS CA"` + CAOptional bool `description:"TLS CA.Optional"` Cert string `description:"TLS cert"` Key string `description:"TLS key"` InsecureSkipVerify bool `description:"TLS insecure skip verify"` @@ -458,6 +459,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { return nil, nil } caPool := x509.NewCertPool() + clientAuth := tls.NoClientCert if clientTLS.CA != "" { var ca []byte if _, errCA := os.Stat(clientTLS.CA); errCA == nil { @@ -469,6 +471,11 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { ca = []byte(clientTLS.CA) } caPool.AppendCertsFromPEM(ca) + if clientTLS.CAOptional { + clientAuth = tls.VerifyClientCertIfGiven + } else { + clientAuth = tls.RequireAndVerifyClientCert + } } cert := tls.Certificate{} @@ -505,6 +512,7 @@ func (clientTLS *ClientTLS) CreateTLSConfig() (*tls.Config, error) { Certificates: []tls.Certificate{cert}, RootCAs: caPool, InsecureSkipVerify: clientTLS.InsecureSkipVerify, + ClientAuth: clientAuth, } return TLSConfig, nil }