diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dcc6c8c4..4d0f5c2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v1.6.6](https://github.com/containous/traefik/tree/v1.6.6) (2018-08-20) +[All Commits](https://github.com/containous/traefik/compare/v1.6.5...v1.6.6) + +**Bug fixes:** +- **[acme]** Avoid duplicated ACME resolution ([#3751](https://github.com/containous/traefik/pull/3751) by [nmengin](https://github.com/nmengin)) +- **[api]** Remove TLS in API ([#3788](https://github.com/containous/traefik/pull/3788) by [Juliens](https://github.com/Juliens)) +- **[cluster]** Remove unusable `--cluster` flag ([#3616](https://github.com/containous/traefik/pull/3616) by [dtomcej](https://github.com/dtomcej)) +- **[ecs]** Fix bad condition in ECS provider ([#3609](https://github.com/containous/traefik/pull/3609) by [mmatur](https://github.com/mmatur)) +- Set keepalive on TCP socket so idleTimeout works ([#3740](https://github.com/containous/traefik/pull/3740) by [ajardan](https://github.com/ajardan)) + +**Documentation:** +- A tiny rewording on the documentation API's page ([#3794](https://github.com/containous/traefik/pull/3794) by [dduportal](https://github.com/dduportal)) +- Adding warnings and solution about the configuration exposure ([#3790](https://github.com/containous/traefik/pull/3790) by [dduportal](https://github.com/dduportal)) +- Fix path to the debug pprof API ([#3608](https://github.com/containous/traefik/pull/3608) by [multani](https://github.com/multani)) + +**Misc:** +- **[oxy,websocket]** Update oxy dependency ([#3777](https://github.com/containous/traefik/pull/3777) by [Juliens](https://github.com/Juliens)) + ## [v1.7.0-rc3](https://github.com/containous/traefik/tree/v1.7.0-rc3) (2018-08-01) [All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc2...v1.7.0-rc3) diff --git a/Gopkg.lock b/Gopkg.lock index 275659522..8c7af184a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1272,7 +1272,7 @@ "roundrobin", "utils" ] - revision = "fb889e801a26e7e18ef36322ac72a07157f8cc1f" + revision = "885e42fe04d8e0efa6c18facad4e0fc5757cde9b" [[projects]] name = "github.com/vulcand/predicate" @@ -1762,6 +1762,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "2b7ffb1d01d8a14224fcc9964900fb5a39fbf38cfacba45f49b931136e4fee9b" + inputs-digest = "b75bf0ae5b8c1ae1ba578fe5a58dfc4cd4270e02f5ea3b9f0d5a92972a36e9b2" solver-name = "gps-cdcl" solver-version = 1 diff --git a/acme/acme.go b/acme/acme.go index 3a7317562..d9c90137f 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -12,6 +12,7 @@ import ( "net/url" "reflect" "strings" + "sync" "time" "github.com/BurntSushi/ty/fun" @@ -64,6 +65,8 @@ type ACME struct { jobs *channels.InfiniteChannel TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` dynamicCerts *safe.Safe + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } func (a *ACME) init() error { @@ -76,6 +79,10 @@ func (a *ACME) init() error { } a.jobs = channels.NewInfiniteChannel() + + // Init the currently resolved domain map + a.resolvingDomains = make(map[string]struct{}) + return nil } @@ -537,6 +544,10 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { if len(uncheckedDomains) == 0 { return } + + a.addResolvingDomains(uncheckedDomains) + defer a.removeResolvingDomains(uncheckedDomains) + certificate, err := a.getDomainsCertificates(uncheckedDomains) if err != nil { log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err) @@ -568,6 +579,24 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } } +func (a *ACME) addResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + a.resolvingDomains[domain] = struct{}{} + } +} + +func (a *ACME) removeResolvingDomains(resolvingDomains []string) { + a.resolvingDomainsMutex.Lock() + defer a.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(a.resolvingDomains, domain) + } +} + // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { @@ -603,6 +632,9 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string { + a.resolvingDomainsMutex.RLock() + defer a.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate to validate %s...", domains) allCerts := make(map[string]*tls.Certificate) @@ -625,6 +657,13 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string } } + // Get currently resolved domains + for domain := range a.resolvingDomains { + if _, ok := allCerts[domain]; !ok { + allCerts[domain] = &tls.Certificate{} + } + } + // Get Configuration Domains for i := 0; i < len(a.Domains); i++ { allCerts[a.Domains[i].Main] = &tls.Certificate{} diff --git a/acme/acme_test.go b/acme/acme_test.go index 9e3d2ace4..aadfa17b6 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -331,9 +331,12 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { mm["*.containo.us"] = &tls.Certificate{} mm["traefik.acme.io"] = &tls.Certificate{} - a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} + dm := make(map[string]struct{}) + dm["*.traefik.wtf"] = struct{}{} - domains := []string{"traefik.containo.us", "trae.containo.us"} + a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}, resolvingDomains: dm} + + domains := []string{"traefik.containo.us", "trae.containo.us", "foo.traefik.wtf"} uncheckedDomains := a.getUncheckedDomains(domains, nil) assert.Empty(t, uncheckedDomains) domains = []string{"traefik.acme.io", "trae.acme.io"} @@ -351,6 +354,9 @@ func TestAcme_getUncheckedCertificates(t *testing.T) { account := Account{DomainsCertificate: domainsCertificates} uncheckedDomains = a.getUncheckedDomains(domains, &account) assert.Empty(t, uncheckedDomains) + domains = []string{"traefik.containo.us", "trae.containo.us", "traefik.wtf"} + uncheckedDomains = a.getUncheckedDomains(domains, nil) + assert.Len(t, uncheckedDomains, 1) } func TestAcme_getProvidedCertificate(t *testing.T) { diff --git a/docs/configuration/api.md b/docs/configuration/api.md index eda514c98..215e2ce7c 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -4,6 +4,9 @@ ```toml # API definition +# Warning: Enabling API will expose Træfik's configuration. +# It is not recommended in production, +# unless secured by authentication and authorizations [api] # Name of the related entry point # @@ -12,7 +15,7 @@ # entryPoint = "traefik" - # Enabled Dashboard + # Enable Dashboard # # Optional # Default: true @@ -38,6 +41,22 @@ For more customization, see [entry points](/configuration/entrypoints/) document ![Web UI Health](/img/traefik-health.png) +## Security + +Enabling the API will expose all configuration elements, +including sensitive data. + +It is not recommended in production, +unless secured by authentication and authorizations. + +A good sane default (but not exhaustive) set of recommendations +would be to apply the following protection mechanism: + +* _At application level:_ enabling HTTP [Basic Authentication](#authentication) +* _At transport level:_ NOT exposing publicly the API's port, +keeping it restricted over internal networks +(restricted networks as in https://en.wikipedia.org/wiki/Principle_of_least_privilege). + ## API | Path | Method | Description | diff --git a/docs/index.md b/docs/index.md index 2afd6b199..f6d0c5a23 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,6 +86,10 @@ services: - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events ``` +!!! warning + Enabling the Web UI with the `--api` flag might exposes configuration elements. You can read more about this on the [API/Dashboard's Security section](/configuration/api#security). + + **That's it. Now you can launch Træfik!** Start your `reverse-proxy` with the following command: @@ -199,3 +203,8 @@ Using the tiny Docker image: ```shell docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik ``` + +## Security + +We want to keep Træfik safe for everyone. +If you've discovered a security vulnerability in Træfik, we appreciate your help in disclosing it to us in a responsible manner, using [this form](https://security.traefik.io). \ No newline at end of file diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 6efcec069..9c3c95b49 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -8,7 +8,7 @@ In addition, we want to use Let's Encrypt to automatically generate and renew SS ## Setting Up -In order for this to work, you'll need a server with a public IP address, with Docker installed on it. +In order for this to work, you'll need a server with a public IP address, with Docker and docker-compose installed on it. In this example, we're using the fictitious domain _my-awesome-app.org_. diff --git a/examples/acme/manage_acme_docker_environment.sh b/examples/acme/manage_acme_docker_environment.sh index e007665b5..6200d041d 100755 --- a/examples/acme/manage_acme_docker_environment.sh +++ b/examples/acme/manage_acme_docker_environment.sh @@ -50,7 +50,7 @@ start_boulder() { # Script usage show_usage() { echo - echo "USAGE : manage_acme_docker_environment.sh [--start|--stop|--restart]" + echo "USAGE : manage_acme_docker_environment.sh [--dev|--start|--stop|--restart]" echo } diff --git a/mkdocs.yml b/mkdocs.yml index 3ac8fd161..bca8ed40e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,14 +16,11 @@ theme: include_sidebar: true favicon: img/traefik.icon.png logo: img/traefik.logo.png - palette: - primary: 'blue' - accent: 'light blue' - feature: - tabs: false palette: primary: 'cyan' accent: 'cyan' + feature: + tabs: false i18n: prev: 'Previous' next: 'Next' diff --git a/provider/acme/provider.go b/provider/acme/provider.go index 95b816b24..8ff0a1c2c 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -62,6 +62,8 @@ type Provider struct { clientMutex sync.Mutex configFromListenerChan chan types.Configuration pool *safe.Pool + resolvingDomains map[string]struct{} + resolvingDomainsMutex sync.RWMutex } // Certificate is a struct which contains all data needed from an ACME certificate @@ -144,6 +146,9 @@ func (p *Provider) Init(_ types.Constraints) error { return fmt.Errorf("unable to get ACME certificates : %v", err) } + // Init the currently resolved domain map + p.resolvingDomains = make(map[string]struct{}) + return nil } @@ -373,6 +378,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return nil, nil } + p.addResolvingDomains(uncheckedDomains) + defer p.removeResolvingDomains(uncheckedDomains) + log.Debugf("Loading ACME certificates %+v...", uncheckedDomains) client, err := p.getClient() @@ -410,6 +418,24 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return certificate, nil } +func (p *Provider) removeResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + delete(p.resolvingDomains, domain) + } +} + +func (p *Provider) addResolvingDomains(resolvingDomains []string) { + p.resolvingDomainsMutex.Lock() + defer p.resolvingDomainsMutex.Unlock() + + for _, domain := range resolvingDomains { + p.resolvingDomains[domain] = struct{}{} + } +} + func (p *Provider) useCertificateWithRetry(domains []string) bool { // Check if we can use the retry mechanism only if we use the DNS Challenge and if is there are at least 2 domains to check if p.DNSChallenge != nil && len(domains) > 1 { @@ -636,6 +662,9 @@ func (p *Provider) renewCertificates() { // Get provided certificate which check a domains list (Main and SANs) // from static and dynamic provided certificates func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string { + p.resolvingDomainsMutex.RLock() + defer p.resolvingDomainsMutex.RUnlock() + log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck) allDomains := p.certificateStore.GetAllDomains() @@ -645,6 +674,11 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati allDomains = append(allDomains, strings.Join(certificate.Domain.ToStrArray(), ",")) } + // Get currently resolved domains + for domain := range p.resolvingDomains { + allDomains = append(allDomains, domain) + } + // Get Configuration Domains if checkConfigurationDomains { for i := 0; i < len(p.Domains); i++ { @@ -664,7 +698,7 @@ func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) [ } if len(uncheckedDomains) == 0 { - log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck) + log.Debugf("No ACME certificate generation required for domains %q.", domainsToCheck) } else { log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ",")) } diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go index e2c948e36..b5287ba4e 100644 --- a/provider/acme/provider_test.go +++ b/provider/acme/provider_test.go @@ -28,6 +28,7 @@ func TestGetUncheckedCertificates(t *testing.T) { desc string dynamicCerts *safe.Safe staticCerts *safe.Safe + resolvingDomains map[string]struct{} acmeCertificates []*Certificate domains []string expectedDomains []string @@ -140,6 +141,40 @@ func TestGetUncheckedCertificates(t *testing.T) { }, expectedDomains: []string{"traefik.wtf"}, }, + { + desc: "all domains already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + "foo.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "one domain already managed by ACME", + domains: []string{"traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "traefik.wtf": {}, + }, + expectedDomains: []string{"foo.traefik.wtf"}, + }, + { + desc: "wildcard domain already managed by ACME checks the domains", + domains: []string{"bar.traefik.wtf", "foo.traefik.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + }, + expectedDomains: []string{}, + }, + { + desc: "wildcard domain already managed by ACME checks domains and another domain checks one other domain, one domain still unchecked", + domains: []string{"traefik.wtf", "bar.traefik.wtf", "foo.traefik.wtf", "acme.wtf"}, + resolvingDomains: map[string]struct{}{ + "*.traefik.wtf": {}, + "traefik.wtf": {}, + }, + expectedDomains: []string{"acme.wtf"}, + }, } for _, test := range testCases { @@ -147,12 +182,17 @@ func TestGetUncheckedCertificates(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + if test.resolvingDomains == nil { + test.resolvingDomains = make(map[string]struct{}) + } + acmeProvider := Provider{ certificateStore: &traefiktls.CertificateStore{ DynamicCerts: test.dynamicCerts, StaticCerts: test.staticCerts, }, - certificates: test.acmeCertificates, + certificates: test.acmeCertificates, + resolvingDomains: test.resolvingDomains, } domains := acmeProvider.getUncheckedDomains(test.domains, false) diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index cd057f59c..3a715e479 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -4,9 +4,11 @@ package forward import ( + "bytes" "crypto/tls" "errors" "fmt" + "io" "net" "net/http" "net/http/httptest" @@ -309,11 +311,6 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { outReq.URL.RawQuery = u.RawQuery outReq.RequestURI = "" // Outgoing request should not have RequestURI - // Do not pass client Host header unless optsetter PassHostHeader is set. - if !f.passHost { - outReq.Host = target.Host - } - outReq.Proto = "HTTP/1.1" outReq.ProtoMajor = 1 outReq.ProtoMinor = 1 @@ -321,6 +318,11 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) { if f.rewriter != nil { f.rewriter.Rewrite(outReq) } + + // Do not pass client Host header unless optsetter PassHostHeader is set. + if !f.passHost { + outReq.Host = target.Host + } } // serveHTTP forwards websocket traffic @@ -396,16 +398,28 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, errBackend := make(chan error, 1) replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + forward := func(messageType int, reader io.Reader) error { + writer, err := dst.NextWriter(messageType) + if err != nil { + return err + } + _, err = io.Copy(writer, reader) + if err != nil { + return err + } + return writer.Close() + } + src.SetPingHandler(func(data string) error { - return dst.WriteMessage(websocket.PingMessage, []byte(data)) + return forward(websocket.PingMessage, bytes.NewReader([]byte(data))) }) src.SetPongHandler(func(data string) error { - return dst.WriteMessage(websocket.PongMessage, []byte(data)) + return forward(websocket.PongMessage, bytes.NewReader([]byte(data))) }) for { - msgType, msg, err := src.ReadMessage() + msgType, reader, err := src.NextReader() if err != nil { m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) @@ -423,11 +437,11 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, } errc <- err if m != nil { - dst.WriteMessage(websocket.CloseMessage, m) + forward(websocket.CloseMessage, bytes.NewReader([]byte(m))) } break } - err = dst.WriteMessage(msgType, msg) + err = forward(msgType, reader) if err != nil { errc <- err break diff --git a/vendor/github.com/vulcand/oxy/utils/handler.go b/vendor/github.com/vulcand/oxy/utils/handler.go index 22d9c6900..24b9e3a88 100644 --- a/vendor/github.com/vulcand/oxy/utils/handler.go +++ b/vendor/github.com/vulcand/oxy/utils/handler.go @@ -1,6 +1,7 @@ package utils import ( + "context" "io" "net" "net/http" @@ -8,6 +9,12 @@ import ( log "github.com/sirupsen/logrus" ) +// StatusClientClosedRequest non-standard HTTP status code for client disconnection +const StatusClientClosedRequest = 499 + +// StatusClientClosedRequestText non-standard HTTP status for client disconnection +const StatusClientClosedRequestText = "Client Closed Request" + // ErrorHandler error handler type ErrorHandler interface { ServeHTTP(w http.ResponseWriter, req *http.Request, err error) @@ -21,6 +28,7 @@ type StdHandler struct{} func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { statusCode := http.StatusInternalServerError + if e, ok := err.(net.Error); ok { if e.Timeout() { statusCode = http.StatusGatewayTimeout @@ -29,10 +37,20 @@ func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err err } } else if err == io.EOF { statusCode = http.StatusBadGateway + } else if err == context.Canceled { + statusCode = StatusClientClosedRequest } + w.WriteHeader(statusCode) - w.Write([]byte(http.StatusText(statusCode))) - log.Debugf("'%d %s' caused by: %v", statusCode, http.StatusText(statusCode), err) + w.Write([]byte(statusText(statusCode))) + log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err) +} + +func statusText(statusCode int) string { + if statusCode == StatusClientClosedRequest { + return StatusClientClosedRequestText + } + return http.StatusText(statusCode) } // ErrorHandlerFunc error handler function type