From 1feeeb2eec547528b5cdc44be50d726bbd377ead Mon Sep 17 00:00:00 2001 From: lishaoxiong Date: Thu, 23 Nov 2017 18:50:03 +0800 Subject: [PATCH] Manage certificates dynamically in kv store --- autogen/gentemplates/gen.go | 13 ++ docs/basics.md | 1 + docs/user-guide/kv-config.md | 36 +++- examples/consul-config.sh | 11 ++ examples/etcd-config.sh | 19 ++ integration/consul_test.go | 156 ++++++++++++++++ integration/etcd3_test.go | 150 +++++++++++++++ integration/etcd_test.go | 174 +++++++++++++++++- integration/fixtures/consul/simple_https.toml | 20 ++ integration/fixtures/etcd/simple_https.toml | 20 ++ templates/kv.tmpl | 13 ++ 11 files changed, 604 insertions(+), 9 deletions(-) create mode 100644 integration/fixtures/consul/simple_https.toml create mode 100644 integration/fixtures/etcd/simple_https.toml diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index da1de84a7..d56fe3e39 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -445,6 +445,7 @@ func templatesKubernetesTmpl() (*asset, error) { var _templatesKvTmpl = []byte(`{{$frontends := List .Prefix "/frontends/" }} {{$backends := List .Prefix "/backends/"}} +{{$tlsconfiguration := List .Prefix "/tlsconfiguration/"}} [backends]{{range $backends}} {{$backend := .}} @@ -508,6 +509,18 @@ var _templatesKvTmpl = []byte(`{{$frontends := List .Prefix "/frontends/" }} rule = "{{Get "" . "/rule"}}" {{end}} {{end}} + +{{range $tlsconfiguration}} +{{$entryPoints := SplitGet . "/entrypoints"}} +[[tlsConfiguration]] + entryPoints = [{{range $entryPoints}} + "{{.}}", + {{end}}] + [tlsConfiguration.certificate] + certFile = """{{Get "" . "/certificate" "/certfile"}}""" + keyFile = """{{Get "" . "/certificate" "/keyfile"}}""" +{{end}} + `) func templatesKvTmplBytes() ([]byte, error) { diff --git a/docs/basics.md b/docs/basics.md index 95beac7f1..1c9dde913 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -546,6 +546,7 @@ The dynamic configuration concerns : - [Frontends](/basics/#frontends) - [Backends](/basics/#backends) - [Servers](/basics/#servers) +- HTTPS Certificates Træfik can hot-reload those rules which could be provided by [multiple configuration backends](/configuration/commons). diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index 637f795b2..dc1542a74 100644 --- a/docs/user-guide/kv-config.md +++ b/docs/user-guide/kv-config.md @@ -85,6 +85,9 @@ defaultEntryPoints = ["http", "https"] keyFile = """-----BEGIN CERTIFICATE----- -----END CERTIFICATE-----""" + [entryPoints.other-https] + address = ":4443" + [entryPoints.other-https.tls] [consul] endpoint = "127.0.0.1:8500" @@ -108,6 +111,7 @@ And there, the same global configuration in the Key-value Store (using `prefix = | `/traefik/entrypoints/https/tls/certificates/0/keyfile` | `integration/fixtures/https/snitest.com.key` | | `/traefik/entrypoints/https/tls/certificates/1/certfile` | `--BEGIN CERTIFICATE----END CERTIFICATE--` | | `/traefik/entrypoints/https/tls/certificates/1/keyfile` | `--BEGIN CERTIFICATE----END CERTIFICATE--` | +| `/traefik/entrypoints/other-https/address` | `:4443` | `/traefik/consul/endpoint` | `127.0.0.1:8500` | | `/traefik/consul/watch` | `true` | | `/traefik/consul/prefix` | `traefik` | @@ -212,7 +216,7 @@ Remember the command `traefik --help` to display the updated list of flags. ## Dynamic configuration in Key-value store -Following our example, we will provide backends/frontends rules to Træfik. +Following our example, we will provide backends/frontends rules and HTTPS certificates to Træfik. !!! note This section is independent of the way Træfik got its static configuration. @@ -265,6 +269,21 @@ Here is the toml configuration we would like to store in the store : entrypoints = ["http", "https"] # overrides defaultEntryPoints backend = "backend2" rule = "Path:/test" + +[[tlsConfiguration]] +entryPoints = ["https"] + [tlsConfiguration.certificate] + certFile = "path/to/your.cert" + keyFile = "path/to/your.key" +[[tlsConfiguration]] +entryPoints = ["https","other-https"] + [tlsConfiguration.certificate] + certFile = """-----BEGIN CERTIFICATE----- + + -----END CERTIFICATE-----""" + keyFile = """-----BEGIN CERTIFICATE----- + + -----END CERTIFICATE-----""" ``` And there, the same dynamic configuration in a KV Store (using `prefix = "traefik"`): @@ -310,6 +329,21 @@ And there, the same dynamic configuration in a KV Store (using `prefix = "traefi | `/traefik/frontends/frontend2/entrypoints` | `http,https` | | `/traefik/frontends/frontend2/routes/test_2/rule` | `PathPrefix:/test` | +- certificate 1 + +| Key | Value | +|----------------------------------------------------|--------------------| +| `/traefik/tlsconfiguration/1/entrypoints` | `https` | +| `/traefik/tlsconfiguration/1/certificate/certfile` | `path/to/your.cert`| +| `/traefik/tlsconfiguration/1/certificate/keyfile` | `path/to/your.key` | + +- certificate 2 + +| Key | Value | +|----------------------------------------------------|-----------------------| +| `/traefik/tlsconfiguration/2/entrypoints` | `https,other-https` | +| `/traefik/tlsconfiguration/2/certificate/certfile` | `` | +| `/traefik/tlsconfiguration/2/certificate/certfile` | `` | ### Atomic configuration changes Træfik can watch the backends/frontends configuration changes and generate its configuration automatically. diff --git a/examples/consul-config.sh b/examples/consul-config.sh index e86c68201..94509e710 100755 --- a/examples/consul-config.sh +++ b/examples/consul-config.sh @@ -23,3 +23,14 @@ curl -i -H "Accept: application/json" -X PUT -d "Host:test.localhost" ht curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints curl -i -H "Accept: application/json" -X PUT -d "Path:/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule + + +# certificate 1 +curl -i -H "Accept: application/json" -X PUT -d "https" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair1/entrypoints +curl -i -H "Accept: application/json" -X PUT -d "/tmp/test1.crt" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair1/certificate/certfile +curl -i -H "Accept: application/json" -X PUT -d "/tmp/test1.key" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair1/certificate/keyfile + +# certificate 2 +curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair2/entrypoints +curl -i -H "Accept: application/json" -X PUT -d "/tmp/test2.crt" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair2/certificate/certfile +curl -i -H "Accept: application/json" -X PUT -d "/tmp/test2.key" http://localhost:8500/v1/kv/traefik/tlsconfiguration/pair2/certificate/keyfile diff --git a/examples/etcd-config.sh b/examples/etcd-config.sh index c5cd7f860..6a5279957 100755 --- a/examples/etcd-config.sh +++ b/examples/etcd-config.sh @@ -27,6 +27,15 @@ function insert_etcd2_data() { curl -i -H "Accept: application/json" -X PUT -d value="http" http://localhost:2379/v2/keys/traefik/frontends/frontend2/entrypoints curl -i -H "Accept: application/json" -X PUT -d value="Path:/test" http://localhost:2379/v2/keys/traefik/frontends/frontend2/routes/test_2/rule + # certificate 1 + curl -i -H "Accept: application/json" -X PUT -d value="https" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair1/entrypoints + curl -i -H "Accept: application/json" -X PUT -d value="/tmp/test1.crt" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair1/certificate/certfile + curl -i -H "Accept: application/json" -X PUT -d value="/tmp/test1.key" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair1/certificate/keyfile + + # certificate 2 + curl -i -H "Accept: application/json" -X PUT -d value="http,https" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair2/entrypoints + curl -i -H "Accept: application/json" -X PUT -d value="/tmp/test2.crt" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair2/certificate/certfile + curl -i -H "Accept: application/json" -X PUT -d value="/tmp/test2.key" http://localhost:2379/v2/keys/traefik/tlsconfiguration/pair2/certificate/keyfile } # @@ -60,6 +69,16 @@ function insert_etcd3_data() { docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/frontends/frontend2/backend" "backend1" docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/frontends/frontend2/entrypoints" "http" docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/frontends/frontend2/routes/test_2/rule" "Path:/test" + + # certificate 1 + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair1/entrypoints" "https" + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair1/certificate/certfile" "/tmp/test1.crt" + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair1/certificate/keyfile" "/tmp/test1.key" + + # certificate 2 + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair2/entrypoints" "https" + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair2/certificate/certfile" "/tmp/test2.crt" + docker container run --rm -ti -e ETCDCTL_DIAL_="TIMEOUT 10s" -e ETCDCTL_API="3" tenstartups/etcdctl --endpoints=[$etcd_ip:2379] put "/traefik/tlsconfiguration/pair2/certificate/keyfile" "/tmp/test2.key" } function show_usage() { diff --git a/integration/consul_test.go b/integration/consul_test.go index 27b26e149..86fbadaab 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -2,7 +2,9 @@ package integration import ( "context" + "crypto/tls" "fmt" + "io/ioutil" "net/http" "os" "sync" @@ -502,3 +504,157 @@ func datastoreContains(datastore *cluster.Datastore, expectedValue string) func( return nil } } + +func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) { + s.setupConsul(c) + consulHost := s.composeProject.Container(c, "consul").NetworkSettings.IPAddress + // start Træfik + file := s.adaptFile(c, "fixtures/consul/simple_https.toml", struct{ ConsulHost string }{consulHost}) + defer os.Remove(file) + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // prepare to config + whoami1IP := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress + whoami2IP := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress + whoami3IP := s.composeProject.Container(c, "whoami3").NetworkSettings.IPAddress + whoami4IP := s.composeProject.Container(c, "whoami4").NetworkSettings.IPAddress + + snitestComCert, err := ioutil.ReadFile("fixtures/https/snitest.com.cert") + c.Assert(err, checker.IsNil) + snitestComKey, err := ioutil.ReadFile("fixtures/https/snitest.com.key") + c.Assert(err, checker.IsNil) + snitestOrgCert, err := ioutil.ReadFile("fixtures/https/snitest.org.cert") + c.Assert(err, checker.IsNil) + snitestOrgKey, err := ioutil.ReadFile("fixtures/https/snitest.org.key") + c.Assert(err, checker.IsNil) + + backend1 := map[string]string{ + "traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5", + "traefik/backends/backend1/servers/server1/url": "http://" + whoami1IP + ":80", + "traefik/backends/backend1/servers/server1/weight": "1", + "traefik/backends/backend1/servers/server2/url": "http://" + whoami2IP + ":80", + "traefik/backends/backend1/servers/server2/weight": "1", + } + backend2 := map[string]string{ + "traefik/backends/backend2/loadbalancer/method": "drr", + "traefik/backends/backend2/servers/server1/url": "http://" + whoami3IP + ":80", + "traefik/backends/backend2/servers/server1/weight": "1", + "traefik/backends/backend2/servers/server2/url": "http://" + whoami4IP + ":80", + "traefik/backends/backend2/servers/server2/weight": "1", + } + frontend1 := map[string]string{ + "traefik/frontends/frontend1/backend": "backend2", + "traefik/frontends/frontend1/entrypoints": "https", + "traefik/frontends/frontend1/priority": "1", + "traefik/frontends/frontend1/routes/test_1/rule": "Host:snitest.com", + } + + frontend2 := map[string]string{ + "traefik/frontends/frontend2/backend": "backend1", + "traefik/frontends/frontend2/entrypoints": "https", + "traefik/frontends/frontend2/priority": "10", + "traefik/frontends/frontend2/routes/test_2/rule": "Host:snitest.org", + } + + tlsconfigure1 := map[string]string{ + "traefik/tlsconfiguration/snitestcom/entrypoints": "https", + "traefik/tlsconfiguration/snitestcom/certificate/keyfile": string(snitestComKey), + "traefik/tlsconfiguration/snitestcom/certificate/certfile": string(snitestComCert), + } + + tlsconfigure2 := map[string]string{ + "traefik/tlsconfiguration/snitestorg/entrypoints": "https", + "traefik/tlsconfiguration/snitestorg/certificate/keyfile": string(snitestOrgKey), + "traefik/tlsconfiguration/snitestorg/certificate/certfile": string(snitestOrgCert), + } + + // config backends,frontends and first tls keypair + for key, value := range backend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range backend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range tlsconfigure1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + // wait for consul + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("traefik/tlsconfiguration/snitestcom/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG")) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client := &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + var resp *http.Response + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.com") + + // now we configure the second keypair in consul and the request for host "snitest.org" will use the second keypair + for key, value := range tlsconfigure2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + // wait for consul + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("traefik/tlsconfiguration/snitestorg/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + // waiting for traefik to pull configuration + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client = &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + resp, err = client.Do(req) + cn = resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.org") +} diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index be70bdc3d..48645ac4a 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -426,3 +426,153 @@ func (s *Etcd3Suite) TestCommandStoreConfig(c *check.C) { c.Assert(string(p.Value), checker.Equals, value) } } + +func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { + // start Træfik + cmd, display := s.traefikCmd( + withConfigFile("fixtures/etcd/simple_https.toml"), + "--etcd", + "--etcd.endpoint="+ipEtcd+":4001", + "--etcd.useAPIV3=true") + defer display(c) + + snitestComCert, err := ioutil.ReadFile("fixtures/https/snitest.com.cert") + c.Assert(err, checker.IsNil) + snitestComKey, err := ioutil.ReadFile("fixtures/https/snitest.com.key") + c.Assert(err, checker.IsNil) + snitestOrgCert, err := ioutil.ReadFile("fixtures/https/snitest.org.cert") + c.Assert(err, checker.IsNil) + snitestOrgKey, err := ioutil.ReadFile("fixtures/https/snitest.org.key") + c.Assert(err, checker.IsNil) + + backend1 := map[string]string{ + "/traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5", + "/traefik/backends/backend1/servers/server1/url": "http://" + ipWhoami01 + ":80", + "/traefik/backends/backend1/servers/server1/weight": "10", + "/traefik/backends/backend1/servers/server2/url": "http://" + ipWhoami02 + ":80", + "/traefik/backends/backend1/servers/server2/weight": "1", + } + backend2 := map[string]string{ + "/traefik/backends/backend2/loadbalancer/method": "drr", + "/traefik/backends/backend2/servers/server1/url": "http://" + ipWhoami03 + ":80", + "/traefik/backends/backend2/servers/server1/weight": "1", + "/traefik/backends/backend2/servers/server2/url": "http://" + ipWhoami04 + ":80", + "/traefik/backends/backend2/servers/server2/weight": "2", + } + frontend1 := map[string]string{ + "/traefik/frontends/frontend1/backend": "backend2", + "/traefik/frontends/frontend1/entrypoints": "https", + "/traefik/frontends/frontend1/priority": "1", + "/traefik/frontends/frontend1/routes/test_1/rule": "Host:snitest.com", + } + + frontend2 := map[string]string{ + "/traefik/frontends/frontend2/backend": "backend1", + "/traefik/frontends/frontend2/entrypoints": "https", + "/traefik/frontends/frontend2/priority": "10", + "/traefik/frontends/frontend2/routes/test_2/rule": "Host:snitest.org", + } + + tlsconfigure1 := map[string]string{ + "/traefik/tlsconfiguration/snitestcom/entrypoints": "https", + "/traefik/tlsconfiguration/snitestcom/certificate/keyfile": string(snitestComKey), + "/traefik/tlsconfiguration/snitestcom/certificate/certfile": string(snitestComCert), + } + + tlsconfigure2 := map[string]string{ + "/traefik/tlsconfiguration/snitestorg/entrypoints": "https", + "/traefik/tlsconfiguration/snitestorg/certificate/keyfile": string(snitestOrgKey), + "/traefik/tlsconfiguration/snitestorg/certificate/certfile": string(snitestOrgCert), + } + + // config backends,frontends and first tls keypair + for key, value := range backend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range backend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range tlsconfigure1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + // wait for etcd + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("/traefik/tlsconfiguration/snitestcom/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + err = cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Træfik + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client := &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + var resp *http.Response + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.com") + + // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair + + for key, value := range tlsconfigure2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + // wait for etcd + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("/traefik/tlsconfiguration/snitestorg/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + // waiting for Træfik to pull configuration + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client = &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + resp, err = client.Do(req) + cn = resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.org") +} diff --git a/integration/etcd_test.go b/integration/etcd_test.go index 000ca355e..29cc02498 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -155,7 +155,7 @@ func (s *EtcdSuite) TestNominalConfiguration(c *check.C) { }) c.Assert(err, checker.IsNil) - // wait for traefik + // wait for Træfik err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains("Path:/test")) c.Assert(err, checker.IsNil) @@ -213,7 +213,7 @@ func (s *EtcdSuite) TestGlobalConfiguration(c *check.C) { }) c.Assert(err, checker.IsNil) - // start traefik + // start Træfik cmd, display := s.traefikCmd( withConfigFile("fixtures/simple_web.toml"), "--etcd", @@ -282,7 +282,7 @@ func (s *EtcdSuite) TestGlobalConfiguration(c *check.C) { err = try.GetRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, try.BodyContains("Path:/test")) c.Assert(err, checker.IsNil) - //check + // check req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8001/", nil) c.Assert(err, checker.IsNil) req.Host = "test.localhost" @@ -293,7 +293,7 @@ func (s *EtcdSuite) TestGlobalConfiguration(c *check.C) { func (s *EtcdSuite) TestCertificatesContentstWithSNIConfigHandshake(c *check.C) { etcdHost := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress - // start traefik + // start Træfik cmd, display := s.traefikCmd( withConfigFile("fixtures/simple_web.toml"), "--etcd", @@ -305,7 +305,7 @@ func (s *EtcdSuite) TestCertificatesContentstWithSNIConfigHandshake(c *check.C) whoami3IP := s.composeProject.Container(c, "whoami3").NetworkSettings.IPAddress whoami4IP := s.composeProject.Container(c, "whoami4").NetworkSettings.IPAddress - //Copy the contents of the certificate files into ETCD + // Copy the contents of the certificate files into ETCD snitestComCert, err := ioutil.ReadFile("fixtures/https/snitest.com.cert") c.Assert(err, checker.IsNil) snitestComKey, err := ioutil.ReadFile("fixtures/https/snitest.com.key") @@ -383,7 +383,7 @@ func (s *EtcdSuite) TestCertificatesContentstWithSNIConfigHandshake(c *check.C) err = try.GetRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, try.BodyContains("Host:snitest.org")) c.Assert(err, checker.IsNil) - //check + // check tlsConfig := &tls.Config{ InsecureSkipVerify: true, ServerName: "snitest.com", @@ -411,10 +411,10 @@ func (s *EtcdSuite) TestCommandStoreConfig(c *check.C) { err := cmd.Start() c.Assert(err, checker.IsNil) - // wait for traefik finish without error + // wait for Træfik finish without error cmd.Wait() - //CHECK + // CHECK checkmap := map[string]string{ "/traefik/loglevel": "DEBUG", "/traefik/defaultentrypoints/0": "http", @@ -434,3 +434,161 @@ func (s *EtcdSuite) TestCommandStoreConfig(c *check.C) { c.Assert(string(p.Value), checker.Equals, value) } } + +func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) { + etcdHost := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress + // start Træfik + cmd, display := s.traefikCmd( + withConfigFile("fixtures/etcd/simple_https.toml"), + "--etcd", + "--etcd.endpoint="+etcdHost+":4001", + "--etcd.watch=true", + ) + defer display(c) + + // prepare to config + whoami1IP := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress + whoami2IP := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress + whoami3IP := s.composeProject.Container(c, "whoami3").NetworkSettings.IPAddress + whoami4IP := s.composeProject.Container(c, "whoami4").NetworkSettings.IPAddress + + snitestComCert, err := ioutil.ReadFile("fixtures/https/snitest.com.cert") + c.Assert(err, checker.IsNil) + snitestComKey, err := ioutil.ReadFile("fixtures/https/snitest.com.key") + c.Assert(err, checker.IsNil) + snitestOrgCert, err := ioutil.ReadFile("fixtures/https/snitest.org.cert") + c.Assert(err, checker.IsNil) + snitestOrgKey, err := ioutil.ReadFile("fixtures/https/snitest.org.key") + c.Assert(err, checker.IsNil) + + backend1 := map[string]string{ + "/traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5", + "/traefik/backends/backend1/servers/server1/url": "http://" + whoami1IP + ":80", + "/traefik/backends/backend1/servers/server1/weight": "1", + "/traefik/backends/backend1/servers/server2/url": "http://" + whoami2IP + ":80", + "/traefik/backends/backend1/servers/server2/weight": "1", + } + backend2 := map[string]string{ + "/traefik/backends/backend2/loadbalancer/method": "drr", + "/traefik/backends/backend2/servers/server1/url": "http://" + whoami3IP + ":80", + "/traefik/backends/backend2/servers/server1/weight": "1", + "/traefik/backends/backend2/servers/server2/url": "http://" + whoami4IP + ":80", + "/traefik/backends/backend2/servers/server2/weight": "1", + } + frontend1 := map[string]string{ + "/traefik/frontends/frontend1/backend": "backend2", + "/traefik/frontends/frontend1/entrypoints": "https", + "/traefik/frontends/frontend1/priority": "1", + "/traefik/frontends/frontend1/routes/test_1/rule": "Host:snitest.com", + } + + frontend2 := map[string]string{ + "/traefik/frontends/frontend2/backend": "backend1", + "/traefik/frontends/frontend2/entrypoints": "https", + "/traefik/frontends/frontend2/priority": "10", + "/traefik/frontends/frontend2/routes/test_2/rule": "Host:snitest.org", + } + + tlsconfigure1 := map[string]string{ + "/traefik/tlsconfiguration/snitestcom/entrypoints": "https", + "/traefik/tlsconfiguration/snitestcom/certificate/keyfile": string(snitestComKey), + "/traefik/tlsconfiguration/snitestcom/certificate/certfile": string(snitestComCert), + } + + tlsconfigure2 := map[string]string{ + "/traefik/tlsconfiguration/snitestorg/entrypoints": "https", + "/traefik/tlsconfiguration/snitestorg/certificate/keyfile": string(snitestOrgKey), + "/traefik/tlsconfiguration/snitestorg/certificate/certfile": string(snitestOrgCert), + } + + // config backends,frontends and first tls keypair + for key, value := range backend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range backend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range tlsconfigure1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + // wait for etcd + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("/traefik/tlsconfiguration/snitestcom/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + err = cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Træfik + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client := &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + var resp *http.Response + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.com") + + // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair + + for key, value := range tlsconfigure2 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + // wait for etcd + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("/traefik/tlsconfiguration/snitestorg/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + // waiting for Træfik to pull configuration + err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client = &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + resp, err = client.Do(req) + cn = resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.org") +} diff --git a/integration/fixtures/consul/simple_https.toml b/integration/fixtures/consul/simple_https.toml new file mode 100644 index 000000000..21326e66f --- /dev/null +++ b/integration/fixtures/consul/simple_https.toml @@ -0,0 +1,20 @@ +defaultEntryPoints = ["http","https"] + +logLevel = "DEBUG" + +[entryPoints] + [entryPoints.http] + address = ":8000" + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + + + +[consul] + endpoint = "{{.ConsulHost}}:8500" + prefix = "traefik" + watch = true + +[web] + address = ":8081" diff --git a/integration/fixtures/etcd/simple_https.toml b/integration/fixtures/etcd/simple_https.toml new file mode 100644 index 000000000..b5d93290d --- /dev/null +++ b/integration/fixtures/etcd/simple_https.toml @@ -0,0 +1,20 @@ +defaultEntryPoints = ["http","https"] + +logLevel = "DEBUG" + +[entryPoints] + [entryPoints.http] + address = ":8000" + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + + + +#[etcd] +# endpoint = "{{.EtcdHost}}:2379" +# prefix = "/traefik" +# watch = true + +[web] + address = ":8081" diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 291a14d38..09b04b683 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -1,5 +1,6 @@ {{$frontends := List .Prefix "/frontends/" }} {{$backends := List .Prefix "/backends/"}} +{{$tlsconfiguration := List .Prefix "/tlsconfiguration/"}} [backends]{{range $backends}} {{$backend := .}} @@ -63,3 +64,15 @@ rule = "{{Get "" . "/rule"}}" {{end}} {{end}} + +{{range $tlsconfiguration}} +{{$entryPoints := SplitGet . "/entrypoints"}} +[[tlsConfiguration]] + entryPoints = [{{range $entryPoints}} + "{{.}}", + {{end}}] + [tlsConfiguration.certificate] + certFile = """{{Get "" . "/certificate" "/certfile"}}""" + keyFile = """{{Get "" . "/certificate" "/keyfile"}}""" +{{end}} +