Skip to content

Commit

Permalink
Modify Client CA certificates management to add optional option
Browse files Browse the repository at this point in the history
  • Loading branch information
nmengin committed Nov 10, 2017
1 parent 1691f58 commit 71c3b2f
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 25 deletions.
10 changes: 8 additions & 2 deletions cmd/traefik/anonymize/anonymize_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 10 additions & 4 deletions configuration/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"),
Expand All @@ -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",
Expand All @@ -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"),
Expand Down
11 changes: 7 additions & 4 deletions docs/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 12 additions & 3 deletions docs/configuration/entrypoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion integration/fixtures/https/clientca/https_1ca1config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion integration/fixtures/https/clientca/https_2ca1config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion integration/fixtures/https/clientca/https_2ca2config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 13 additions & 4 deletions integration/https_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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,
Expand All @@ -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
Expand Down
20 changes: 17 additions & 3 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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 {
Expand All @@ -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{}
Expand Down Expand Up @@ -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
}

0 comments on commit 71c3b2f

Please sign in to comment.