Adds TLS SNI support for the frontends

This commit is contained in:
Jan Broer 2015-11-21 02:59:49 +01:00
parent 1604786285
commit d6e28a923c
11 changed files with 268 additions and 14 deletions

View file

@ -16,7 +16,7 @@ type GlobalConfiguration struct {
GraceTimeOut int64 GraceTimeOut int64
AccessLogsFile string AccessLogsFile string
TraefikLogsFile string TraefikLogsFile string
CertFile, KeyFile string Certificates []Certificate
LogLevel string LogLevel string
ProvidersThrottleDuration time.Duration ProvidersThrottleDuration time.Duration
Docker *provider.Docker Docker *provider.Docker
@ -29,6 +29,12 @@ type GlobalConfiguration struct {
Boltdb *provider.BoltDb Boltdb *provider.BoltDb
} }
// Certificate holds a SSL cert/key pair
type Certificate struct {
CertFile string
KeyFile string
}
// NewGlobalConfiguration returns a GlobalConfiguration with default values. // NewGlobalConfiguration returns a GlobalConfiguration with default values.
func NewGlobalConfiguration() *GlobalConfiguration { func NewGlobalConfiguration() *GlobalConfiguration {
globalConfiguration := new(GlobalConfiguration) globalConfiguration := new(GlobalConfiguration)

View file

@ -102,10 +102,12 @@ For example:
# #
# logLevel = "ERROR" # 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 # Optional
# #
# [[certificates]]
# CertFile = "traefik.crt" # CertFile = "traefik.crt"
# KeyFile = "traefik.key" # KeyFile = "traefik.key"

View file

@ -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"

View file

@ -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-----

View file

@ -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-----

View file

@ -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-----

View file

@ -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-----

110
integration/https_test.go Normal file
View file

@ -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
}

View file

@ -24,6 +24,7 @@ func Test(t *testing.T) {
func init() { func init() {
check.Suite(&SimpleSuite{}) check.Suite(&SimpleSuite{})
check.Suite(&HTTPSSuite{})
check.Suite(&FileSuite{}) check.Suite(&FileSuite{})
check.Suite(&DockerSuite{}) check.Suite(&DockerSuite{})
check.Suite(&ConsulSuite{}) check.Suite(&ConsulSuite{})

View file

@ -209,35 +209,43 @@ func main() {
log.Fatal("Error preparing server: ", er) log.Fatal("Error preparing server: ", er)
} }
go startServer(srv, globalConfiguration) go startServer(srv, globalConfiguration)
//TODO change that!
time.Sleep(100 * time.Millisecond)
serverLock.Unlock() serverLock.Unlock()
<-stopChan <-stopChan
log.Info("Shutting down") 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{} config := &tls.Config{}
if config.NextProtos == nil { if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"} config.NextProtos = []string{"http/1.1"}
} }
var err error var err error
config.Certificates = make([]tls.Certificate, 1) config.Certificates = make([]tls.Certificate, len(certs))
if len(certFile) > 0 && len(keyFile) > 0 { for i, v := range certs {
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
if err != nil { if err != nil {
return nil, err 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 return config, nil
} }
func startServer(srv *manners.GracefulServer, globalConfiguration *GlobalConfiguration) { func startServer(srv *manners.GracefulServer, globalConfiguration *GlobalConfiguration) {
log.Info("Starting server") log.Info("Starting server")
if len(globalConfiguration.CertFile) > 0 && len(globalConfiguration.KeyFile) > 0 { if srv.TLSConfig != nil {
err := srv.ListenAndServeTLS(globalConfiguration.CertFile, globalConfiguration.KeyFile) err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig)
if err != nil { if err != nil {
log.Fatal("Error creating server: ", err) log.Fatal("Error creating server: ", err)
} }
@ -258,7 +266,7 @@ func prepareServer(router *mux.Router, globalConfiguration *GlobalConfiguration,
negroni.Use(middleware) negroni.Use(middleware)
} }
negroni.UseHandler(router) negroni.UseHandler(router)
tlsConfig, err := createTLSConfig(globalConfiguration.CertFile, globalConfiguration.KeyFile) tlsConfig, err := createTLSConfig(globalConfiguration.Certificates)
if err != nil { if err != nil {
log.Fatalf("Error creating TLS config %s", err) log.Fatalf("Error creating TLS config %s", err)
return nil, err return nil, err
@ -273,8 +281,9 @@ func prepareServer(router *mux.Router, globalConfiguration *GlobalConfiguration,
}), nil }), nil
} }
server, err := oldServer.HijackListener(&http.Server{ server, err := oldServer.HijackListener(&http.Server{
Addr: globalConfiguration.Port, Addr: globalConfiguration.Port,
Handler: negroni, Handler: negroni,
TLSConfig: tlsConfig,
}, tlsConfig) }, tlsConfig)
if err != nil { if err != nil {
log.Fatalf("Error hijacking server %s", err) log.Fatalf("Error hijacking server %s", err)

View file

@ -37,10 +37,12 @@
# #
# logLevel = "ERROR" # 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 # Optional
# #
# [[certificates]]
# CertFile = "traefik.crt" # CertFile = "traefik.crt"
# KeyFile = "traefik.key" # KeyFile = "traefik.key"