diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index dceabc0d8..f15f99aaa 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -2,6 +2,7 @@ package main import ( "crypto/tls" + "crypto/x509" "encoding/json" "fmt" fmtlog "log" @@ -28,6 +29,7 @@ import ( "github.com/coreos/go-systemd/daemon" "github.com/docker/libkv/store" "github.com/satori/go.uuid" + "golang.org/x/net/http2" ) func main() { @@ -104,6 +106,7 @@ Complete documentation is available at https://traefik.io`, //add custom parsers f.AddParser(reflect.TypeOf(server.EntryPoints{}), &server.EntryPoints{}) f.AddParser(reflect.TypeOf(server.DefaultEntryPoints{}), &server.DefaultEntryPoints{}) + f.AddParser(reflect.TypeOf(server.RootCAs{}), &server.RootCAs{}) f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{}) f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{}) f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{}) @@ -180,6 +183,23 @@ func run(traefikConfiguration *server.TraefikConfiguration) { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } + if len(globalConfiguration.RootCAs) > 0 { + roots := x509.NewCertPool() + for _, cert := range globalConfiguration.RootCAs { + certContent, err := cert.Read() + if err != nil { + log.Error("Error while read RootCAs", err) + continue + } + roots.AppendCertsFromPEM(certContent) + } + + tr := http.DefaultTransport.(*http.Transport) + tr.TLSClientConfig = &tls.Config{RootCAs: roots} + + http2.ConfigureTransport(tr) + } + if globalConfiguration.File != nil && len(globalConfiguration.File.Filename) == 0 { // no filename, setting to global config file if len(traefikConfiguration.ConfigFile) != 0 { diff --git a/docs/toml.md b/docs/toml.md index b1211ea34..189eea033 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -95,6 +95,13 @@ # # InsecureSkipVerify = true +# Register Certificates in the RootCA. This certificates will be use for backends calls. +# Note: You can use file path or cert content directly +# Optional +# Default: [] +# +# RootCAs = [ "/mycert.cert" ] + # Entrypoints to be used by frontends that do not specify any entrypoint. # Each frontend can specify its own entrypoints. # diff --git a/integration/fixtures/https/rootcas/https.toml b/integration/fixtures/https/rootcas/https.toml new file mode 100644 index 000000000..4623351cd --- /dev/null +++ b/integration/fixtures/https/rootcas/https.toml @@ -0,0 +1,41 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http"] + +# Use certificate in net/internal/testcert.go +RootCAs = [ """ +-----BEGIN CERTIFICATE----- +MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw +MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 +iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul +rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO +BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw +AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA +AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 +tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs +h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM +fblo6RBxUQ== +-----END CERTIFICATE----- +"""] + +[entryPoints] + [entryPoints.http] + address = ":8081" + +[web] + address = ":8080" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "{{ .BackendHost }}" + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Path: /ping" \ No newline at end of file diff --git a/integration/fixtures/https/rootcas/https_with_file.toml b/integration/fixtures/https/rootcas/https_with_file.toml new file mode 100644 index 000000000..294ba4a75 --- /dev/null +++ b/integration/fixtures/https/rootcas/https_with_file.toml @@ -0,0 +1,25 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http"] + +# Use certificate in net/internal/testcert.go +RootCAs = [ "fixtures/https/rootcas/local.crt"] + +[entryPoints] + [entryPoints.http] + address = ":8081" + +[web] + address = ":8080" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "{{ .BackendHost }}" +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Path: /ping" \ No newline at end of file diff --git a/integration/fixtures/https/rootcas/local.crt b/integration/fixtures/https/rootcas/local.crt new file mode 100644 index 000000000..017a30876 --- /dev/null +++ b/integration/fixtures/https/rootcas/local.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw +MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 +iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul +rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO +BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw +AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA +AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 +tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs +h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM +fblo6RBxUQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/integration/https_test.go b/integration/https_test.go index ec4a02846..093ed0b37 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/http/httptest" + "os" "time" "github.com/containous/traefik/integration/try" @@ -271,6 +272,48 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAsMultipleFi c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server")) } +func (s *HTTPSSuite) TestWithRootCAsContentForHTTPSOnBackend(c *check.C) { + backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + defer backend.Close() + + file := s.adaptFile(c, "fixtures/https/rootcas/https.toml", struct{ BackendHost string }{backend.URL}) + defer os.Remove(file) + cmd, _ := s.cmdTraefikWithConfigFile(file) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains(backend.URL)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8081/ping", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) +} + +func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { + backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + })) + defer backend.Close() + + file := s.adaptFile(c, "fixtures/https/rootcas/https_with_file.toml", struct{ BackendHost string }{backend.URL}) + defer os.Remove(file) + cmd, _ := s.cmdTraefikWithConfigFile(file) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains(backend.URL)) + c.Assert(err, checker.IsNil) + + err = try.GetRequest("http://127.0.0.1:8081/ping", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + c.Assert(err, checker.IsNil) +} + func startTestServer(port string, statusCode int) (ts *httptest.Server) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(statusCode) diff --git a/server/configuration.go b/server/configuration.go index 2fc81428e..9c2bdd638 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -2,8 +2,8 @@ package server import ( "crypto/tls" - "errors" "fmt" + "io/ioutil" "os" "regexp" "strings" @@ -56,6 +56,7 @@ type GlobalConfiguration struct { MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used"` IdleTimeout flaeg.Duration `description:"maximum amount of time an idle (keep-alive) connection will remain idle before closing itself."` InsecureSkipVerify bool `description:"Disable SSL certificate verification"` + RootCAs RootCAs `description:"Add cert file for self-signed certicate"` Retry *Retry `description:"Enable retry sending request if network error"` HealthCheck *HealthCheckConfig `description:"Health check parameters"` Docker *docker.Provider `description:"Enable Docker backend"` @@ -110,7 +111,69 @@ func (dep *DefaultEntryPoints) SetValue(val interface{}) { // Type is type of the struct func (dep *DefaultEntryPoints) Type() string { - return fmt.Sprint("defaultentrypoints") + return "defaultentrypoints" +} + +// RootCAs hold the CA we want to have in root +type RootCAs []FileOrContent + +// FileOrContent hold a file path or content +type FileOrContent string + +func (f FileOrContent) String() string { + return string(f) +} + +func (f FileOrContent) Read() ([]byte, error) { + var content []byte + if _, err := os.Stat(f.String()); err == nil { + content, err = ioutil.ReadFile(f.String()) + if err != nil { + return nil, err + } + } else { + content = []byte(f) + } + return content, nil +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (r *RootCAs) String() string { + sliceOfString := make([]string, len([]FileOrContent(*r))) + for key, value := range *r { + sliceOfString[key] = value.String() + } + return strings.Join(sliceOfString, ",") +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (r *RootCAs) Set(value string) error { + rootCAs := strings.Split(value, ",") + if len(rootCAs) == 0 { + return fmt.Errorf("bad RootCAs format: %s", value) + } + for _, rootCA := range rootCAs { + *r = append(*r, FileOrContent(rootCA)) + } + return nil +} + +// Get return the EntryPoints map +func (r *RootCAs) Get() interface{} { + return RootCAs(*r) +} + +// SetValue sets the EntryPoints map with val +func (r *RootCAs) SetValue(val interface{}) { + *r = RootCAs(val.(RootCAs)) +} + +// Type is type of the struct +func (r *RootCAs) Type() string { + return "rootcas" } // EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...) @@ -192,7 +255,7 @@ func (ep *EntryPoints) SetValue(val interface{}) { // Type is type of the struct func (ep *EntryPoints) Type() string { - return fmt.Sprint("entrypoints") + return "entrypoints" } // EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...) @@ -264,32 +327,25 @@ func (certs *Certificates) CreateTLSConfig() (*tls.Config, error) { config.Certificates = []tls.Certificate{} certsSlice := []Certificate(*certs) for _, v := range certsSlice { - isAPath := false - _, errCert := os.Stat(v.CertFile) - _, errKey := os.Stat(v.KeyFile) - if errCert == nil { - if errKey == nil { - isAPath = true - } else { - return nil, errors.New("bad TLS Certificate KeyFile format, expected a path") - } - } else if errKey == nil { - return nil, errors.New("bad TLS Certificate KeyFile format, expected a path") + cert := tls.Certificate{} + + var err error + + certContent, err := v.CertFile.Read() + if err != nil { + return nil, err } - cert := tls.Certificate{} - var err error - if isAPath { - cert, err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile) - if err != nil { - return nil, err - } - } else { - cert, err = tls.X509KeyPair([]byte(v.CertFile), []byte(v.KeyFile)) - if err != nil { - return nil, err - } + keyContent, err := v.KeyFile.Read() + if err != nil { + return nil, err } + + cert, err = tls.X509KeyPair(certContent, keyContent) + if err != nil { + return nil, err + } + config.Certificates = append(config.Certificates, cert) } return config, nil @@ -303,7 +359,7 @@ func (certs *Certificates) String() string { } var result []string for _, certificate := range *certs { - result = append(result, certificate.CertFile+","+certificate.KeyFile) + result = append(result, certificate.CertFile.String()+","+certificate.KeyFile.String()) } return strings.Join(result, ";") } @@ -319,8 +375,8 @@ func (certs *Certificates) Set(value string) error { return fmt.Errorf("bad certificates format: %s", value) } *certs = append(*certs, Certificate{ - CertFile: files[0], - KeyFile: files[1], + CertFile: FileOrContent(files[0]), + KeyFile: FileOrContent(files[1]), }) } return nil @@ -328,14 +384,14 @@ func (certs *Certificates) Set(value string) error { // Type is type of the struct func (certs *Certificates) Type() string { - return fmt.Sprint("certificates") + return "certificates" } // Certificate holds a SSL cert/key pair // Certs and Key could be either a file path, or the file content itself type Certificate struct { - CertFile string - KeyFile string + CertFile FileOrContent + KeyFile FileOrContent } // Retry contains request retry config diff --git a/traefik.sample.toml b/traefik.sample.toml index b2e1a1db2..ffc69b16f 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -76,6 +76,13 @@ # # InsecureSkipVerify = true +# Register Certificates in the RootCA. This certificates will be use for backends calls. +# Note: You can use file path or cert content directly +# Optional +# Default: [] +# +# RootCAs = [ "/mycert.cert" ] + # Entrypoints to be used by frontends that do not specify any entrypoint. # Each frontend can specify its own entrypoints. #