diff --git a/configuration.go b/configuration.go index 25b4d035c..92dadf383 100644 --- a/configuration.go +++ b/configuration.go @@ -16,7 +16,7 @@ type GlobalConfiguration struct { GraceTimeOut int64 AccessLogsFile string TraefikLogsFile string - CertFile, KeyFile string + Certificates []Certificate LogLevel string ProvidersThrottleDuration time.Duration Docker *provider.Docker @@ -29,6 +29,12 @@ type GlobalConfiguration struct { Boltdb *provider.BoltDb } +// Certificate holds a SSL cert/key pair +type Certificate struct { + CertFile string + KeyFile string +} + // NewGlobalConfiguration returns a GlobalConfiguration with default values. func NewGlobalConfiguration() *GlobalConfiguration { globalConfiguration := new(GlobalConfiguration) diff --git a/docs/index.md b/docs/index.md index 1e566de79..eca6c7ca4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -102,10 +102,12 @@ For example: # # logLevel = "ERROR" -# SSL certificate and key used +# SSL certificates and keys +# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI # # Optional # +# [[certificates]] # CertFile = "traefik.crt" # KeyFile = "traefik.key" diff --git a/integration/fixtures/https/https_sni.toml b/integration/fixtures/https/https_sni.toml new file mode 100644 index 000000000..6af20c15f --- /dev/null +++ b/integration/fixtures/https/https_sni.toml @@ -0,0 +1,32 @@ +port = ":443" +logLevel = "DEBUG" + +[[certificates]] +CertFile = "fixtures/https/snitest.com.cert" +KeyFile = "fixtures/https/snitest.com.key" + +[[certificates]] +CertFile = "fixtures/https/snitest.org.cert" +KeyFile = "fixtures/https/snitest.org.key" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:9010" + [backends.backend2] + [backends.backend2.servers.server1] + url = "http://127.0.0.1:9020" + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host" + value = "snitest.com" + [frontends.frontend2] + backend = "backend2" + [frontends.frontend2.routes.test_2] + rule = "Host" + value = "snitest.org" diff --git a/integration/fixtures/https/snitest.com.cert b/integration/fixtures/https/snitest.com.cert new file mode 100644 index 000000000..1efb82d17 --- /dev/null +++ b/integration/fixtures/https/snitest.com.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIJAL858pci5XyjMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMMC3NuaXRlc3QuY29tMB4XDTE1MTEyMzIyMDU1NloXDTI1MTEyMDIyMDU1Nlow +FjEUMBIGA1UEAwwLc25pdGVzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDVF25wEroSIUO/dNgHxlyt8pZFVpJ8fNaJw7cnlZ1JP2hLuEbmjAFT +dHqS8wKDNYHktsBEOUfN/qbk0AiGb+SvhQw6kfM/QSj9fXVQ7KhYP9XYOekTOH7d +M0Z2L3RGgqs8z+83exOOnAFVvIJCMZJXEeijV6iJlmpCcJa0Kg/JHlxhoWTEeZuU +G+hITafk1yWOKorTCPlMhB30wuQoWfbHP+3G0bsERLXFiMANE8EtQu8+ZhfseBUh +5Tu5gIC4Fnria7mRixAZeEiAblFP9h0vrNRcP3nmuVz0tHPIeQsJQiEhxaZ09oUW +h9WqTsYCP1821+SVazM9oFRTpy6chZyTAgMBAAGjUDBOMB0GA1UdDgQWBBSz9mbX +ia1TM5FG4Zgagaet24S8HDAfBgNVHSMEGDAWgBSz9mbXia1TM5FG4Zgagaet24S8 +HDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB79W8XTxlozh9w/W7T +5vDev67G4T/wetABSb68CRrqojt78PMJuS89JarA8I3ts00O+0JYnsHxp+9qC7pf +jWHcDSiLwRUMu7MXW/KIen1EB8BQNA0xWbAiQaWYPHzsBlX48+9wBe0HTDx7Lcxb +OsmnXHBF5fd2EY+R8qJu+PyTDDL1WLItFJpzHiFiGiYF8Tyic3kkPjje6eIOxRmT +hq+qbwApzbzz6h/VD5xR3zBDFBo2Xs5tdP264KIw/YXDpaXVBiJ5DDjQ3dtJw1G5 +yzgrHQZWJN8Gs8ZZgGdgRf7PHox8xEZtqPiMkChDz6T7Ha3U0xYN6TZGNZOR6DHs +K9/8 +-----END CERTIFICATE----- diff --git a/integration/fixtures/https/snitest.com.key b/integration/fixtures/https/snitest.com.key new file mode 100644 index 000000000..35638a803 --- /dev/null +++ b/integration/fixtures/https/snitest.com.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG +5owBU3R6kvMCgzWB5LbARDlHzf6m5NAIhm/kr4UMOpHzP0Eo/X11UOyoWD/V2Dnp +Ezh+3TNGdi90RoKrPM/vN3sTjpwBVbyCQjGSVxHoo1eoiZZqQnCWtCoPyR5cYaFk +xHmblBvoSE2n5NcljiqK0wj5TIQd9MLkKFn2xz/txtG7BES1xYjADRPBLULvPmYX +7HgVIeU7uYCAuBZ64mu5kYsQGXhIgG5RT/YdL6zUXD955rlc9LRzyHkLCUIhIcWm +dPaFFofVqk7GAj9fNtfklWszPaBUU6cunIWckwIDAQABAoIBABAdQYDAKcoNMe5c +i6mq2n9dBPghX9qCJkcswcEAk3BilySCvvnYRJFnEY3jSqFZfoUpPMjr+/4b78sF +4F8qPwT27sHPH7H833ir8B86hlCGI0nCt1l4wD9CDWYKmKRsZT6oCtMLP6NdMMyn +AMK4tPRYqlsP2fLtqQN1ODBPrfnraoNHtOVE784iBCD5dewICA5RIQG2i/d2+CGF ++bahFqUXVCqHoxBz4AVvrRFL99VcP7P2iZyk6hDQ7fci7Xay8Wb/HutRxuqvF0aU +bG6Enk6CCtNZHLwNPp4Hqft0Udvg2tG8okYwbEmoEO40nQsCSzRCpq5Uvzi+LX1k +LykQ6+ECgYEA7x8vQoyOK60Q3LPpJFGDec2+XJPoesTfJTT6idaP7ukUL8p3FsUo +9vtxRRfhSOdPoAINmrL0TyMekO2B6zXx0pmWVpMrFwZW6zMwZAnLp/w+3USpbGCy +K12IIwvRYzTzKwoMTVAKTXm36b6oqr2La4bTdJR7REY6G374FrJb/H0CgYEA5CHk +Ym0h7cf00fw9UEHRfzUZxmCfRWY6K8InOuHdLi+u4TiyXzs8x5s0e/DN/raNmTGx +QO81UzuS3nKwc4n5QyXjVnhzR5DbbSACDwHtdnxZByL0D1KvPjtRF8F+rWXViXv2 +TM7UiOmn6R375FPSAPxeyMx8Womc3EnAAfLWGk8CgYEAv8I2WBv3dzcWqqbsdF+a +G/fOjNdgO/PdLy1JLXiPfHwV4C1xSyVZMJd7wnjgBWLaC+sZldGk8kGrpXWSFlnw +T38zfMIQcCp5Uax/RfpFA7XZhAAoDe2NdBFRtyknBXPU/dLVArsJSBAwWJa5FBNk +1xoMQRVBtQLMXnh341utQNECgYEA4o1R2/ka16NaWmpPjXM/lD9skFgF84p4vFn8 +UXpaB3LtDdcbNH2Ed4mHToouWAR8jCUQLTcg0r53tRdaafMcKfXnVUka2nhdoHpH +8RVt99u3IeIxU0I+q+OGPbw3jAV0UStcxpwj7q9zw4q2SuJ+y+HUUz7XQ6Yjs5Q9 +7PF2c/sCgYEAhdVn5gZ5FvYKrBi46t3pxPsWK476HmQEVHVi5+od7wg+araDelAe +8QE8hc8qdZGbjdB/AHSPCeUxfO2vnpsCoSRs29o6pDvQuqvHYs+M53l5LEYeOjof +t6J/DK5Pim2CAFjYFcZk8/Gyl5HjTw3PpdWxoPD5v2Xw3bbY57IIbm4= +-----END RSA PRIVATE KEY----- diff --git a/integration/fixtures/https/snitest.org.cert b/integration/fixtures/https/snitest.org.cert new file mode 100644 index 000000000..251ca939c --- /dev/null +++ b/integration/fixtures/https/snitest.org.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIJALAYHG/vGqWEMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMMC3NuaXRlc3Qub3JnMB4XDTE1MTEyMzIyMDU0NFoXDTI1MTEyMDIyMDU0NFow +FjEUMBIGA1UEAwwLc25pdGVzdC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC8b2Qv68Xnv4wgJ6HNupxABSUA5KXmv9g7pwwsFMSOK15o2qGFzx/x +9loIi5pMIYIy4SVwJNrYUi772nCYMqSIVXlwct/CE70j2Jb2geIHu3jHbFWXruWb +W1tGGUYzvnsOUziPE3rLWa/NObNYLLlUKJaxfHrxnpuKpQUsXzoLl25cJEVr4jg2 +ZITpdraxaBLisdlWY7EwwHBLu2nxH5Rn+nIjenFfdUwKF9s5dGy63tfBc8LX9yJk ++kOwy1al/Wxa0DUb6rSt0QDCcD+rXnjk2zWPtsHz1btwtqM+FLtN5z0Lmnx7DF3C +tCf1TMzduzZ6aeHk77zc664ZQun5cH33AgMBAAGjUDBOMB0GA1UdDgQWBBRn/nNz +PUsmDKmKv3GGo3km5KKvUDAfBgNVHSMEGDAWgBRn/nNzPUsmDKmKv3GGo3km5KKv +UDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBkuutIcbBdESgvNLLr +k/8HUDuFm72lYHZFE+c76CxqYN52w02NCTiq1InoDUvqZXb/StATBwRRduTUPCj9 +KUkC7pOjAFxjzjExsHrtZSq01WinrxNI+qSKvI8jFngMHnwN1omTt7/D7nxeW5Of +FJTkElnxtELAGHoIwZ+bKprnexefpn9UW84VJvJ2crSR63vBvdTrgsrEGW6kQj1I +62laDpax4+x8t2h+sfG6uNIA1cFrG8Sk+O2Bi3ogB7Y/4e8r6WA23IRP+aSv0J2b +k5fvuuXbIc979pQOoO03zG0S7Wpmpsw+9dQB9TOxGITOLfCZwEuIhnv+M9lLqCks +7H2A +-----END CERTIFICATE----- diff --git a/integration/fixtures/https/snitest.org.key b/integration/fixtures/https/snitest.org.key new file mode 100644 index 000000000..7714e49e3 --- /dev/null +++ b/integration/fixtures/https/snitest.org.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r/YO6cMLBTEjiteaNqh +hc8f8fZaCIuaTCGCMuElcCTa2FIu+9pwmDKkiFV5cHLfwhO9I9iW9oHiB7t4x2xV +l67lm1tbRhlGM757DlM4jxN6y1mvzTmzWCy5VCiWsXx68Z6biqUFLF86C5duXCRF +a+I4NmSE6Xa2sWgS4rHZVmOxMMBwS7tp8R+UZ/pyI3pxX3VMChfbOXRsut7XwXPC +1/ciZPpDsMtWpf1sWtA1G+q0rdEAwnA/q1545Ns1j7bB89W7cLajPhS7Tec9C5p8 +ewxdwrQn9UzM3bs2emnh5O+83OuuGULp+XB99wIDAQABAoIBAGOn9bByXQQnhZAr +5aLMIn6pOdyzEBptM4q42fMmOJ2HyjJiDjKaTCbHRu5mBoBk6FrIP+iDVUo6jKad +7BZSEjoYGlWiKzyU+97NWWmdX1D/kOzHGq1RzhTPyAHWtA4Bm0sEMFFa2AJbuGIt +NfBYFtuva6MKVmsamuBETewdoLEnxzzDFcuOaxXRfTC/ikWcYyB4KEWA5fjroUQC +Llo9/UTGTkh1Hynv9AXY6Qia/RbrIQjKveKCRj6PjxyE/qN9qfmngczz2pK0hRhL +Z+K06y8G+Yj1I1zm5jNg1kakVQKoBsnaYkmIUBUSmWv6ERotedOWtOAMlOKa+0l2 +DS7Ou2ECgYEA91doi+3XrMVsgyTEm/ArzEyRUfM5dCSvBCRFhO7QQp2OYAbjJk5S +pmdpqmwTsXNNMU+XNkWCLug5pk0PTJwP0mVLE2fLYqCCXoyaMltQ0Yk2gaun/RwE +w5EfyMwOQakLFY/ODvduQfyNpaoWgFz4/WPNTVNCGs04LepSGKaFNy0CgYEAwwgV +jKeFA+QZGooTInyk07ZlAbenEPu/c2y3UUFxclP0CjP2/VBOpz9B62vhzCKbjD1c +/L3x1CKC4n4lbeyHi4vrF69LX9SHr1Jm0SUtyKeV3g0EAzIWI0HFhVUkMvtbibQ4 +HXrLVCJO77xetQ7RQszss1z9g3WotAAiBMiQgDMCgYBTLjoilOIrYFmV4Q+dwa95 +DWbxwHJZ9NxG8EvQ4N95B7OR578Matqwy6ZlgeM9kiErrDCWN9oIHGEG5HN4uCM6 +BoaxB/8GNCSj13Uj6kHLtfF2ulvMa1fOzUd7J+TDgC4SGkKaFewmlOCuDf1zPdEe +pimtD4rzqIB0MJFbaOT0IQKBgDBPjlb7IB3ooLdMQJUoXwP6iGa2gXHZioEjCv3b +wihZ13e3i5UQEYuoRcH1RUd1wyYoBSKuQnsT2WwVZ1wlXSYaELAbQgaI9NtfBA0G +sqKjsKICg13vSECPiEgQ4Rin3vLra4MR6c/7d6Y2+RbMhtWPQYrkm/+2Y4XDCqo4 +rGK1AoGAOFZ3RVhuwXzFdKNe32LM1wm1eZ7waxjI4bQS2xUN/3C/uWS7A3LaSlc3 +eRG3DaVpez4DQVupZDHMgxJUYqqKynUj6GD1YiaxGROj3TYCu6e7OxyhalhCllSu +w/X5M802XqzLjeec5zHoZDfknnAkgR9MsxZYmZPFaDyL6GOKUB8= +-----END RSA PRIVATE KEY----- diff --git a/integration/https_test.go b/integration/https_test.go new file mode 100644 index 000000000..9a81609e0 --- /dev/null +++ b/integration/https_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httptest" + "os/exec" + "time" + + checker "github.com/vdemeester/shakers" + check "gopkg.in/check.v1" +) + +// HTTPSSuite +type HTTPSSuite struct{ BaseSuite } + +// TestWithSNIConfigHandshake involves a client sending a SNI hostname of +// "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test +// verifies that traefik presents the correct certificate. +func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) { + cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + time.Sleep(500 * time.Millisecond) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + } + conn, err := tls.Dial("tcp", "127.0.0.1:443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("snitest.com") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not match SNI servername")) +} + +// TestWithSNIConfigRoute involves a client sending HTTPS requests with +// SNI hostnames of "snitest.org" and "snitest.com". The test verifies +// that traefik routes the requests to the expected backends. +func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { + cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + backend1 := startTestServer("9010", 204) + backend2 := startTestServer("9020", 205) + defer backend1.Close() + defer backend2.Close() + + time.Sleep(2000 * time.Millisecond) + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + client := &http.Client{Transport: tr1} + req, _ := http.NewRequest("GET", "https://127.0.0.1/", nil) + req.Host = "snitest.com" + req.Header.Set("Host", "snitest.com") + req.Header.Set("Accept", "*/*") + resp, err := client.Do(req) + c.Assert(err, checker.IsNil) + // Expected a 204 (from backend1) + c.Assert(resp.StatusCode, checker.Equals, 204) + + client = &http.Client{Transport: tr2} + req, _ = http.NewRequest("GET", "https://127.0.0.1/", nil) + req.Host = "snitest.org" + req.Header.Set("Host", "snitest.org") + req.Header.Set("Accept", "*/*") + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + // Expected a 205 (from backend2) + c.Assert(resp.StatusCode, checker.Equals, 205) +} + +func startTestServer(port string, statusCode int) (ts *httptest.Server) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(statusCode) + }) + listener, err := net.Listen("tcp", "127.0.0.1:"+port) + if err != nil { + panic(err) + } + + ts = &httptest.Server{ + Listener: listener, + Config: &http.Server{Handler: handler}, + } + ts.Start() + return +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 4cee94201..72d992bd9 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -24,6 +24,7 @@ func Test(t *testing.T) { func init() { check.Suite(&SimpleSuite{}) + check.Suite(&HTTPSSuite{}) check.Suite(&FileSuite{}) check.Suite(&DockerSuite{}) check.Suite(&ConsulSuite{}) diff --git a/traefik.go b/traefik.go index ee9668a40..c84d99ba4 100644 --- a/traefik.go +++ b/traefik.go @@ -209,35 +209,43 @@ func main() { log.Fatal("Error preparing server: ", er) } go startServer(srv, globalConfiguration) + //TODO change that! + time.Sleep(100 * time.Millisecond) serverLock.Unlock() <-stopChan log.Info("Shutting down") } -func createTLSConfig(certFile string, keyFile string) (*tls.Config, error) { +// creates a TLS config that allows terminating HTTPS for multiple domains using SNI +func createTLSConfig(certs []Certificate) (*tls.Config, error) { + if len(certs) == 0 { + return nil, nil + } + config := &tls.Config{} if config.NextProtos == nil { config.NextProtos = []string{"http/1.1"} } var err error - config.Certificates = make([]tls.Certificate, 1) - if len(certFile) > 0 && len(keyFile) > 0 { - config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + config.Certificates = make([]tls.Certificate, len(certs)) + for i, v := range certs { + config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile) if err != nil { return nil, err } - } else { - return nil, nil } + // BuildNameToCertificate parses the CommonName and SubjectAlternateName fields + // in each certificate and populates the config.NameToCertificate map. + config.BuildNameToCertificate() return config, nil } func startServer(srv *manners.GracefulServer, globalConfiguration *GlobalConfiguration) { log.Info("Starting server") - if len(globalConfiguration.CertFile) > 0 && len(globalConfiguration.KeyFile) > 0 { - err := srv.ListenAndServeTLS(globalConfiguration.CertFile, globalConfiguration.KeyFile) + if srv.TLSConfig != nil { + err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig) if err != nil { log.Fatal("Error creating server: ", err) } @@ -258,7 +266,7 @@ func prepareServer(router *mux.Router, globalConfiguration *GlobalConfiguration, negroni.Use(middleware) } negroni.UseHandler(router) - tlsConfig, err := createTLSConfig(globalConfiguration.CertFile, globalConfiguration.KeyFile) + tlsConfig, err := createTLSConfig(globalConfiguration.Certificates) if err != nil { log.Fatalf("Error creating TLS config %s", err) return nil, err @@ -273,8 +281,9 @@ func prepareServer(router *mux.Router, globalConfiguration *GlobalConfiguration, }), nil } server, err := oldServer.HijackListener(&http.Server{ - Addr: globalConfiguration.Port, - Handler: negroni, + Addr: globalConfiguration.Port, + Handler: negroni, + TLSConfig: tlsConfig, }, tlsConfig) if err != nil { log.Fatalf("Error hijacking server %s", err) diff --git a/traefik.sample.toml b/traefik.sample.toml index 62aef45bb..f6cf80c77 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -37,10 +37,12 @@ # # logLevel = "ERROR" -# SSL certificate and key used +# SSL certificates and keys +# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI # # Optional # +# [[certificates]] # CertFile = "traefik.crt" # KeyFile = "traefik.key"