Allow adding optional Client CA files
This commit is contained in:
parent
1691f586d7
commit
4f4491c247
12 changed files with 97 additions and 25 deletions
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
10
tls/tls.go
10
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue