diff --git a/.gitignore b/.gitignore index 243850797..a74de8fb8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ *.log *.exe .DS_Store -/example/acme/acme.json +/examples/acme/acme.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2bead45..89b2844eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Change Log +## [v1.5.0-rc5](https://github.com/containous/traefik/tree/v1.5.0-rc5) (2018-01-15) +[All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc4...v1.5.0-rc5) + +**Enhancements:** +- **[acme]** Add Let's Encrypt HTTP Challenge ([#2701](https://github.com/containous/traefik/pull/2701) by [Juliens](https://github.com/Juliens)) + +**Bug fixes:** +- **[acme,logs]** Modify DEBUG messages to get ACME certificates ([#2685](https://github.com/containous/traefik/pull/2685) by [nmengin](https://github.com/nmengin)) +- **[authentication,middleware]** Fix concurrent map writes on digest auth ([#2695](https://github.com/containous/traefik/pull/2695) by [mmatur](https://github.com/mmatur)) +- **[docker]** Typo in Docker template. ([#2692](https://github.com/containous/traefik/pull/2692) by [ldez](https://github.com/ldez)) +- **[docker]** Return errors from Docker client.Events ([#2689](https://github.com/containous/traefik/pull/2689) by [BlakeMesdag](https://github.com/BlakeMesdag)) +- **[kv]** List entries parsing. ([#2669](https://github.com/containous/traefik/pull/2669) by [ldez](https://github.com/ldez)) +- **[metrics]** Fix data races. ([#2287](https://github.com/containous/traefik/pull/2287) by [tcolgate](https://github.com/tcolgate)) +- **[middleware]** GzipResponse must implement CloseNotifier if ResponseWriter implement it ([#2657](https://github.com/containous/traefik/pull/2657) by [Juliens](https://github.com/Juliens)) +- **[websocket]** Add compression and better error handling ([#2702](https://github.com/containous/traefik/pull/2702) by [Juliens](https://github.com/Juliens)) +- Fix: timeout integration test ([#2679](https://github.com/containous/traefik/pull/2679) by [ldez](https://github.com/ldez)) + +**Documentation:** +- **[cluster]** Add a clustering example with Docker Swarm ([#2589](https://github.com/containous/traefik/pull/2589) by [jmaitrehenry](https://github.com/jmaitrehenry)) +- **[k8s]** Apply various contentual and stylish improvements to the k8s docs. ([#2677](https://github.com/containous/traefik/pull/2677) by [timoreimann](https://github.com/timoreimann)) +- **[k8s]** Document rewrite-target annotation. ([#2676](https://github.com/containous/traefik/pull/2676) by [timoreimann](https://github.com/timoreimann)) +- **[provider,webui]** Fix redirect problem on dashboard + docs/tests on [web] ([#2686](https://github.com/containous/traefik/pull/2686) by [Juliens](https://github.com/Juliens)) + ## [v1.5.0-rc4](https://github.com/containous/traefik/tree/v1.5.0-rc4) (2018-01-04) [All Commits](https://github.com/containous/traefik/compare/v1.5.0-rc3...v1.5.0-rc4) diff --git a/Gopkg.lock b/Gopkg.lock index f3a87b0ce..456ac24a2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -125,10 +125,11 @@ version = "v1.0.0" [[projects]] + branch = "containous-fork" name = "github.com/abbot/go-http-auth" packages = ["."] - revision = "0ddd408d5d60ea76e320503cc7dd091992dee608" - version = "v0.4.0" + revision = "65b0cdae8d7fe5c05c7430e055938ef6d24a66c9" + source = "github.com/containous/go-http-auth" [[projects]] name = "github.com/aokoli/goutils" @@ -418,7 +419,7 @@ name = "github.com/docker/leadership" packages = ["."] revision = "af20da7d3e62be9259835e93261acf931b5adecf" - source = "https://github.com/containous/leadership.git" + source = "github.com/containous/leadership" [[projects]] name = "github.com/docker/libcompose" @@ -552,7 +553,7 @@ name = "github.com/go-check/check" packages = ["."] revision = "ca0bf163426aa183d03fd4949101785c0347f273" - source = "https://github.com/containous/check.git" + source = "github.com/containous/check" [[projects]] name = "github.com/go-ini/ini" @@ -1481,6 +1482,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "0f85e152f938507b05ee27ce99b5d2ffe7999d4c27123277a1c398deba0c08b1" + inputs-digest = "aa1aa76d15a49bbeee747782a71809fcfbaccf4586e924e70a6d93c51c173a30" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index b3c9a3e7d..cb5785c8e 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -38,8 +38,9 @@ ignored = ["github.com/sirupsen/logrus"] name = "github.com/NYTimes/gziphandler" [[constraint]] + branch = "containous-fork" name = "github.com/abbot/go-http-auth" - version = "0.4.0" + source = "github.com/containous/go-http-auth" [[constraint]] branch = "master" @@ -74,8 +75,9 @@ ignored = ["github.com/sirupsen/logrus"] version = "14.0.0" [[constraint]] + branch = "master" name = "github.com/docker/leadership" - source = "https://github.com/containous/leadership.git" + source = "github.com/containous/leadership" [[constraint]] name = "github.com/docker/libkv" @@ -92,7 +94,7 @@ ignored = ["github.com/sirupsen/logrus"] [[constraint]] branch = "fork-containous" name = "github.com/go-check/check" - source = "https://github.com/containous/check.git" + source = "github.com/containous/check" [[constraint]] name = "github.com/go-kit/kit" @@ -188,15 +190,15 @@ ignored = ["github.com/sirupsen/logrus"] revision = "6018b68f96b839edfbe3fb48668853f5dbad88a3" source = "github.com/ijc25/Gotty" +[[override]] + name = "github.com/gorilla/websocket" + revision = "a69d9f6de432e2c6b296a947d8a5ee88f68522cf" + [[override]] # ALWAYS keep this override name = "github.com/mailgun/timetools" revision = "7e6055773c5137efbeb3bd2410d705fe10ab6bfd" -[[override]] - name = "github.com/gorilla/websocket" - revision = "a69d9f6de432e2c6b296a947d8a5ee88f68522cf" - # Must be remove when logrus migration happen [[override]] name = "github.com/vulcand/predicate" diff --git a/acme/account.go b/acme/account.go index a9f2c5421..425f63489 100644 --- a/acme/account.go +++ b/acme/account.go @@ -24,6 +24,7 @@ type Account struct { PrivateKey []byte DomainsCertificate DomainsCertificates ChallengeCerts map[string]*ChallengeCert + HTTPChallenge map[string]map[string][]byte } // ChallengeCert stores a challenge certificate diff --git a/acme/acme.go b/acme/acme.go index edc15e7b1..0599dc864 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -7,6 +7,8 @@ import ( "fmt" "io/ioutil" fmtlog "log" + "net" + "net/http" "os" "regexp" "strings" @@ -14,6 +16,8 @@ import ( "github.com/BurntSushi/ty/fun" "github.com/cenk/backoff" + "github.com/containous/flaeg" + "github.com/containous/mux" "github.com/containous/staert" "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" @@ -33,25 +37,39 @@ var ( // ACME allows to connect to lets encrypt and retrieve certs type ACME struct { - Email string `description:"Email address used for registration"` - Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` - Storage string `description:"File or key used for certificates storage."` - StorageFile string // deprecated - OnDemand bool `description:"Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` - OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` - CAServer string `description:"CA server to use."` - EntryPoint string `description:"Entrypoint to proxy acme challenge to."` - DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."` - DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` - ACMELogging bool `description:"Enable debug logging of ACME actions."` - client *acme.Client - defaultCertificate *tls.Certificate - store cluster.Store - challengeProvider *challengeProvider - checkOnDemandDomain func(domain string) bool - jobs *channels.InfiniteChannel - TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` - dynamicCerts *safe.Safe + Email string `description:"Email address used for registration"` + Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"` + Storage string `description:"File or key used for certificates storage."` + StorageFile string // deprecated + OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` //deprecated + OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` + CAServer string `description:"CA server to use."` + EntryPoint string `description:"Entrypoint to proxy acme challenge to."` + DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"` + HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge"` + DNSProvider string `description:"Use a DNS-01 acme challenge rather than TLS-SNI-01 challenge."` // deprecated + DelayDontCheckDNS flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` // deprecated + ACMELogging bool `description:"Enable debug logging of ACME actions."` + client *acme.Client + defaultCertificate *tls.Certificate + store cluster.Store + challengeTLSProvider *challengeTLSProvider + challengeHTTPProvider *challengeHTTPProvider + checkOnDemandDomain func(domain string) bool + jobs *channels.InfiniteChannel + TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` + dynamicCerts *safe.Safe +} + +// DNSChallenge contains DNS challenge Configuration +type DNSChallenge struct { + Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` + DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` +} + +// HTTPChallenge contains HTTP challenge Configuration +type HTTPChallenge struct { + EntryPoint string `description:"HTTP challenge EntryPoint"` } //Domains parse []Domain @@ -107,15 +125,39 @@ func (a *ACME) init() error { return err } a.defaultCertificate = cert - // TODO: to remove in the future - if len(a.StorageFile) > 0 && len(a.Storage) == 0 { - log.Warn("ACME.StorageFile is deprecated, use ACME.Storage instead") - a.Storage = a.StorageFile - } + a.jobs = channels.NewInfiniteChannel() return nil } +// AddRoutes add routes on internal router +func (a *ACME) AddRoutes(router *mux.Router) { + router.Methods(http.MethodGet). + Path(acme.HTTP01ChallengePath("{token}")). + Handler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if a.challengeHTTPProvider == nil { + rw.WriteHeader(http.StatusNotFound) + return + } + + vars := mux.Vars(req) + if token, ok := vars["token"]; ok { + domain, _, err := net.SplitHostPort(req.Host) + if err != nil { + log.Debugf("Unable to split host and port: %v. Fallback to request host.", err) + domain = req.Host + } + tokenValue := a.challengeHTTPProvider.getTokenValue(token, domain) + if len(tokenValue) > 0 { + rw.WriteHeader(http.StatusOK) + rw.Write(tokenValue) + return + } + } + rw.WriteHeader(http.StatusNotFound) + })) +} + // CreateClusterConfig creates a tls.config using ACME configuration in cluster mode func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, certs *safe.Safe, checkOnDemandDomain func(domain string) bool) error { err := a.init() @@ -155,7 +197,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl } a.store = datastore - a.challengeProvider = &challengeProvider{store: a.store} + a.challengeTLSProvider = &challengeTLSProvider{store: a.store} ticker := time.NewTicker(24 * time.Hour) leadership.Pool.AddGoCtx(func(ctx context.Context) { @@ -171,69 +213,69 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl } }) - leadership.AddListener(func(elected bool) error { - if elected { - _, err := a.store.Load() + leadership.AddListener(a.leadershipListener) + return nil +} + +func (a *ACME) leadershipListener(elected bool) error { + if elected { + _, err := a.store.Load() + if err != nil { + return err + } + transaction, object, err := a.store.Begin() + if err != nil { + return err + } + account := object.(*Account) + account.Init() + var needRegister bool + if account == nil || len(account.Email) == 0 { + account, err = NewAccount(a.Email) if err != nil { return err } - transaction, object, err := a.store.Begin() + needRegister = true + } + a.client, err = a.buildACMEClient(account) + if err != nil { + return err + } + if needRegister { + // New users will need to register; be sure to save it + log.Debug("Register...") + reg, err := a.client.Register() if err != nil { return err } - account := object.(*Account) - account.Init() - var needRegister bool - if account == nil || len(account.Email) == 0 { - account, err = NewAccount(a.Email) - if err != nil { - return err - } - needRegister = true - } + account.Registration = reg + } + // The client has a URL to the current Let's Encrypt Subscriber + // Agreement. The user will need to agree to it. + log.Debug("AgreeToTOS...") + err = a.client.AgreeToTOS() + if err != nil { + log.Debug(err) + // Let's Encrypt Subscriber Agreement renew ? + reg, err := a.client.QueryRegistration() if err != nil { return err } - a.client, err = a.buildACMEClient(account) - if err != nil { - return err - } - if needRegister { - // New users will need to register; be sure to save it - log.Debug("Register...") - reg, err := a.client.Register() - if err != nil { - return err - } - account.Registration = reg - } - // The client has a URL to the current Let's Encrypt Subscriber - // Agreement. The user will need to agree to it. - log.Debug("AgreeToTOS...") + account.Registration = reg err = a.client.AgreeToTOS() if err != nil { - // Let's Encrypt Subscriber Agreement renew ? - reg, err := a.client.QueryRegistration() - if err != nil { - return err - } - account.Registration = reg - err = a.client.AgreeToTOS() - if err != nil { - log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error()) - } + log.Errorf("Error sending ACME agreement to TOS: %+v: %s", account, err.Error()) } - err = transaction.Commit(account) - if err != nil { - return err - } - - a.retrieveCertificates() - a.renewCertificates() - a.runJobs() } - return nil - }) + err = transaction.Commit(account) + if err != nil { + return err + } + + a.retrieveCertificates() + a.renewCertificates() + a.runJobs() + } return nil } @@ -253,7 +295,7 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, certs *safe.Safe, checkO a.TLSConfig = tlsConfig localStore := NewLocalStore(a.Storage) a.store = localStore - a.challengeProvider = &challengeProvider{store: a.store} + a.challengeTLSProvider = &challengeTLSProvider{store: a.store} var needRegister bool var account *Account @@ -337,7 +379,7 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat return providedCertificate, nil } - if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok { + if challengeCert, ok := a.challengeTLSProvider.getCertificate(domain); ok { log.Debugf("ACME got challenge %s", domain) return challengeCert, nil } @@ -351,7 +393,7 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat } return a.loadCertificateOnDemand(clientHello) } - log.Debugf("ACME got nothing %s", domain) + log.Debugf("No certificate found or generated for %s", domain) return nil, nil } @@ -472,16 +514,16 @@ func (a *ACME) storeRenewedCertificate(certificateResource *DomainsCertificate, return nil } -func dnsOverrideDelay(delay int) error { +func dnsOverrideDelay(delay flaeg.Duration) error { var err error if delay > 0 { - log.Debugf("Delaying %d seconds rather than validating DNS propagation", delay) + log.Debugf("Delaying %d rather than validating DNS propagation", delay) acme.PreCheckDNS = func(_, _ string) (bool, error) { - time.Sleep(time.Duration(delay) * time.Second) + time.Sleep(time.Duration(delay)) return true, nil } } else if delay < 0 { - err = fmt.Errorf("invalid negative DelayDontCheckDNS: %d", delay) + err = fmt.Errorf("invalid negative DelayBeforeCheck: %d", delay) } return err } @@ -497,25 +539,29 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) { return nil, err } - if len(a.DNSProvider) > 0 { - log.Debugf("Using DNS Challenge provider: %s", a.DNSProvider) + if a.DNSChallenge != nil && len(a.DNSChallenge.Provider) > 0 { + log.Debugf("Using DNS Challenge provider: %s", a.DNSChallenge.Provider) - err = dnsOverrideDelay(a.DelayDontCheckDNS) + err = dnsOverrideDelay(a.DNSChallenge.DelayBeforeCheck) if err != nil { return nil, err } var provider acme.ChallengeProvider - provider, err = dns.NewDNSChallengeProviderByName(a.DNSProvider) + provider, err = dns.NewDNSChallengeProviderByName(a.DNSChallenge.Provider) if err != nil { return nil, err } client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01}) err = client.SetChallengeProvider(acme.DNS01, provider) + } else if a.HTTPChallenge != nil && len(a.HTTPChallenge.EntryPoint) > 0 { + client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + a.challengeHTTPProvider = &challengeHTTPProvider{store: a.store} + err = client.SetChallengeProvider(acme.HTTP01, a.challengeHTTPProvider) } else { client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01}) - err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider) + err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeTLSProvider) } if err != nil { @@ -623,7 +669,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { // 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 { - log.Debugf("Look for provided certificate to validate %s...", domains) + log.Debugf("Looking for provided certificate to validate %s...", domains) cert := searchProvidedCertificateForDomains(domains, a.TLSConfig.NameToCertificate) if cert == nil && a.dynamicCerts != nil && a.dynamicCerts.Get() != nil { cert = searchProvidedCertificateForDomains(domains, a.dynamicCerts.Get().(*traefikTls.DomainsCertificates).Get().(map[string]*tls.Certificate)) @@ -659,7 +705,7 @@ func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple) if len(failures) > 0 { log.Error(failures) - return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) + return nil, fmt.Errorf("cannot obtain certificates %+v", failures) } log.Debugf("Loaded ACME certificates %s", domains) return &Certificate{ diff --git a/acme/acme_example.json b/acme/acme_example.json new file mode 100644 index 000000000..8ebcbfd20 --- /dev/null +++ b/acme/acme_example.json @@ -0,0 +1,43 @@ +{ + "Email": "test@traefik.io", + "Registration": { + "body": { + "resource": "reg", + "id": 3, + "key": { + "kty": "RSA", + "n": "y5a71suIqvEtovDmDVQ3SSNagk5IVCFI_TvqWpEXSrdbcDE2C-PTEtEUJuLkYwygcpiWYbPmXgdS628vQCw5Uo4DeDyHiuysJOWBLaWow3p9goOdhnPbGBq0liIR9xXyRoctdipVk8UiO9scWsu4jMBM3sMr7_yBWPfYYiLEQmZGFO3iE7Oqr55h_kncHIj5lUQY1j_jkftqxlxUB5_0quyJ7l915j5QY--eY7h4GEhRvx0TlUpi-CnRtRblGeDDDilXZD6bQN2962WdKecsmRaYx-ttLz6jCPXz2VDJRWNcIS501ne2Zh3hzw_DS6IRd2GIia1Wg4sisi9epC9sumXPHi6xzR6-_i_nsFjdtTkUcV8HmorOYoc820KQVZaLScxa8e7-ixpOd6mr6AIbEf7dBAkb9f_iK3GwpqKD8yNcaj1EQgNSyJSjnKSulXI_GwkGnuXe00Qpb1a8ha5Z8yWg7XmZZnJyAZrmK60RfwRNQ1rO5ioerNUBJ2KYTYNzVjBdob9Ug6Cjh4bEKNNjqcbjQ50_Z97Vw40xzpDQ_fYllc6n92eSuv6olxFJTmK7EhHuanDzITngaqei3zL9RwQ7P-1jfEZ03qmGrQYYqXcsS46PQ8cE-frzY2mKp16pRNCG7-03gKVGV0JHyW1aYbevNUk7OumCAXhC2YOigBk", + "e": "AQAB" + }, + "contact": [ + "mailto:test@traefik.io" + ], + "agreement": "http://boulder:4000/terms/v1" + }, + "uri": "http://127.0.0.1:4000/acme/reg/3", + "new_authzr_uri": "http://127.0.0.1:4000/acme/new-authz", + "terms_of_service": "http://boulder:4000/terms/v1" + }, + "PrivateKey": "MIIJJwIBAAKCAgEAy5a71suIqvEtovDmDVQ3SSNagk5IVCFI/TvqWpEXSrdbcDE2C+PTEtEUJuLkYwygcpiWYbPmXgdS628vQCw5Uo4DeDyHiuysJOWBLaWow3p9goOdhnPbGBq0liIR9xXyRoctdipVk8UiO9scWsu4jMBM3sMr7/yBWPfYYiLEQmZGFO3iE7Oqr55h/kncHIj5lUQY1j/jkftqxlxUB5/0quyJ7l915j5QY++eY7h4GEhRvx0TlUpi+CnRtRblGeDDDilXZD6bQN2962WdKecsmRaYx+ttLz6jCPXz2VDJRWNcIS501ne2Zh3hzw/DS6IRd2GIia1Wg4sisi9epC9sumXPHi6xzR6+/i/nsFjdtTkUcV8HmorOYoc820KQVZaLScxa8e7+ixpOd6mr6AIbEf7dBAkb9f/iK3GwpqKD8yNcaj1EQgNSyJSjnKSulXI/GwkGnuXe00Qpb1a8ha5Z8yWg7XmZZnJyAZrmK60RfwRNQ1rO5ioerNUBJ2KYTYNzVjBdob9Ug6Cjh4bEKNNjqcbjQ50/Z97Vw40xzpDQ/fYllc6n92eSuv6olxFJTmK7EhHuanDzITngaqei3zL9RwQ7P+1jfEZ03qmGrQYYqXcsS46PQ8cE+frzY2mKp16pRNCG7+03gKVGV0JHyW1aYbevNUk7OumCAXhC2YOigBkCAwEAAQKCAgA8XW1EuwTC6tAFSDhuK1JZNUpY6K05hMUHkQRj5jFpzgQmt/C2hc7H/YZkIVJmrA/G6sdsINNlffZwKH9yH6q/d6w/snLeFl7UcdhjmIL5sxAT6sKCY0fLVd/FxERfZvp3Pw2Tw+mr7v+/j7BQm6cU1M/2HRiiB9SydIqMTpKyvXB6NC6ceOFbQTL9GxlQvKyEPbS/kiH/3vRB7I5d1GfPZmNfcp6ark9X0my8VK4HRSo36H8t/OhrfLrZXvh/O82aHVf0OTv/d8AgU/jNu+XVXoXegUfWglQFDChJf1KuaE+g5w1tqgFDNgkGRD475soXA6xgZi0Iw/B9tN3zALzT4IiAW1q72feeTgKOMA2zGtKXxQZZSOV+DuWFZNz/tT7XqGQThqxM09CHv2WGOe80vobtegXYTUt90hysrqIZmBW5XYdzQlJh1KWTtfCaTrWd47kbGvhkEPc8aA3Ji4/AqfkVXiqwaLu+MSlgzPpRj7U7UAIDqnpZjgttgLp74Ujnk3bTaUzdyyNqYDBG3IFGr/Sv+2GQDAUn/PYRJKWr0BteqOzX9zvW3zY8g9CYVXfK/AW3RMWLV8ly6vH/gWqa9gEuzRNRlzjUU6/HCVbUx3UT8RMWH2TQ0uuQZr5JX1iTwjeeT0dEIly1NnRQC92wcrE4UUTBEF3krGVpDBf0AQKCAQEA4jB8w+2fwzbF8X+gCODcY7sTeJRunzGy+jbdaLkcThuylga+6W3ZgWx0BD30ql9K2mouCVu86fCTnBeXXEC3QoTdgw/EzJ83+4JU3QSDdzs9Ta9vLHyvrpUkQfZ8UZpeLLmFsmsBMbBbnfw0S1TzXDsgrAc+G4tia8nO/Iqu75kEMGzmHQAvmN3iSqc1aTS4qumbB19g+v+csq9NEht4F9jt39KotG+OD3MxCxtMu7vxAkJRjFFcgcbb2Rtqe/kQEKA1vLEAJg27lV4k8XibCSerVUR6IzT8WZHrNiXmpRguTLl2k8uFUdCOOx6aLGyRVJ6+8SgIsMR540vnxwQzEQKCAQEA5mu2wtWT19mvXopC3easPsXIPzc5oaRkqfWZYT1KHcVQ7NIXsE3vCjcf/3igZ8l/FVQ4G4fpk/GoTqlpV5Aq/JHCpVOR2O69uB+W4kWgliejpHvF9gszzAYnC8lIXqDbWiinBhmm3ii8sDGAoBaSDw5NMUq3mI+nd8zZ+jx1bLBczDafmQ0YKr8k0YaROxIgoBgDOQDdSqG387lwzpza2DKI5Al3HfS42zjT0RmBahPiuT2aEoUZmIYuvFY0fEjfkpbdvLyexHfZCILRUGlG1nAwASFg86lp+mFSBJ3E3cvbP0CpbFGxon5u4Ao3/7htoOh6huh7MQ91h41fv1hsiQKCAQAe7WRR4e7jYVzlbX7zV9Oqq0y5QwpxJ/mB7viNNiphn7Xmf5uhDU0dPjgK0HHgzdDNVpFe5DVLg4KbaDpg+dRU+xfSsNhG5kpgUGzMH67eIbJ7Kc64tX/MDkZ74nkTK1lPIjrer3TlV2jfjDmWR1JTPR51hzP9ziwx8tEjhM7woeqJuIoqUvkvHL+xV3WdIgFSFUkGVAtNpp/FauTN4gWktRupbAN3UH2LLUP6ccwnK0aD+Y9u8T0F3av33qDLvL1umIlgeI89pMkOXmYMwmHoeY0axpcwszECCkqwB7SmxEyoXv+Qq9ZZ3ntkKAYKpvmkKWSQUtoFWYgVBS727mMRAoIBABLdwusU/bPwuPEutObiWjwRiaHTbb6UbUGVQGe70vO5EjUxxorC9s2JUe9i+w9EakleyfFHIZLheHxoVp26yio/7QYIX6q5cYM/4uTH+qwQts9i6wSISkdsQYovguNsnEk3huVy+Dy8bSaoBvYUowTkkOF2Uq4FJRskBLz+ckbh8dcuqcaoUdA+Mk+NixqhE1bIYIssTPItZ5hnGJtyMGD/UkIJnF0ximk4r+8w/W2oDypHpvPZPg1E/1KgZE/Az7166NDpSL6haX3O6ECDPi+Uo/mTuBJ7TpgXm9WQ7WuTo3H8Y2LhFYBOhdmGPKuNeDxyjIW7R0rvDxp4MtzB6rECggEAJIl7/qp1lxUQPQJRTsEYBkOtdRw0IGG1Rcj0emhHaBN05c9opCy+Osb7mVeU5ZiULe5kD02phL+36pEumprz7QzN46Y5pZc8AQ2W/QkeL4Wo9U9QzczvQQzc1EqrBkzvQTZtBhn4DRzz0IuTn1beVyHtBZeNpBFgMQFv9VYQuUNwFoTOkkQrBRnYbXH6KEnhF3c/1Hzi4KHVdHdfZ3LH7KFQJ34xio0q2tWQSQYeybmwOXdd9sxpz/Y4KBS9fqm7UrwnPK8yuOc05HLEaws+1iam5YyJprlQo3mGKe0wRztwn44HDeQr70LlFm0lzigVAv0hSiWO1Q5hJL7nDu8m/Q==", + "DomainsCertificate": { + "Certs": [ + { + "Domains": { + "Main": "local1.com", + "SANs": [ + "test1.local1.com", + "test2.local1.com" + ] + }, + "Certificate": { + "Domain": "local1.com", + "CertURL": "http://127.0.0.1:4000/acme/cert/ffc4f3f14def9ee6ec6a0522b5c0baa3379d", + "CertStableURL": "", + "PrivateKey": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBdVNoTTR4enF6cE5YcFNaNnAvZnQrRmt5VmgyK1BSZXJUelV0OERRSng2UkVjQS9FCnN2RnNIVmNOSkZMS2twYTNlOEd3SUZBakJQNnJPK3hoR1JjWlJrdENON1gyOW5LZFhGbHZkYzJxd0hyTFF5WWkKTTB3ODhTck41VERiNi96TWU2dTB0dERiYWtDbDd6ZEJKUXJ6a1h5ZU1MeVkzTUs3aVkrMHpwL2JqMVhvbk5DdQpaQStkZ3hsMVNrV01DVUYvQk9HNWFyT1hwb0x4S0dQWGdzV3hOTVNLVmJKSHczL3ZqNTViZU92Um5lT3BNWlhvCmMwOWpZT3VBakNka1Z5czBSWHJLNWNCRDRMbVRXdnN4MFdTK2VMVHlGTTdQTHVZM3lEWkNNWEhjVmlqRHhnbFMKYjB1ZVRQcGFUWEQwYkxqZ0RNOUVEdE15ZEJzMUNPWlpPWG9ickN5Q2I1eWxTOFdVd1NzVXM1UldxZnlVbnAvcgpSNGx2c2RZOWRVZjRPdkNMVnJvWWk5NWFGc1Zxa0xLOExuL0Eyc3kxYWlDTnR4RmpKOXRXbWU0V0NhdzRoU0YvCkR4NWVNNWNYR2JSYXduVlZJQlZXeHhzNTBPMFJlUWRvbXBQZEFNS1RDWk9SRmxYaDdOWTdxQVdWRGtpdzhyam8Kekd3Ni9XdjlOR3hTNTliKzc0YVAxcjBxOTZ2RS9Rdi8zTCtjbjhiN0lBLytPYmFKdzhIT3RGbXc4RjBxQkN3MAprYWVVSloxb1JueGFYQUo4RHhHREpFOVdNUzh0QmJtVm16YkxoRkMzeDdVc0xGeTBrSzh1SFBFT3dQb2NKNUFUCkE1UHBvclNEMmFleHA0Z3VqYVp5c1JManpmY0dnaTdva0JFNlZVNWVqRE1iYS9lNERQNEJQUVg5VmtVQ0F3RUEKQVFLQ0FnQmZjMWdYcUp1ZmZMT3REcVlpbXh4UmIrSVVKT2NpWldaSndmZDVvY244NGtEcHFDZFZ2RUZvNnF4NgpzamQ5MURhb2xOUHdCSC9aSGxRMTR3aTNQNEluQzdzS0wwTXVEeTN5SXFUa0RPOWVwSzdPWWdVMWZyTFgvS0lCCjZlc2x2Ny9HYldFTzhhSjdKdktqM0U4NEFtcEg4UDgzenJIYTlJUnJTT3NEcmNNcEpEZHpSOXp1OW1IVDZMYmYKWC9UdC9KYTNkSW42YUxUZ0FSYkRKSjAvN0J3TFFOcXpqT0dUOWdzUWRhbGdMK2x5eEo4L1ViRndhRmVwNmgzdApvbzBHcHQ0ZWgwdTdueDhlNVd3Q2RnWmJsTnpnS3grMC9Gd3dLRHhQZVRFc2ZpOEJONmlkR2NjbVdzd3prTWdtCnJmbERaeGNSWTNRSlZIVHBCL0dTTWZXRFBPQ3dRdGltQk1WN3kxM2hPMTdPWXpSNDBMZnpUalJBbmtna2V2eWYKcFowb3dLR3o4QS9haHhRWWJmYVQ5VEhXV0wrYUpYeUhFanBKckp5aTg3UExVbzhsOFVydU56MDRWNXpLOFJPbgo2cG9EWmVtbm1EYWRlU09pK3hZRWlGT1NwSXNWbzlpcm9jUGFKN2YzYWpiNUU4RHpuN1o1MmhzL2R6akpLcFZJCm5mVDFkUU9SZEowSXRUNlRlQ2RTL0dpS25IS1RtNjR2T21IbmlJcm8rUGRhUmFjV0IrTUJ0VytRd0cyUStyRGkKc3g4NlpQbHRpTVpLMDZ5TVlyVHZUdGk2aFVGaUY5cWh4b3RGazdNQkNrZlIwYUVhaUREQUpKNm1jb1lpRUQ2QgpBVGJhVmpVaGNaUiswYkRST25PN0ozRk5rZmx3K2dMaVhvcXFRRW9pU2ZWb2h5SWY3UUtDQVFFQThjYTM5K0g4CjN3L2Qrcm0yUGNhM0RMQnBYaWU4Z3ZYcGpjazVYSkpvSGVmbnJjZWQrcFpXaTZEYncwYld0MEdtYkxmVjJNSlAKV2I1aTZzSXhmdkN3YlFqbHY0UnExMVA5ZEswT3poMnVpKzZ6cXVBMG5YTVcrN0lJS0cvdDhmS2NJZGRRNnRGcwpFclFVTFBDak56ODA2cHBiSlhPRmVvMW1BK293TGhHNlA3dDhCdlZHSk1NaTNxejNlSUNuVVE2eDNFY01ITXNuClhrM21DUzI1WUZaNk96cytFK254cGVraTAzZmQwblp3UE1jdElHZys1c3hleE9zREsrTHlvb2FqQnc5N0oyUzIKcUNNWXFtT0tLcmxEQ3Y1WmQ4dlZLN3hXVmpKRVhGTTNMZ2pieHBRcCtuVXNVVWxwS01LOVlGS0lRREl0RU9aMApWcWExTXJaOElzN1l5d0tDQVFFQXhBemZIa2pIVGlvTHdZbG5EcEk0MWlOTDh5Y0ZBallrTC94dWhPU2tlVkE4CjdRWDZPZUpDekR3Z0FUYXVqOWR6Y0wwby9yTndWV0xWcnQ3OXk3YnJvVDdFREZKWVNTY25GRXNMTlVWSXRncGkKckNSUXJTL1F2TkVGTmE5K0pRc1dmYkdBNHdIUTFaSjI4MFp1cWMvNlEyUi9kZVh3cUZBQVBHN2NIcEhHWlR6ZQoyRmFRUHFLRkV4WlEyZkpvRys0SVBRNHVQVERybXlGMmVUWXk2T3BaaDBHbWJRYlVTa1dFWDlQRmF1cHJIWVdGCk8wK25DaVVPNVRaMFZoaGR2dUNKMWdPclZHYzhBUlJtUVZ1aUNEWTZCaGlvVTU0ZmZsSXlDTXZ5a3MwcmRXZ3MKWVJ2TmN4TXNlRGJpTDRKSURkMHhiN1d4VUdmVjRVNHZPMks5Vms1N0x3S0NBUUVBMkd1eE1jcXd1RnRUc0tPYwpaaUFDcXZFZTRKRmhSVGtySHlnSW1MelZSaS9ZU3M1c3MycnZmWDA0T3N5bVZ0UUZUVHdoeUMzbktjWXFkVW52ClZGblBFMHJyblV2Qzk0elBUQ205SHZPaTBzK1JORndOdlFMUWgrME5NR1ZBOFZyaU44aXRQZ1RJWU5XaFdianQKNFA1TE45V0QwVHBmT1J4cFBRZmNxT0JsZjdjcmhtNzNvdUNwemZtMmE3OStCaWpKUFF5NzR1cFhDeXRmeHNlUApNSlU0Uk56NjdJaDFMclpKM2xGbDFvYitZT2xKazhDOHpZd1RLT0hWck9zeGxobyt4SXN2Q2t3MDFMelZ6Mi9hCnRmT3Y5NTlHSnQzbXE0ZWpJUFZPQy9iUlpmdTMvMEdSY2dpQTZ5SnpaM0VxWTVaOU1EbTU3VzdjcE5RRlRxZmEKNXEyUmtRS0NBUUErNGhZSzQ3TXg2aUNkTWxKaEJSdS82OUJucktOWm96NFdPalRFNFlXejk3MmpGU0Mrd2tsRQpzeUJjNDBvNGp4WFRHb2wwc04rZU03WndnY3dNTko3OXVHRXZ4cFhVMlA4YTdqc3BHaEVKZXVsTlo5U015R0orCnZkaWE4TEJZZDJiK2FCbjhOay9pd1Rqd0xTNC92NXI1Vk5uaFdpRElDK2tYZVVPWGRwQ1pWbDN3TEV2V0cxRHQKMzJHTmxzZzM5VENsVE5BZUJudjc1VTdYOEQrQ0gvRVpoa0E0aGxFL2hXN0JRZTczclRzd1creHhLc3BjWWFpVwpjdEg3NzVMYUw3Rm1lUVRTYk01OVZpcTZXZ2J0OVY3Rko5R09DSkQzZHF2ZjBITDlEVndjSzQ3WWt3OWlFc3RYCnY5cnEvREhhYUpGNzBGNlFlTTNNbDhSa212WTZJYkEzQW9JQkFRRGt6RmZLeG9HQ3dWUDlua3k4NmFQSjFvd2kKc2FDZEx6RjRWTENRZzkrUXJITzEyY0p5MFFQUnJ2cUQyMGp1cDFlOWJhWVZzbkdYc1FZTFg2NVR6UzJSSCtlSAp6S0NPTTdnMVE3djMxNWpjMDMvN1lQck4rb3RrV0VBOUkyaDZjUE1vY3c0aERTNk02OFlxQVlKTS9RclVhenZhCnhBTFJaZEVkQW1xWDA4VHhuY1hRUEVxYkk0ZnlSZ2pVM1BYR3RRaFFFbERpR2kwbThjQTJNTXdsR1RmbTdOSXgKaENjZ2ZkL296TEp2VUhiMkxLRi82cXEySmJVRHlOMkVoK0xSZUJjdnp6Y1grZE5MdGQxY0Uvcm1SM2hMbWxmNgo3KzRpTVMxK0t1eWV3VlJVUEE1c1F1aUYyVUVoeEs1MUpZK1FpOG9HbERKdGRrOXB3QlZNN1F0WW9KVEwKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K", + "Certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZvakNDQklxZ0F3SUJBZ0lUQVAvRTgvRk43NTdtN0dvRklyWEF1cU0zblRBTkJna3Foa2lHOXcwQkFRc0YKQURBZk1SMHdHd1lEVlFRRERCUm9NbkJ3ZVNCb01tTnJaWElnWm1GclpTQkRRVEFlRncweE9EQXhNVFV3TnpJNQpNREJhRncweE9EQTBNVFV3TnpJNU1EQmFNRVF4RXpBUkJnTlZCQU1UQ214dlkyRnNNUzVqYjIweExUQXJCZ05WCkJBVVRKR1ptWXpSbU0yWXhOR1JsWmpsbFpUWmxZelpoTURVeU1tSTFZekJpWVdFek16YzVaRENDQWlJd0RRWUoKS29aSWh2Y05BUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBTGtvVE9NYzZzNlRWNlVtZXFmMzdmaFpNbFlkdmowWApxMDgxTGZBMENjZWtSSEFQeExMeGJCMVhEU1JTeXBLV3QzdkJzQ0JRSXdUK3F6dnNZUmtYR1VaTFFqZTE5dlp5Cm5WeFpiM1hOcXNCNnkwTW1Jak5NUFBFcXplVXcyK3Y4ekh1cnRMYlEyMnBBcGU4M1FTVUs4NUY4bmpDOG1OekMKdTRtUHRNNmYyNDlWNkp6UXJtUVBuWU1aZFVwRmpBbEJmd1RodVdxemw2YUM4U2hqMTRMRnNUVEVpbFd5UjhOLwo3NCtlVzNqcjBaM2pxVEdWNkhOUFkyRHJnSXduWkZjck5FVjZ5dVhBUStDNWsxcjdNZEZrdm5pMDhoVE96eTdtCk44ZzJRakZ4M0ZZb3c4WUpVbTlMbmt6NldrMXc5R3k0NEF6UFJBN1RNblFiTlFqbVdUbDZHNndzZ20rY3BVdkYKbE1FckZMT1VWcW44bEo2ZjYwZUpiN0hXUFhWSCtEcndpMWE2R0l2ZVdoYkZhcEN5dkM1L3dOck10V29namJjUgpZeWZiVnBudUZnbXNPSVVoZnc4ZVhqT1hGeG0wV3NKMVZTQVZWc2NiT2REdEVYa0hhSnFUM1FEQ2t3bVRrUlpWCjRleldPNmdGbFE1SXNQSzQ2TXhzT3Yxci9UUnNVdWZXL3UrR2o5YTlLdmVyeFAwTC85eS9uSi9HK3lBUC9qbTIKaWNQQnpyUlpzUEJkS2dRc05KR25sQ1dkYUVaOFdsd0NmQThSZ3lSUFZqRXZMUVc1bFpzMnk0UlF0OGUxTEN4Ywp0SkN2TGh6eERzRDZIQ2VRRXdPVDZhSzBnOW1uc2FlSUxvMm1jckVTNDgzM0JvSXU2SkFST2xWT1hvd3pHMnYzCnVBeitBVDBGL1ZaRkFnTUJBQUdqZ2dHd01JSUJyREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdIUVlEVlIwbEJCWXcKRkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQndNQ01Bd0dBMVVkRXdFQi93UUNNQUF3SFFZRFZSME9CQllFRk5LZQpBVUZYc2Z2N2lML0lYVVBXdzY2ZU5jQnhNQjhHQTFVZEl3UVlNQmFBRlB0NFR4TDVZQldETEo4WGZ6UVpzeTQyCjZrR0pNR1lHQ0NzR0FRVUZCd0VCQkZvd1dEQWlCZ2dyQmdFRkJRY3dBWVlXYUhSMGNEb3ZMekV5Tnk0d0xqQXUKTVRvME1EQXlMekF5QmdnckJnRUZCUWN3QW9ZbWFIUjBjRG92THpFeU55NHdMakF1TVRvME1EQXdMMkZqYldVdgphWE56ZFdWeUxXTmxjblF3T1FZRFZSMFJCREl3TUlJS2JHOWpZV3d4TG1OdmJZSVFkR1Z6ZERFdWJHOWpZV3d4CkxtTnZiWUlRZEdWemRESXViRzlqWVd3eExtTnZiVEFuQmdOVkhSOEVJREFlTUJ5Z0dxQVloaFpvZEhSd09pOHYKWlhoaGJYQnNaUzVqYjIwdlkzSnNNR0VHQTFVZElBUmFNRmd3Q0FZR1o0RU1BUUlCTUV3R0F5b0RCREJGTUNJRwpDQ3NHQVFVRkJ3SUJGaFpvZEhSd09pOHZaWGhoYlhCc1pTNWpiMjB2WTNCek1COEdDQ3NHQVFVRkJ3SUNNQk1NCkVVUnZJRmRvWVhRZ1ZHaHZkU0JYYVd4ME1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ3A0Q2FxZlR4THNQTzQKS2JueDJZdEc4bTN3MC9keTVVR1VRNjZHbGxPVTk0L2I0MmNhbTRuNUZrTWlpZ01IaUx4c2JZVXh0cDZKQ3R5cQpLKzFNcDFWWEtSTTVKbFBTNWRIaWhxdHk1U3BrTUhjampwQSs3U2YyVWtoNmpKRWYxTUVJY2JnWnpJRk5IT0hYClVUUUppVFhKcno3blJDZnlQWFZtbWErUGtIRlU4R0VEVzJGOVptU1kzVFBiQWhiWkV2UkZubjUrR1lxbkZuancKWWw3Y0I2MXYwRzVpOGQwbnVvbTB4a2hiNTU3Y3BiZHhLblhsaFU4N2RZSTR5SUdPdUFGUWpYcXFXN2NIZCtXUQpWSDB2dFA3cEgrRmt2YnY4WkkxMHMrNU5ZcCtzZjFQZGQxekJsRmdNSGF3dnFFYUg3SU9sejdkajlCdmtVc0dpClhxQWVqQnFPCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVpakNDQTNLZ0F3SUJBZ0lDRWswd0RRWUpLb1pJaHZjTkFRRUxCUUF3S3pFcE1DY0dBMVVFQXd3Z1kyRmoKYTJ4cGJtY2dZM0o1Y0hSdlozSmhjR2hsY2lCbVlXdGxJRkpQVDFRd0hoY05NVFV4TURJeE1qQXhNVFV5V2hjTgpNakF4TURFNU1qQXhNVFV5V2pBZk1SMHdHd1lEVlFRREV4Um9ZWEJ3ZVNCb1lXTnJaWElnWm1GclpTQkRRVENDCkFTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUlLUjNtYUJjVVNzbmNYWXpRVDEzRDUKTnIrWjNtTHhNTWgzVFVkdDZzQUNtcWJKMGJ0UmxnWGZNdE5MTTJPVTFJNmEzSnUrdElaU2RuMnYyMUpCd3Z4VQp6cFpRNHp5MmNpbUlpTVFEWkNRSEp3ekM5R1puOEhhVzA5MWl6OUgwR28zQTdXRFh3WU5tc2RMTlJpMDBvMTRVCmpvYVZxYVBzWXJaV3ZSS2FJUnFhVTBoSG1TMEFXd1FTdk4vOTNpTUlYdXlpd3l3bWt3S2JXbm54Q1EvZ3NjdEsKRlV0Y05yd0V4OVdnajZLbGh3RFR5STFRV1NCYnhWWU55VWdQRnpLeHJTbXdNTzB5TmZmN2hvK1FUOXg1K1kvNwpYRTU5UzRNYzRaWHhjWEtldy9nU2xOOVU1bXZUK0QyQmhEdGtDdXBkZnNaTkNRV3AyN0ErYi9EbXJGSTlOcXNDCkF3RUFBYU9DQWNJd2dnRytNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUF3UXdZRFZSMGVCRHd3T3FFNE1BYUMKQkM1dGFXd3dDb2NJQUFBQUFBQUFBQUF3SW9jZ0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQpBQUFBQUFBd0RnWURWUjBQQVFIL0JBUURBZ0dHTUg4R0NDc0dBUVVGQndFQkJITXdjVEF5QmdnckJnRUZCUWN3CkFZWW1hSFIwY0RvdkwybHpjbWN1ZEhKMWMzUnBaQzV2WTNOd0xtbGtaVzUwY25WemRDNWpiMjB3T3dZSUt3WUIKQlFVSE1BS0dMMmgwZEhBNkx5OWhjSEJ6TG1sa1pXNTBjblZ6ZEM1amIyMHZjbTl2ZEhNdlpITjBjbTl2ZEdOaAplRE11Y0Rkak1COEdBMVVkSXdRWU1CYUFGT21rUCs2ZXBlYnkxZGQ1WUR5VHBpNGtqcGVxTUZRR0ExVWRJQVJOCk1Fc3dDQVlHWjRFTUFRSUJNRDhHQ3lzR0FRUUJndDhUQVFFQk1EQXdMZ1lJS3dZQkJRVUhBZ0VXSW1oMGRIQTYKTHk5amNITXVjbTl2ZEMxNE1TNXNaWFJ6Wlc1amNubHdkQzV2Y21jd1BBWURWUjBmQkRVd016QXhvQytnTFlZcgphSFIwY0RvdkwyTnliQzVwWkdWdWRISjFjM1F1WTI5dEwwUlRWRkpQVDFSRFFWZ3pRMUpNTG1OeWJEQWRCZ05WCkhRNEVGZ1FVKzNoUEV2bGdGWU1zbnhkL05CbXpMamJxUVlrd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFBMFkKQWVMWE9rbHg0aGhDaWtVVWwrQmRuRmZuMWcwVzVBaVFMVk5JT0w2UG5xWHUwd2puaE55aHFkd25maFlNbm95NAppZFJoNGxCNnB6OEdmOXBubExkL0RuV1NWM2dTKy9JL21BbDFkQ2tLYnk2SDJWNzkwZTZJSG1JSzJLWW0zam0rClUrK0ZJZEdwQmRzUVRTZG1pWC9yQXl1eE1ETTBhZE1rTkJ3VGZRbVpRQ3o2bkdIdzFRY1NQWk12WnBzQzhTa3YKZWt6eHNqRjFvdE9yTVVQTlBRdnRUV3JWeDhHbFIycWZ4LzR4YlFhMXYyZnJOdkZCQ21PNTlnb3oram5XdmZUdApqMk5qd0RaN3ZsTUJzUG0xNmRiS1lDODQwdXZSb1pqeHFzZGMzQ2hDWmpxaW1GcWxORy94b1BBOCtkVGljWnpDClhFOWlqUEljdlc2eTFhYTNiR3c9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" + } + } + ] + }, + "ChallengeCerts": {} +} diff --git a/acme/acme_test.go b/acme/acme_test.go index 272e2571d..3498e7b9f 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -267,7 +267,7 @@ cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`) }`)) })) defer ts.Close() - a := ACME{DNSProvider: "manual", DelayDontCheckDNS: 10, CAServer: ts.URL} + a := ACME{DNSChallenge: &DNSChallenge{Provider: "manual", DelayBeforeCheck: 10}, CAServer: ts.URL} client, err := a.buildACMEClient(account) if err != nil { diff --git a/acme/challenge_http_provider.go b/acme/challenge_http_provider.go new file mode 100644 index 000000000..901fa5594 --- /dev/null +++ b/acme/challenge_http_provider.go @@ -0,0 +1,92 @@ +package acme + +import ( + "fmt" + "sync" + "time" + + "github.com/cenk/backoff" + "github.com/containous/traefik/cluster" + "github.com/containous/traefik/log" + "github.com/containous/traefik/safe" + "github.com/xenolf/lego/acme" +) + +var _ acme.ChallengeProviderTimeout = (*challengeHTTPProvider)(nil) + +type challengeHTTPProvider struct { + store cluster.Store + lock sync.RWMutex +} + +func (c *challengeHTTPProvider) getTokenValue(token, domain string) []byte { + log.Debugf("Looking for an existing ACME challenge for token %v...", token) + c.lock.RLock() + defer c.lock.RUnlock() + account := c.store.Get().(*Account) + if account.HTTPChallenge == nil { + return []byte{} + } + var result []byte + operation := func() error { + var ok bool + if result, ok = account.HTTPChallenge[token][domain]; !ok { + return fmt.Errorf("cannot find challenge for token %v", token) + } + return nil + } + notify := func(err error, time time.Duration) { + log.Errorf("Error getting challenge for token retrying in %s", time) + } + ebo := backoff.NewExponentialBackOff() + ebo.MaxElapsedTime = 60 * time.Second + err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify) + if err != nil { + log.Errorf("Error getting challenge for token: %v", err) + return []byte{} + } + return result +} + +func (c *challengeHTTPProvider) Present(domain, token, keyAuth string) error { + log.Debugf("Challenge Present %s", domain) + c.lock.Lock() + defer c.lock.Unlock() + transaction, object, err := c.store.Begin() + if err != nil { + return err + } + account := object.(*Account) + if account.HTTPChallenge == nil { + account.HTTPChallenge = map[string]map[string][]byte{} + } + if _, ok := account.HTTPChallenge[token]; !ok { + account.HTTPChallenge[token] = map[string][]byte{} + } + account.HTTPChallenge[token][domain] = []byte(keyAuth) + return transaction.Commit(account) +} + +func (c *challengeHTTPProvider) CleanUp(domain, token, keyAuth string) error { + log.Debugf("Challenge CleanUp %s", domain) + c.lock.Lock() + defer c.lock.Unlock() + transaction, object, err := c.store.Begin() + if err != nil { + return err + } + account := object.(*Account) + if _, ok := account.HTTPChallenge[token]; ok { + if _, domainOk := account.HTTPChallenge[token][domain]; domainOk { + delete(account.HTTPChallenge[token], domain) + } + if len(account.HTTPChallenge[token]) == 0 { + delete(account.HTTPChallenge, token) + } + } + return transaction.Commit(account) +} + +func (c *challengeHTTPProvider) Timeout() (timeout, interval time.Duration) { + return 60 * time.Second, 5 * time.Second +} diff --git a/acme/challengeProvider.go b/acme/challenge_tls_provider.go similarity index 87% rename from acme/challengeProvider.go rename to acme/challenge_tls_provider.go index 9a678e4d4..ec2d35954 100644 --- a/acme/challengeProvider.go +++ b/acme/challenge_tls_provider.go @@ -23,15 +23,15 @@ import ( "github.com/xenolf/lego/acme" ) -var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil) +var _ acme.ChallengeProviderTimeout = (*challengeTLSProvider)(nil) -type challengeProvider struct { +type challengeTLSProvider struct { store cluster.Store lock sync.RWMutex } -func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { - log.Debugf("Challenge GetCertificate %s", domain) +func (c *challengeTLSProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) { + log.Debugf("Looking for an existing ACME challenge for %s...", domain) if !strings.HasSuffix(domain, ".acme.invalid") { return nil, false } @@ -67,7 +67,7 @@ func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate return result, true } -func (c *challengeProvider) Present(domain, token, keyAuth string) error { +func (c *challengeTLSProvider) Present(domain, token, keyAuth string) error { log.Debugf("Challenge Present %s", domain) cert, _, err := tlsSNI01ChallengeCert(keyAuth) if err != nil { @@ -88,7 +88,7 @@ func (c *challengeProvider) Present(domain, token, keyAuth string) error { return transaction.Commit(account) } -func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error { +func (c *challengeTLSProvider) CleanUp(domain, token, keyAuth string) error { log.Debugf("Challenge CleanUp %s", domain) c.lock.Lock() defer c.lock.Unlock() @@ -101,7 +101,7 @@ func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error { return transaction.Commit(account) } -func (c *challengeProvider) Timeout() (timeout, interval time.Duration) { +func (c *challengeTLSProvider) Timeout() (timeout, interval time.Duration) { return 60 * time.Second, 5 * time.Second } diff --git a/acme/localStore_test.go b/acme/localStore_test.go new file mode 100644 index 000000000..a2bfce742 --- /dev/null +++ b/acme/localStore_test.go @@ -0,0 +1,41 @@ +package acme + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestLoad(t *testing.T) { + acmeFile := "./acme_example.json" + + folder, prefix := filepath.Split(acmeFile) + tmpFile, err := ioutil.TempFile(folder, prefix) + defer os.Remove(tmpFile.Name()) + + if err != nil { + t.Error(err) + } + + fileContent, err := ioutil.ReadFile(acmeFile) + if err != nil { + t.Error(err) + } + + tmpFile.Write(fileContent) + + localStore := NewLocalStore(tmpFile.Name()) + obj, err := localStore.Load() + if err != nil { + t.Error(err) + } + account, ok := obj.(*Account) + if !ok { + t.Error("Object is not an ACME Account") + } + + if len(account.DomainsCertificate.Certs) != 1 { + t.Errorf("Must found %d and found %d certificates in Account", 3, len(account.DomainsCertificate.Certs)) + } +} diff --git a/api/dashboard.go b/api/dashboard.go index 64717b7dc..75b573597 100644 --- a/api/dashboard.go +++ b/api/dashboard.go @@ -15,7 +15,7 @@ type DashboardHandler struct{} func (g DashboardHandler) AddRoutes(router *mux.Router) { // Expose dashboard router.Methods(http.MethodGet).Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) { - http.Redirect(response, request, "/dashboard/", 302) + http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", 302) }) router.Methods(http.MethodGet).PathPrefix("/dashboard/"). Handler(http.StripPrefix("/dashboard/", http.FileServer(&assetfs.AssetFS{Asset: genstatic.Asset, AssetInfo: genstatic.AssetInfo, AssetDir: genstatic.AssetDir, Prefix: "static"}))) diff --git a/api/handler.go b/api/handler.go index f9510c072..2ecb103e1 100644 --- a/api/handler.go +++ b/api/handler.go @@ -19,9 +19,9 @@ type Handler struct { Dashboard bool `description:"Activate dashboard" export:"true"` Debug bool `export:"true"` CurrentConfigurations *safe.Safe - Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"` - Stats *thoas_stats.Stats - StatsRecorder *middlewares.StatsRecorder + Statistics *types.Statistics `description:"Enable more detailed statistics" export:"true"` + Stats *thoas_stats.Stats `json:"-"` + StatsRecorder *middlewares.StatsRecorder `json:"-"` } var ( diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index 0480b29ce..0e6e7b6e4 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -169,7 +169,7 @@ func TestDo_globalConfiguration(t *testing.T) { OnHostRule: true, CAServer: "CAServer", EntryPoint: "EntryPoint", - DNSProvider: "DNSProvider", + DNSChallenge: &acme.DNSChallenge{Provider: "DNSProvider"}, DelayDontCheckDNS: 666, ACMELogging: true, TLSConfig: &tls.Config{ diff --git a/configuration/configuration.go b/configuration/configuration.go index 49cf34b93..d78f926da 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -243,6 +243,23 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) { log.Errorln("Error using file configuration backend, no filename defined") } } + + if gc.ACME != nil { + // TODO: to remove in the futurs + if len(gc.ACME.StorageFile) > 0 && len(gc.ACME.Storage) == 0 { + log.Warn("ACME.StorageFile is deprecated, use ACME.Storage instead") + gc.ACME.Storage = gc.ACME.StorageFile + } + + if len(gc.ACME.DNSProvider) > 0 { + log.Warn("ACME.DNSProvider is deprecated, use ACME.DNSChallenge instead") + gc.ACME.DNSChallenge = &acme.DNSChallenge{Provider: gc.ACME.DNSProvider, DelayBeforeCheck: gc.ACME.DelayDontCheckDNS} + } + + if gc.ACME.OnDemand { + log.Warn("ACME.OnDemand is deprecated") + } + } } // DefaultEntryPoints holds default entry points diff --git a/docs/basics.md b/docs/basics.md index 9a84281a4..5c30b78e4 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -632,8 +632,7 @@ Once a day (the first call begins 10 minutes after the start of Træfik), we col [entryPoints.http] address = ":80" -[web] - address = ":8080" +[api] [Docker] endpoint = "tcp://10.10.10.10:2375" @@ -663,8 +662,7 @@ Once a day (the first call begins 10 minutes after the start of Træfik), we col [entryPoints.http] address = ":80" -[web] - address = ":8080" +[api] [Docker] Endpoint = "xxxx" diff --git a/docs/configuration/acme.md b/docs/configuration/acme.md index 05a105c1c..53cd3cae7 100644 --- a/docs/configuration/acme.md +++ b/docs/configuration/acme.md @@ -7,10 +7,14 @@ See also [Let's Encrypt examples](/user-guide/examples/#lets-encrypt-support) an ```toml # Sample entrypoint configuration when using ACME. [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] +``` +```toml # Enable ACME (Let's Encrypt): automatic SSL. [acme] @@ -33,17 +37,16 @@ email = "test@traefik.io" storage = "acme.json" # or `storage = "traefik/acme/account"` if using KV store. -# Entrypoint to proxy acme challenge/apply certificates to. -# WARNING, must point to an entrypoint on port 443 +# Entrypoint to proxy acme apply certificates to. +# WARNING, if the TLS-SNI-01 challenge is used, it must point to an entrypoint on port 443 # # Required # entryPoint = "https" -# Use a DNS based acme challenge rather than external HTTPS access +# Use a DNS-01 acme challenge rather than TLS-SNI-01 challenge # -# -# Optional +# Optional (Deprecated, replaced by [acme.dnsChallenge]) # # dnsProvider = "digitalocean" @@ -51,25 +54,29 @@ entryPoint = "https" # If delayDontCheckDNS is greater than zero, avoid this & instead just wait so many seconds. # Useful if internal networks block external DNS queries. # -# Optional +# Optional (Deprecated, replaced by [acme.dnsChallenge]) +# Default: 0 # # delayDontCheckDNS = 0 # If true, display debug log messages from the acme client library. # # Optional +# Default: false # # acmeLogging = true -# Enable on demand certificate. (Deprecated) +# Enable on demand certificate generation. # -# Optional +# Optional (Deprecated) +# Default: false # # onDemand = true # Enable certificate generation on frontends Host rules. # # Optional +# Default: false # # onHostRule = true @@ -78,26 +85,64 @@ entryPoint = "https" # - Leave comment to go to prod. # # Optional +# Default: "https://acme-v01.api.letsencrypt.org/directory" # # caServer = "https://acme-staging.api.letsencrypt.org/directory" # Domains list. # # [[acme.domains]] -# main = "local1.com" -# sans = ["test1.local1.com", "test2.local1.com"] +# main = "local1.com" +# sans = ["test1.local1.com", "test2.local1.com"] # [[acme.domains]] -# main = "local2.com" -# sans = ["test1.local2.com", "test2.local2.com"] +# main = "local2.com" +# sans = ["test1.local2.com", "test2.local2.com"] # [[acme.domains]] -# main = "local3.com" +# main = "local3.com" # [[acme.domains]] -# main = "local4.com" +# main = "local4.com" + +# Use a HTTP-01 acme challenge rather than TLS-SNI-01 challenge +# +# Optional but recommend +# +[acme.httpChallenge] + + # EntryPoint to use for the challenges. + # + # Required + # + entryPoint = "http" + +# Use a DNS-01 acme challenge rather than TLS-SNI-01 challenge +# +# Optional +# +# [acme.dnsChallenge] + + # Provider used. + # + # Required + # + # provider = "digitalocean" + + # By default, the provider will verify the TXT DNS challenge record before letting ACME verify. + # If delayBeforeCheck is greater than zero, avoid this & instead just wait so many seconds. + # Useful if internal networks block external DNS queries. + # + # Optional + # Default: 0 + # + # delayBeforeCheck = 0 ``` +!!! note + Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188) for the moment, it stays the _by default_ ACME Challenge in Træfik. + If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik. !!! note - ACME entryPoint has to be relied to the port 443, otherwise ACME Challenges can not be done. - It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). + If `TLS-SNI-01` challenge is used, `acme.entryPoint` has to be reachable by Let's Encrypt through the port 443. + If `HTTP-01` challenge is used, `acme.httpChallenge.entryPoint` has to be defined and reachable by Let's Encrypt through the port 80. + These are Let's Encrypt limitations as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). ### `storage` @@ -110,7 +155,7 @@ storage = "acme.json" File or key used for certificates storage. -**WARNING** If you use Træfik in Docker, you have 2 options: +**WARNING:** If you use Træfik in Docker, you have 2 options: - create a file on your host and mount it as a volume: ```toml @@ -133,19 +178,60 @@ docker run -v "/my/host/acme:/etc/traefik/acme" traefik !!! note During Træfik configuration migration from a configuration file to a KV store (thanks to `storeconfig` subcommand as described [here](/user-guide/kv-config/#store-configuration-in-key-value-store)), if ACME certificates have to be migrated too, use both `storageFile` and `storage`. - `storageFile` will contain the path to the `acme.json` file to migrate. - `storage` will contain the key where the certificates will be stored. -### `dnsProvider` + - `storageFile` will contain the path to the `acme.json` file to migrate. + - `storage` will contain the key where the certificates will be stored. + +### `acme.httpChallenge` + +Use `HTTP-01` challenge to generate/renew ACME certificates. ```toml [acme] # ... -dnsProvider = "digitalocean" +entryPoint = "https" +[acme.httpChallenge] + entryPoint = "http" +``` + +#### `entryPoint` + +Specify the entryPoint to use during the challenges. + +```toml +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] +# ... + +[acme] + # ... + entryPoint = "https" + [acme.httpChallenge] + entryPoint = "http" +``` + +!!! note + `acme.httpChallenge.entryPoint` has to be reachable by Let's Encrypt through the port 80. + It's a Let's Encrypt limitation as described on the [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72). + +### `acme.dnsChallenge` + +Use `DNS-01` challenge to generate/renew ACME certificates. + +```toml +[acme] +# ... +[acme.dnsChallenge] + provider = "digitalocean" + delayBeforeCheck = 0 # ... ``` -Use a DNS based acme challenge rather than external HTTPS access, e.g. for a firewalled server. +#### `provider` Select the provider that matches the DNS domain that will host the challenge TXT record, and provide environment variables to enable setting it: @@ -164,7 +250,7 @@ Select the provider that matches the DNS domain that will host the challenge TXT | [GoDaddy](https://godaddy.com/domains) | `godaddy` | `GODADDY_API_KEY`, `GODADDY_API_SECRET` | | [Google Cloud DNS](https://cloud.google.com/dns/docs/) | `gcloud` | `GCE_PROJECT`, `GCE_SERVICE_ACCOUNT_FILE` | | [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | -| manual | - | none, but run Træfik interactively & turn on `acmeLogging` to see instructions & press Enter. | +| manual | - | none, but run Træfik interactively & turn on `acmeLogging` to see instructions & press Enter. | | [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | | [Ns1](https://ns1.com/) | `ns1` | `NS1_API_KEY` | | [Open Telekom Cloud](https://cloud.telekom.de/en/) | `otc` | `OTC_DOMAIN_NAME`, `OTC_USER_NAME`, `OTC_PASSWORD`, `OTC_PROJECT_NAME`, `OTC_IDENTITY_ENDPOINT` | @@ -175,22 +261,21 @@ Select the provider that matches the DNS domain that will host the challenge TXT | [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or configured user/instance IAM profile. | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | -### `delayDontCheckDNS` +#### `delayBeforeCheck` -```toml -[acme] -# ... -delayDontCheckDNS = 0 -# ... -``` - -By default, the dnsProvider will verify the TXT DNS challenge record before letting ACME verify. -If `delayDontCheckDNS` is greater than zero, avoid this & instead just wait so many seconds. +By default, the `provider` will verify the TXT DNS challenge record before letting ACME verify. +If `delayBeforeCheck` is greater than zero, avoid this & instead just wait so many seconds. Useful if internal networks block external DNS queries. +!!! note + This field has no sense if a `provider` is not defined. + ### `onDemand` (Deprecated) +!!! warning + This option is deprecated. + ```toml [acme] # ... @@ -208,9 +293,6 @@ This will request a certificate from Let's Encrypt during the first TLS handshak !!! warning Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits). -!!! warning - This option is deprecated. - ### `onHostRule` ```toml @@ -240,21 +322,21 @@ CA server to use. - Uncomment the line to run on the staging Let's Encrypt server. - Leave comment to go to prod. -### `domains` +### `acme.domains` ```toml [acme] # ... [[acme.domains]] -main = "local1.com" -sans = ["test1.local1.com", "test2.local1.com"] + main = "local1.com" + sans = ["test1.local1.com", "test2.local1.com"] [[acme.domains]] -main = "local2.com" -sans = ["test1.local2.com", "test2.local2.com"] + main = "local2.com" + sans = ["test1.local2.com", "test2.local2.com"] [[acme.domains]] -main = "local3.com" + main = "local3.com" [[acme.domains]] -main = "local4.com" + main = "local4.com" # ... ``` @@ -265,3 +347,15 @@ All domains must have A/AAAA records pointing to Træfik. Take note that Let's Encrypt have [rate limiting](https://letsencrypt.org/docs/rate-limits). Each domain & SANs will lead to a certificate request. + +### `dnsProvider` (Deprecated) + +!!! warning + This option is deprecated. + Please refer to [DNS challenge provider section](/configuration/acme/#provider) + +### `delayDontCheckDNS` (Deprecated) + +!!! warning + This option is deprecated. + Please refer to [DNS challenge delayBeforeCheck section](/configuration/acme/#delaybeforecheck) diff --git a/docs/configuration/api.md b/docs/configuration/api.md index 8c89644bf..ac6afd7dc 100644 --- a/docs/configuration/api.md +++ b/docs/configuration/api.md @@ -161,7 +161,7 @@ curl -s "http://localhost:8080/health" | jq . // average response time in seconds "average_response_time_sec": 0.8648016000000001, - // request statistics [requires --web.statistics to be set] + // request statistics [requires --statistics to be set] // ten most recent requests with 4xx and 5xx status codes "recent_errors": [ { diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index b2b5b94f4..6093a9a80 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -4,7 +4,6 @@ Træfik can be configured to use Kubernetes Ingress as a backend configuration. See also [Kubernetes user guide](/user-guide/kubernetes). - ## Configuration ```toml @@ -44,7 +43,7 @@ See also [Kubernetes user guide](/user-guide/kubernetes). # # namespaces = ["default", "production"] -# Ingress label selector to identify Ingress objects that should be processed. +# Ingress label selector to filter Ingress objects that should be processed. # # Optional # Default: empty (process all Ingresses) @@ -75,30 +74,33 @@ See also [Kubernetes user guide](/user-guide/kubernetes). ### `endpoint` -The Kubernetes server endpoint. +The Kubernetes server endpoint as URL. -When deployed as a replication controller in Kubernetes, Traefik will use the environment variables `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` to construct the endpoint. +When deployed into Kubernetes, Traefik will read the environment variables `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` to construct the endpoint. -Secure token will be found in `/var/run/secrets/kubernetes.io/serviceaccount/token` and SSL CA cert in `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` +The access token will be looked up in `/var/run/secrets/kubernetes.io/serviceaccount/token` and the SSL CA certificate in `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`. +Both are provided mounted automatically when deployed inside Kubernetes. -The endpoint may be given to override the environment variable values. +The endpoint may be specified to override the environment variable values inside a cluster. When the environment variables are not found, Traefik will try to connect to the Kubernetes API server with an external-cluster client. In this case, the endpoint is required. -Specifically, it may be set to the URL used by `kubectl proxy` to connect to a Kubernetes cluster from localhost. +Specifically, it may be set to the URL used by `kubectl proxy` to connect to a Kubernetes cluster using the granted autentication and authorization of the associated kubeconfig. ### `labelselector` -Ingress label selector to identify Ingress objects that should be processed. +By default, Traefik processes all Ingress objects in the configured namespaces. +A label selector can be defined to filter on specific Ingress objects only. See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. - ## Annotations -Annotations can be used on containers to override default behaviour for the whole Ingress resource: +### General annotations -- `traefik.frontend.rule.type: PathPrefixStrip` +The following general annotations are applicable on the Ingress object: + +- `traefik.frontend.rule.type: PathPrefixStrip` Override the default frontend rule type. Default: `PathPrefix`. - `traefik.frontend.priority: "3"` Override the default frontend rule priority. @@ -108,50 +110,41 @@ Annotations can be used on containers to override default behaviour for the whol Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.replacement`. - `traefik.frontend.redirect.replacement: http://mydomain/$1`: Redirect to another URL for that frontend. Must be set with `traefik.frontend.redirect.regex`. -- `traefik.frontend.entryPoints: http,https` +- `traefik.frontend.entryPoints: http,https` Override the default frontend endpoints. -- `traefik.frontend.passTLSCert: true` +- `traefik.frontend.passTLSCert: true` Override the default frontend PassTLSCert value. Default: `false`. - `ingress.kubernetes.io/rewrite-target: /users` Replaces each matched Ingress path with the specified one, and adds the old path to the `X-Replaced-Path` header. +- `ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` + A comma-separated list of IP ranges permitted for access. all source IPs are permitted if the list is empty or a single range is ill-formatted. !!! note Please note that `traefik.frontend.redirect.regex` and `traefik.frontend.redirect.replacement` do not have to be set if `traefik.frontend.redirect.entryPoint` is defined for the redirection (they will not be used in this case). +The following annotations are applicable on the Service object associated with a particular Ingress object: -Annotations can be used on the Kubernetes service to override default behaviour: +- `traefik.backend.loadbalancer.method=drr` + Override the default `wrr` load balancer algorithm. +- `traefik.backend.loadbalancer.stickiness=true` + Enable backend sticky sessions. +- `traefik.backend.loadbalancer.stickiness.cookieName=NAME` + Manually set the cookie name for sticky sessions. +- `traefik.backend.loadbalancer.sticky=true` + Enable backend sticky sessions (DEPRECATED). +- `traefik.backend.circuitbreaker: ` + Set the circuit breaker expression for the backend. -- `traefik.backend.loadbalancer.method=drr` - Override the default `wrr` load balancer algorithm -- `traefik.backend.loadbalancer.stickiness=true` - Enable backend sticky sessions -- `traefik.backend.loadbalancer.stickiness.cookieName=NAME` - Manually set the cookie name for sticky sessions -- `traefik.backend.loadbalancer.sticky=true` - Enable backend sticky sessions (DEPRECATED) +### Security annotations -Additionally, an annotation can be used on Kubernetes services to set the [circuit breaker expression](/basics/#backends) for a backend. +The following security annotations are applicable on the Ingress object: -- `traefik.backend.circuitbreaker: ` - Set the circuit breaker expression for the backend. Default: `nil`. - -As known from nginx when used as Kubernetes Ingress Controller, a list of IP-Ranges which are allowed to access can be configured by using an ingress annotation: - -- `ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` - -An unset or empty list allows all Source-IPs to access. -If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. - -#### Security annotations - -The following security annotations can be applied to the ingress object to add security features: - -| Annotation | Description | -|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Annotation | Description | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ingress.kubernetes.io/allowed-hosts:EXPR` | Provides a list of allowed hosts that requests will be processed. Format: `Host1,Host2` | -| `ingress.kubernetes.io/custom-request-headers:EXPR ` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: HEADER:value||HEADER2:value2 | +| `ingress.kubernetes.io/custom-request-headers:EXPR` | Provides the container with custom request headers that will be appended to each request forwarded to the container. Format: HEADER:value||HEADER2:value2 | | `ingress.kubernetes.io/custom-response-headers:EXPR` | Appends the headers to each response returned by the container, before forwarding the response to the client. Format: HEADER:value||HEADER2:value2 | -| `ingress.kubernetes.io/proxy-headers:EXPR ` | Provides a list of headers that the proxied hostname may be stored. Format: `HEADER1,HEADER2` | +| `ingress.kubernetes.io/proxy-headers:EXPR` | Provides a list of headers that the proxied hostname may be stored. Format: `HEADER1,HEADER2` | | `ingress.kubernetes.io/ssl-redirect:true` | Forces the frontend to redirect to SSL if a non-SSL request is sent. | | `ingress.kubernetes.io/ssl-temporary-redirect:true` | Forces the frontend to redirect to SSL if a non-SSL request is sent, but by sending a 302 instead of a 301. | | `ingress.kubernetes.io/ssl-host:HOST` | This setting configures the hostname that redirects will be based on. Default is "", which is the same host as the request. | @@ -171,17 +164,17 @@ The following security annotations can be applied to the ingress object to add s ### Authentication -Is possible to add additional authentication annotations in the Ingress rule. -The source of the authentication is a secret that contains usernames and passwords inside the key auth. +Is possible to add additional authentication annotations to the Ingress object. +The source of the authentication is a Secret object that contains the credentials. - `ingress.kubernetes.io/auth-type`: `basic` -- `ingress.kubernetes.io/auth-secret`: `mysecret` - Contains the usernames and passwords with access to the paths defined in the Ingress Rule. + Contains the authentication type. The only permitted type is `basic`. +- `ingress.kubernetes.io/auth-secret`: `mysecret` + Contains the username and password with access to the paths defined in the Ingress object. -The secret must be created in the same namespace as the Ingress rule. +The secret must be created in the same namespace as the Ingress object. -Limitations: +The following limitations hold: -- Basic authentication only. -- Realm not configurable; only `traefik` default. -- Secret must contain only single file. +- The realm is not configurable; the only supported (and default) value is `traefik`. +- The Secret must contain a single file only. diff --git a/docs/configuration/backends/web.md b/docs/configuration/backends/web.md index 93cce9676..a5e185560 100644 --- a/docs/configuration/backends/web.md +++ b/docs/configuration/backends/web.md @@ -35,6 +35,15 @@ address = ":8080" # Default: false # readOnly = true + + +# Set the root path for webui and API +# +# Deprecated +# Optional +# +# path = "/mypath" +# ``` ## Web UI @@ -80,7 +89,7 @@ Users can be specified directly in the toml file, or indirectly by referencing a # To enable digest auth on the webui with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 [web.auth.digest] -users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] +users = ["test:traefik:a2688e031edb4be6a3797f3882655c05", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] usersFile = "/path/to/.htdigest" # ... @@ -375,3 +384,35 @@ curl -s "http://localhost:8080/api" | jq . } } ``` + +## Path + +As web is deprecated, you can handle the `Path` option like this + +```toml +[entrypoints.http] +address=":80" + +[entrypoints.dashboard] +address=":8080" + +[entrypoints.api] +address=":8081" + +#Activate API and Dashboard +[api] +entrypoint="api" + +[file] + [backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:8081" + + [frontends] + [frontends.frontend1] + entrypoints=["dashboard"] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "PathPrefixStrip:/yourprefix;PathPrefix:/yourprefix" +``` \ No newline at end of file diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index ab29a2142..a9b949f9b 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -135,8 +135,8 @@ Users can be specified directly in the toml file, or indirectly by referencing a [entryPoints] [entryPoints.http] address = ":80" - [entryPoints.http.auth.basic] - users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] + [entryPoints.http.auth.digest] + users = ["test:traefik:a2688e031edb4be6a3797f3882655c05", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] usersFile = "/path/to/.htdigest" ``` diff --git a/docs/index.md b/docs/index.md index 4ff7d5dcf..53791bccd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -108,7 +108,7 @@ version: '2' services: proxy: image: traefik - command: --web --docker --docker.domain=docker.localhost --logLevel=DEBUG + command: --api --docker --docker.domain=docker.localhost --logLevel=DEBUG networks: - webgateway ports: diff --git a/docs/user-guide/cluster-docker-consul.md b/docs/user-guide/cluster-docker-consul.md index 8b3f70c84..48c5de9e1 100644 --- a/docs/user-guide/cluster-docker-consul.md +++ b/docs/user-guide/cluster-docker-consul.md @@ -56,15 +56,18 @@ $ traefik \ --acme \ --acme.storage=/etc/traefik/acme/acme.json \ --acme.entryPoint=https \ + --acme.httpChallenge.entryPoint=http \ --acme.email=contact@mydomain.ca ``` -Let's Encrypt needs 3 parameters: an entry point to listen to, a storage for certificates, and an email for the registration. +Let's Encrypt needs 4 parameters: an TLS entry point to listen to, a non-TLS entry point to allow HTTP challenges, a storage for certificates, and an email for the registration. To enable Let's Encrypt support, you need to add `--acme` flag. Now, Træfik needs to know where to store the certificates, we can choose between a key in a Key-Value store, or a file path: `--acme.storage=my/key` or `--acme.storage=/path/to/acme.json`. +The `acme.httpChallenge.entryPoint` flag enables the `HTTP-01` challenge and specifies the entryPoint to use during the challenges. + For your email and the entry point, it's `--acme.entryPoint` and `--acme.email` flags. ### Docker configuration @@ -90,13 +93,14 @@ services: traefik: image: traefik:1.5 command: - - "--web" + - "--api" - "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https" - "--entrypoints=Name:https Address::443 TLS" - "--defaultentrypoints=http,https" - "--acme" - "--acme.storage=/etc/traefik/acme/acme.json" - "--acme.entryPoint=https" + - "--acme.httpChallenge.entryPoint=http" - "--acme.OnHostRule=true" - "--acme.onDemand=false" - "--acme.email=contact@mydomain.ca" @@ -155,7 +159,7 @@ The initializer in a docker-compose file will be: image: traefik:1.5 command: - "storeconfig" - - "--web" + - "--api" [...] - "--consul" - "--consul.endpoint=consul:8500" @@ -199,19 +203,20 @@ services: image: traefik:1.5 command: - "storeconfig" - - "--web" + - "--api" - "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https" - "--entrypoints=Name:https Address::443 TLS" - "--defaultentrypoints=http,https" - "--acme" - "--acme.storage=traefik/acme/account" - "--acme.entryPoint=https" + - "--acme.httpChallenge.entryPoint=http" - "--acme.OnHostRule=true" - "--acme.onDemand=false" - - "--acme.email=contact@jmaitrehenry.ca" + - "--acme.email=foobar@example.com" - "--docker" - "--docker.swarmmode" - - "--docker.domain=jmaitrehenry.ca" + - "--docker.domain=example.com" - "--docker.watch" - "--consul" - "--consul.endpoint=consul:8500" diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index 24c149016..cca881979 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -104,6 +104,8 @@ email = "your-email-here@my-awesome-app.org" storage = "acme.json" entryPoint = "https" OnHostRule = true +[acme.httpChallenge] +entryPoint = "http" ``` This is the minimum configuration required to do the following: diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 666b7c918..ff2813a6c 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -6,6 +6,7 @@ You will find here some configuration examples of Træfik. ```toml defaultEntryPoints = ["http"] + [entryPoints] [entryPoints.http] address = ":80" @@ -15,6 +16,7 @@ defaultEntryPoints = ["http"] ```toml defaultEntryPoints = ["http", "https"] + [entryPoints] [entryPoints.http] address = ":80" @@ -34,6 +36,7 @@ Note that we can either give path to certificate file or directly the file conte ```toml defaultEntryPoints = ["http", "https"] + [entryPoints] [entryPoints.http] address = ":80" @@ -52,10 +55,16 @@ defaultEntryPoints = ["http", "https"] ## Let's Encrypt support -### Basic example +!!! note + Even if `TLS-SNI-01` challenge is [disabled](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188), for the moment, it stays the _by default_ ACME Challenge in Træfik but all the examples use the `HTTP-01` challenge (except DNS challenge examples). + If `TLS-SNI-01` challenge is not re-enabled in the future, it we will be removed from Træfik. + +### Basic example with HTTP challenge ```toml [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] @@ -65,6 +74,8 @@ email = "test@traefik.io" storage = "acme.json" caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" + [acme.httpChallenge] + entryPoint = "http" [[acme.domains]] main = "local1.com" @@ -78,14 +89,16 @@ entryPoint = "https" main = "local4.com" ``` -This configuration allows generating Let's Encrypt certificates for the four domains `local[1-4].com` with described SANs. +This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com` with described SANs. Traefik generates these certificates when it starts and it needs to be restart if new domains are added. -### OnHostRule option +### OnHostRule option (with HTTP challenge) ```toml [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] @@ -96,6 +109,8 @@ storage = "acme.json" onHostRule = true caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" + [acme.httpChallenge] + entryPoint = "http" [[acme.domains]] main = "local1.com" @@ -109,16 +124,18 @@ entryPoint = "https" main = "local4.com" ``` -This configuration allows generating Let's Encrypt certificates for the four domains `local[1-4].com`. +This configuration allows generating Let's Encrypt certificates (thanks to `HTTP-01` challenge) for the four domains `local[1-4].com`. Traefik generates these certificates when it starts. If a backend is added with a `onHost` rule, Traefik will automatically generate the Let's Encrypt certificate for the new domain. -### OnDemand option +### OnDemand option (with HTTP challenge) ```toml [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] @@ -129,9 +146,11 @@ storage = "acme.json" onDemand = true caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" + [acme.httpChallenge] + entryPoint = "http" ``` -This configuration allows generating a Let's Encrypt certificate during the first HTTPS request on a new domain. +This configuration allows generating a Let's Encrypt certificate (thanks to `HTTP-01` challenge) during the first HTTPS request on a new domain. !!! note @@ -153,10 +172,11 @@ This configuration allows generating a Let's Encrypt certificate during the firs [acme] email = "test@traefik.io" storage = "acme.json" -dnsProvider = "digitalocean" # DNS Provider name (cloudflare, OVH, gandi...) -delayDontCheckDNS = 0 caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" + [acme.dnsChallenge] + provider = "digitalocean" # DNS Provider name (cloudflare, OVH, gandi...) + delayBeforeCheck = 0 [[acme.domains]] main = "local1.com" @@ -173,12 +193,14 @@ entryPoint = "https" DNS challenge needs environment variables to be executed. This variables have to be set on the machine/container which host Traefik. -These variables are described [in this section](/configuration/acme/#dnsprovider). +These variables are described [in this section](/configuration/acme/#provider). -### OnHostRule option and provided certificates +### OnHostRule option and provided certificates (with HTTP challenge) ```toml [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] @@ -192,10 +214,11 @@ storage = "acme.json" onHostRule = true caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" - + [acme.httpChallenge] + entryPoint = "http" ``` -Traefik will only try to generate a Let's encrypt certificate if the domain cannot be checked by the provided certificates. +Traefik will only try to generate a Let's encrypt certificate (thanks to `HTTP-01` challenge) if the domain cannot be checked by the provided certificates. ### Cluster mode @@ -207,6 +230,8 @@ Before you use Let's Encrypt in a Traefik cluster, take a look to [the key-value ```toml [entryPoints] + [entryPoints.http] + address = ":80" [entryPoints.https] address = ":443" [entryPoints.https.tls] @@ -217,6 +242,9 @@ storage = "traefik/acme/account" caServer = "http://172.18.0.1:4000/directory" entryPoint = "https" +[acme.httpChallenge] + entryPoint = "http" + [[acme.domains]] main = "local1.com" sans = ["test1.local1.com", "test2.local1.com"] @@ -244,10 +272,12 @@ The `consul` provider contains the configuration. ```toml [frontends] + [frontends.frontend1] backend = "backend2" [frontends.frontend1.routes.test_1] rule = "Host:test.localhost" + [frontends.frontend2] backend = "backend1" passHostHeader = true @@ -255,10 +285,11 @@ The `consul` provider contains the configuration. entrypoints = ["https"] # overrides defaultEntryPoints [frontends.frontend2.routes.test_1] rule = "Host:{subdomain:[a-z]+}.localhost" + [frontends.frontend3] entrypoints = ["http", "https"] # overrides defaultEntryPoints backend = "backend2" - rule = "Path:/test" + rule = "Path:/test" ``` ## Enable Basic authentication in an entrypoint @@ -272,6 +303,7 @@ Passwords are encoded in MD5: you can use htpasswd to generate those ones. ```toml defaultEntryPoints = ["http"] + [entryPoints] [entryPoints.http] address = ":80" @@ -286,6 +318,7 @@ via a configurable header value. ```toml defaultEntryPoints = ["http"] + [entryPoints] [entryPoints.http] address = ":80" @@ -306,7 +339,7 @@ idleTimeout = "360s" ## Securing Ping Health Check -The `/ping` health-check URL is enabled together with the web admin panel, enabled with the command-line `--web` or config file option `[web]`. +The `/ping` health-check URL is enabled with the command-line `--ping` or config file option `[ping]`. Thus, if you have a regular path for `/foo` and an entrypoint on `:80`, you would access them as follows: * Regular path: `http://hostname:80/foo` diff --git a/docs/user-guide/grpc.md b/docs/user-guide/grpc.md index f4f609d92..02c9531a8 100644 --- a/docs/user-guide/grpc.md +++ b/docs/user-guide/grpc.md @@ -57,8 +57,7 @@ RootCAs = [ "./backend.cert" ] keyFile = "./frontend.key" -[web] - address = ":8080" +[api] [file] diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 94a9aa238..7c7294b66 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -1,6 +1,6 @@ # Kubernetes Ingress Controller -This guide explains how to use Træfik as an Ingress controller in a Kubernetes cluster. +This guide explains how to use Træfik as an Ingress controller for a Kubernetes cluster. If you are not familiar with Ingresses in Kubernetes you might want to read the [Kubernetes user guide](https://kubernetes.io/docs/concepts/services-networking/ingress/) @@ -8,8 +8,10 @@ The config files used in this guide can be found in the [examples directory](htt ## Prerequisites -1. A working Kubernetes cluster. If you want to follow along with this guide, you should setup [minikube](https://kubernetes.io/docs/getting-started-guides/minikube/) -on your machine, as it is the quickest way to get a local Kubernetes cluster setup for experimentation and development. +1. A working Kubernetes cluster. If you want to follow along with this guide, you should setup [minikube](https://kubernetes.io/docs/getting-started-guides/minikube/) on your machine, as it is the quickest way to get a local Kubernetes cluster setup for experimentation and development. + +!!! note + The guide is likely not fully adequate for a production-ready setup. 2. The `kubectl` binary should be [installed on your workstation](https://kubernetes.io/docs/getting-started-guides/minikube/#download-kubectl). @@ -79,8 +81,8 @@ For namespaced restrictions, one RoleBinding is required per watched namespace a It is possible to use Træfik with a [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or a [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) object, whereas both options have their own pros and cons: -- The scalability is much better when using a Deployment, because you will have a Single-Pod-per-Node model when using the DeaemonSet. -- It is possible to exclusively run a Service on a dedicated set of machines using taints and tolerations with a DaemonSet. +- The scalability is much better when using a Deployment, because you will have a Single-Pod-per-Node model when using the DeaemonSet. +- It is possible to exclusively run a Service on a dedicated set of machines using taints and tolerations with a DaemonSet. - On the other hand the DaemonSet allows you to access any Node directly on Port 80 and 443, where you have to setup a [Service](https://kubernetes.io/docs/concepts/services-networking/service/) object with a Deployment. The Deployment objects looks like this: @@ -117,7 +119,7 @@ spec: - image: traefik name: traefik-ingress-lb args: - - --web + - --api - --kubernetes --- kind: Service @@ -137,6 +139,7 @@ spec: name: admin type: NodePort ``` + [examples/k8s/traefik-deployment.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik-deployment.yaml) !!! note @@ -182,7 +185,7 @@ spec: privileged: true args: - -d - - --web + - --api - --kubernetes --- kind: Service @@ -233,7 +236,7 @@ Start by listing the pods in the `kube-system` namespace: kubectl --namespace=kube-system get pods ``` -``` +```shell NAME READY STATUS RESTARTS AGE kube-addon-manager-minikubevm 1/1 Running 0 4h kubernetes-dashboard-s8krj 1/1 Running 0 4h @@ -250,19 +253,21 @@ _It might take a few moments for kubernetes to pull the Træfik image and start You should now be able to access Træfik on port 80 of your Minikube instance when using the DaemonSet: -```sh +```shell curl $(minikube ip) ``` -``` + +```shell 404 page not found ``` If you decided to use the deployment, then you need to target the correct NodePort, which can be seen when you execute `kubectl get services --namespace=kube-system`. -```sh +```shell curl $(minikube ip): ``` -``` + +```shell 404 page not found ``` @@ -273,19 +278,20 @@ All further examples below assume a DaemonSet installation. Deployment users wil ## Deploy Træfik using Helm Chart -Instead of installing Træfik via an own object, you can also use the Træfik Helm chart. +!!! note + The Helm Chart is maintained by the community, not the Traefik project maintainers. -This allows more complex configuration via Kubernetes [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configmap/) and enabled TLS certificates. +Instead of installing Træfik via Kubernetes object directly, you can also use the Træfik Helm chart. -Install Træfik chart by: +Install the Træfik chart by: ```shell helm install stable/traefik ``` -For more information, check out [the doc](https://github.com/kubernetes/charts/tree/master/stable/traefik). +For more information, check out [the documentation](https://github.com/kubernetes/charts/tree/master/stable/traefik). -## Submitting An Ingress to the cluster. +## Submitting an Ingress to the Cluster Lets start by creating a Service and an Ingress that will expose the [Træfik Web UI](https://github.com/containous/traefik#web-ui). @@ -318,22 +324,23 @@ spec: serviceName: traefik-web-ui servicePort: 80 ``` + [examples/k8s/ui.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/ui.yaml) ```shell kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/ui.yaml ``` -Now lets setup an entry in our /etc/hosts file to route `traefik-ui.minikube` to our cluster. +Now lets setup an entry in our `/etc/hosts` file to route `traefik-ui.minikube` to our cluster. -In production you would want to set up real dns entries. -You can get the ip address of your minikube instance by running `minikube ip` +In production you would want to set up real DNS entries. +You can get the IP address of your minikube instance by running `minikube ip`: ```shell echo "$(minikube ip) traefik-ui.minikube" | sudo tee -a /etc/hosts ``` -We should now be able to visit [traefik-ui.minikube](http://traefik-ui.minikube) in the browser and view the Træfik Web UI. +We should now be able to visit [traefik-ui.minikube](http://traefik-ui.minikube) in the browser and view the Træfik web UI. ### Add a TLS Certificate to the Ingress @@ -382,11 +389,9 @@ If there are any errors while loading the TLS section of an ingress, the whole i ## Basic Authentication -It's possible to add additional authentication annotations in the Ingress rule. -The source of the authentication is a secret that contains usernames and passwords inside the key auth. -To read about basic auth limitations see the [Kubernetes Ingress](/configuration/backends/kubernetes) configuration page. +It's possible to protect access to Traefik through basic authentication. (See the [Kubernetes Ingress](/configuration/backends/kubernetes) configuration page for syntactical details and restrictions.) -#### Creating the Secret +### Creating the Secret A. Use `htpasswd` to create a file containing the username and the base64-encoded password: @@ -400,25 +405,28 @@ You will be prompted for a password which you will have to enter twice. ```shell cat auth ``` -``` + +```shell myusername:$apr1$78Jyn/1K$ERHKVRPPlzAX8eBtLuvRZ0 ``` -B. Now use `kubectl` to create a secret in the monitoring namespace using the file created by `htpasswd`. +B. Now use `kubectl` to create a secret in the `monitoring` namespace using the file created by `htpasswd`. ```shell kubectl create secret generic mysecret --from-file auth --namespace=monitoring ``` !!! note - Secret must be in same namespace as the ingress rule. + Secret must be in same namespace as the Ingress object. -C. Create the ingress using the following annotations to specify basic auth and that the username and password is stored in `mysecret`. +C. Attach the following annotations to the Ingress object: - `ingress.kubernetes.io/auth-type: "basic"` - `ingress.kubernetes.io/auth-secret: "mysecret"` -Following is a full ingress example based on Prometheus: +They specify basic authentication and reference the Secret `mysecret` containing the credentials. + +Following is a full Ingress example based on Prometheus: ```yaml apiVersion: extensions/v1beta1 @@ -440,17 +448,17 @@ spec: servicePort: 9090 ``` -You can apply the example ingress as following: +You can apply the example as following: ```shell kubectl create -f prometheus-ingress.yaml -n monitoring ``` -## Name based routing +## Name-based Routing -In this example we are going to setup websites for 3 of the United Kingdoms best loved cheeses, Cheddar, Stilton and Wensleydale. +In this example we are going to setup websites for three of the United Kingdoms best loved cheeses: Cheddar, Stilton, and Wensleydale. -First lets start by launching the 3 pods for the cheese websites. +First lets start by launching the pods for the cheese websites. ```yaml --- @@ -532,13 +540,14 @@ spec: ports: - containerPort: 80 ``` + [examples/k8s/cheese-deployments.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-deployments.yaml) ```shell kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-deployments.yaml ``` -Next we need to setup a service for each of the cheese pods. +Next we need to setup a Service for each of the cheese pods. ```yaml --- @@ -587,7 +596,6 @@ spec: !!! note We also set a [circuit breaker expression](/basics/#backends) for one of the backends by setting the `traefik.backend.circuitbreaker` annotation on the service. - [examples/k8s/cheese-services.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-services.yaml) ```shell @@ -627,6 +635,7 @@ spec: serviceName: wensleydale servicePort: http ``` + [examples/k8s/cheese-ingress.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-ingress.yaml) !!! note @@ -637,7 +646,7 @@ kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/exa ``` Now visit the [Træfik dashboard](http://traefik-ui.minikube/) and you should see a frontend for each host. -Along with a backend listing for each service with a Server set up for each pod. +Along with a backend listing for each service with a server set up for each pod. If you edit your `/etc/hosts` again you should be able to access the cheese websites in your browser. @@ -645,11 +654,11 @@ If you edit your `/etc/hosts` again you should be able to access the cheese webs echo "$(minikube ip) stilton.minikube cheddar.minikube wensleydale.minikube" | sudo tee -a /etc/hosts ``` -* [Stilton](http://stilton.minikube/) -* [Cheddar](http://cheddar.minikube/) -* [Wensleydale](http://wensleydale.minikube/) +- [Stilton](http://stilton.minikube/) +- [Cheddar](http://cheddar.minikube/) +- [Wensleydale](http://wensleydale.minikube/) -## Path based routing +## Path-based Routing Now lets suppose that our fictional client has decided that while they are super happy about our cheesy web design, when they asked for 3 websites they had not really bargained on having to buy 3 domain names. @@ -681,10 +690,11 @@ spec: serviceName: wensleydale servicePort: http ``` + [examples/k8s/cheeses-ingress.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheeses-ingress.yaml) !!! note - we are configuring Træfik to strip the prefix from the url path with the `traefik.frontend.rule.type` annotation so that we can use the containers from the previous example without modification. + We are configuring Træfik to strip the prefix from the url path with the `traefik.frontend.rule.type` annotation so that we can use the containers from the previous example without modification. ```shell kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheeses-ingress.yaml @@ -696,14 +706,14 @@ echo "$(minikube ip) cheeses.minikube" | sudo tee -a /etc/hosts You should now be able to visit the websites in your browser. -* [cheeses.minikube/stilton](http://cheeses.minikube/stilton/) -* [cheeses.minikube/cheddar](http://cheeses.minikube/cheddar/) -* [cheeses.minikube/wensleydale](http://cheeses.minikube/wensleydale/) +- [cheeses.minikube/stilton](http://cheeses.minikube/stilton/) +- [cheeses.minikube/cheddar](http://cheeses.minikube/cheddar/) +- [cheeses.minikube/wensleydale](http://cheeses.minikube/wensleydale/) -## Specifying priority for routing +## Specifying Routing Priorities -Sometimes you need to specify priority for ingress route, especially when handling wildcard routes. -This can be done by adding annotation `traefik.frontend.priority`, i.e.: +Sometimes you need to specify priority for ingress routes, especially when handling wildcard routes. +This can be done by adding the `traefik.frontend.priority` annotation, i.e.: ```yaml apiVersion: extensions/v1beta1 @@ -738,34 +748,33 @@ spec: servicePort: http ``` -Note that priority values must be quoted to avoid them being interpreted as numbers (which are illegal for annotations). +Note that priority values must be quoted to avoid numeric interpretation (which are illegal for annotations). ## Forwarding to ExternalNames When specifying an [ExternalName](https://kubernetes.io/docs/concepts/services-networking/service/#services-without-selectors), -Træfik will forward requests to the given host accordingly and use HTTPS when the Service port matches 443. +Træfik will forward requests to the given host accordingly and use HTTPS when the Service port matches 443. This still requires setting up a proper port mapping on the Service from the Ingress port to the (external) Service port. -## Disable passing the Host header +## Disable passing the Host Header -By default Træfik will pass the incoming Host header on to the upstream resource. +By default Træfik will pass the incoming Host header to the upstream resource. -There are times however where you may not want this to be the case. -For example if your service is of the ExternalName type. +However, there are times when you may not want this to be the case. For example, if your service is of the ExternalName type. -### Disable entirely +### Disable globally -Add the following to your toml config: +Add the following to your TOML configuration file: ```toml disablePassHostHeaders = true ``` -### Disable per ingress +### Disable per Ingress -To disable passing the Host header per ingress resource set the `traefik.frontend.passHostHeader` annotation on your ingress to `false`. +To disable passing the Host header per ingress resource set the `traefik.frontend.passHostHeader` annotation on your ingress to `"false"`. -Here is an example ingress definition: +Here is an example definition: ```yaml apiVersion: extensions/v1beta1 @@ -801,12 +810,11 @@ spec: externalName: static.otherdomain.com ``` -If you were to visit `example.com/static` the request would then be passed onto `static.otherdomain.com/static` and s`tatic.otherdomain.com` would receive the request with the Host header being `static.otherdomain.com`. +If you were to visit `example.com/static` the request would then be passed on to `static.otherdomain.com/static`, and `static.otherdomain.com` would receive the request with the Host header being `static.otherdomain.com`. !!! note - The per ingress annotation overides whatever the global value is set to. - So you could set `disablePassHostHeaders` to `true` in your toml file and then enable passing - the host header per ingress if you wanted. + The per-ingress annotation overrides whatever the global value is set to. + So you could set `disablePassHostHeaders` to `true` in your TOML configuration file and then enable passing the host header per ingress if you wanted. ## Partitioning the Ingress object space diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index ef38e0629..07e6ed777 100644 --- a/docs/user-guide/kv-config.md +++ b/docs/user-guide/kv-config.md @@ -70,10 +70,13 @@ logLevel = "DEBUG" defaultEntryPoints = ["http", "https"] [entryPoints] + [entryPoints.api] + address = ":8081" [entryPoints.http] address = ":80" [entryPoints.https] address = ":443" + [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] certFile = "integration/fixtures/https/snitest.com.cert" @@ -94,8 +97,8 @@ defaultEntryPoints = ["http", "https"] watch = true prefix = "traefik" -[web] - address = ":8081" +[api] + entrypoint = "api" ``` And there, the same global configuration in the Key-value Store (using `prefix = "traefik"`): @@ -105,6 +108,7 @@ And there, the same global configuration in the Key-value Store (using `prefix = | `/traefik/loglevel` | `DEBUG` | | `/traefik/defaultentrypoints/0` | `http` | | `/traefik/defaultentrypoints/1` | `https` | +| `/traefik/entrypoints/api/address` | `:8081` | | `/traefik/entrypoints/http/address` | `:80` | | `/traefik/entrypoints/https/address` | `:443` | | `/traefik/entrypoints/https/tls/certificates/0/certfile` | `integration/fixtures/https/snitest.com.cert` | @@ -115,7 +119,7 @@ And there, the same global configuration in the Key-value Store (using `prefix = | `/traefik/consul/endpoint` | `127.0.0.1:8500` | | `/traefik/consul/watch` | `true` | | `/traefik/consul/prefix` | `traefik` | -| `/traefik/web/address` | `:8081` | +| `/traefik/api/entrypoint` | `api` | In case you are setting key values manually: diff --git a/docs/user-guide/swarm-mode.md b/docs/user-guide/swarm-mode.md index f9a4cc1f3..ac74036f7 100644 --- a/docs/user-guide/swarm-mode.md +++ b/docs/user-guide/swarm-mode.md @@ -90,7 +90,7 @@ docker-machine ssh manager "docker service create \ --docker.swarmmode \ --docker.domain=traefik \ --docker.watch \ - --web" + --api" ``` Let's explain this command: @@ -102,7 +102,7 @@ Let's explain this command: | `--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock` | we bind mount the docker socket where Træfik is scheduled to be able to speak to the daemon. | | `--network traefik-net` | we attach the Træfik service (and thus the underlying container) to the `traefik-net` network. | | `--docker` | enable docker backend, and `--docker.swarmmode` to enable the swarm mode on Træfik. | -| `--web` | activate the webUI on port 8080 | +| `--api | activate the webUI on port 8080 | ## Deploy your apps diff --git a/docs/user-guide/swarm.md b/docs/user-guide/swarm.md index 3d9c7df8c..0cf7296e1 100644 --- a/docs/user-guide/swarm.md +++ b/docs/user-guide/swarm.md @@ -93,7 +93,7 @@ docker $(docker-machine config mhs-demo0) run \ --docker.tls.key=/ssl/server-key.pem \ --docker.tls.insecureSkipVerify \ --docker.watch \ - --web + --api ``` Let's explain this command: @@ -107,7 +107,7 @@ Let's explain this command: | `--docker` | enable docker backend | | `--docker.endpoint=tcp://172.18.0.1:2376` | connect to the swarm master using the docker_gwbridge network | | `--docker.tls` | enable TLS using the docker-machine keys | -| `--web` | activate the webUI on port 8080 | +| `--api` | activate the webUI on port 8080 | ## Deploy your apps diff --git a/examples/acme/acme.toml b/examples/acme/acme.toml index c6f6348ba..151ffb54c 100644 --- a/examples/acme/acme.toml +++ b/examples/acme/acme.toml @@ -19,6 +19,8 @@ entryPoint = "https" onDemand = false OnHostRule = true caServer = "http://traefik.localhost.com:4000/directory" +[acme.httpChallenge] +entryPoint="http" [web] diff --git a/examples/acme/compose-acme.yml b/examples/acme/compose-acme.yml index f9e9c32d0..c3c6317e3 100644 --- a/examples/acme/compose-acme.yml +++ b/examples/acme/compose-acme.yml @@ -78,6 +78,7 @@ services : - "80:80" - "443:443" - "5001:443" # Needed for SNI challenge + - "5002:80" # Needed for HTTP challenge expose: - "8080" labels: diff --git a/examples/cluster/docker-compose.yml b/examples/cluster/docker-compose.yml index c45102874..0f9640f6b 100644 --- a/examples/cluster/docker-compose.yml +++ b/examples/cluster/docker-compose.yml @@ -136,11 +136,13 @@ services: expose: - "443" - "5001" + - "5002" ports: - "80:80" - "8080:8080" - "443:443" - "5001:443" # Needed for SNI challenge + - "5002:80" # Needed for HTTP challenge networks: net: ipv4_address: 10.0.1.8 @@ -157,6 +159,7 @@ services: expose: - "443" - "5001" + - "5002" ports: - "88:80" - "8888:8080" diff --git a/examples/cluster/traefik.toml.tmpl b/examples/cluster/traefik.toml.tmpl index 2569e66d8..891e994b1 100644 --- a/examples/cluster/traefik.toml.tmpl +++ b/examples/cluster/traefik.toml.tmpl @@ -15,6 +15,8 @@ storage = "traefik/acme/account" entryPoint = "https" OnHostRule = true caServer = "http://traefik.boulder.com:4000/directory" +[acme.httpChallenge] +entryPoint="http" [web] diff --git a/healthcheck/healthcheck.go b/healthcheck/healthcheck.go index c77ee86c4..3dabe95a5 100644 --- a/healthcheck/healthcheck.go +++ b/healthcheck/healthcheck.go @@ -49,6 +49,7 @@ type BackendHealthCheck struct { //HealthCheck struct type HealthCheck struct { + mutex sync.Mutex Backends map[string]*BackendHealthCheck cancel context.CancelFunc } @@ -77,14 +78,16 @@ func NewBackendHealthCheck(options Options, backendName string) *BackendHealthCh //SetBackendsConfiguration set backends configuration func (hc *HealthCheck) SetBackendsConfiguration(parentCtx context.Context, backends map[string]*BackendHealthCheck) { + hc.mutex.Lock() hc.Backends = backends if hc.cancel != nil { hc.cancel() } ctx, cancel := context.WithCancel(parentCtx) hc.cancel = cancel + hc.mutex.Unlock() - for _, backend := range hc.Backends { + for _, backend := range backends { currentBackend := backend safe.Go(func() { hc.execute(ctx, currentBackend) diff --git a/integration/acme_test.go b/integration/acme_test.go index 7a7c31604..078d9c230 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -72,6 +72,26 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificate(c *check.C) { s.retrieveAcmeCertificate(c, testCase) } +// Test OnDemand option with none provided certificate and challenge HTTP-01 +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_http01.toml", + onDemand: true, + domainToCheck: acmeDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test OnHostRule option with none provided certificate and challenge HTTP-01 +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_http01.toml", + onDemand: false, + domainToCheck: acmeDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + // Test OnDemand option with a wildcard provided certificate func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C) { testCase := AcmeTestCase{ diff --git a/integration/consul_test.go b/integration/consul_test.go index f55340617..d2178e47e 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -294,7 +294,10 @@ func (s *ConsulSuite) skipTestGlobalConfigurationWithClientTLS(c *check.C) { s.setupConsulTLS(c) consulHost := s.composeProject.Container(c, "consul").NetworkSettings.IPAddress - err := s.kv.Put("traefik/web/address", []byte(":8081"), nil) + err := s.kv.Put("traefik/api/entrypoint", []byte("api"), nil) + c.Assert(err, checker.IsNil) + + err = s.kv.Put("traefik/entrypoints/api/address", []byte(":8081"), nil) c.Assert(err, checker.IsNil) // wait for consul @@ -341,7 +344,7 @@ func (s *ConsulSuite) TestCommandStoreConfig(c *check.C) { "/traefik/loglevel": "DEBUG", "/traefik/defaultentrypoints/0": "http", "/traefik/entrypoints/http/address": ":8000", - "/traefik/web/address": ":8080", + "/traefik/api/entrypoint": "traefik", "/traefik/consul/endpoint": consulHost + ":8500", } diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index 4d55948db..9c4dd8ed0 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -411,7 +411,7 @@ func (s *Etcd3Suite) TestCommandStoreConfig(c *check.C) { "/traefik/loglevel": "DEBUG", "/traefik/defaultentrypoints/0": "http", "/traefik/entrypoints/http/address": ":8000", - "/traefik/web/address": ":8080", + "/traefik/api/entrypoint": "traefik", "/traefik/etcd/endpoint": ipEtcd + ":4001", } diff --git a/integration/etcd_test.go b/integration/etcd_test.go index 24d57884b..856d3814a 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -419,7 +419,7 @@ func (s *EtcdSuite) TestCommandStoreConfig(c *check.C) { "/traefik/loglevel": "DEBUG", "/traefik/defaultentrypoints/0": "http", "/traefik/entrypoints/http/address": ":8000", - "/traefik/web/address": ":8080", + "/traefik/api/entrypoint": "traefik", "/traefik/etcd/endpoint": etcdHost + ":4001", } diff --git a/integration/fixtures/acme/acme_http01.toml b/integration/fixtures/acme/acme_http01.toml new file mode 100644 index 000000000..1e4f354fe --- /dev/null +++ b/integration/fixtures/acme/acme_http01.toml @@ -0,0 +1,35 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":5002" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + + +[acme] +email = "test@traefik.io" +storage = "/dev/null" +entryPoint = "https" +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} +caServer = "http://{{.BoulderHost}}:4000/directory" +[acme.httpchallenge] +entrypoint="http" + +[file] + +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" diff --git a/integration/fixtures/consul/simple.toml b/integration/fixtures/consul/simple.toml index e4bbb1a44..e6db89985 100644 --- a/integration/fixtures/consul/simple.toml +++ b/integration/fixtures/consul/simple.toml @@ -5,6 +5,8 @@ logLevel = "DEBUG" [entryPoints] [entryPoints.http] address = ":8000" + [entryPoints.api] + address = ":8081" [consul] @@ -12,5 +14,5 @@ logLevel = "DEBUG" watch = true prefix = "traefik" -[web] - address = ":8081" +[api] + entryPoint = "api" diff --git a/integration/fixtures/consul/simple_https.toml b/integration/fixtures/consul/simple_https.toml index 21326e66f..bc4b235cd 100644 --- a/integration/fixtures/consul/simple_https.toml +++ b/integration/fixtures/consul/simple_https.toml @@ -3,6 +3,8 @@ defaultEntryPoints = ["http","https"] logLevel = "DEBUG" [entryPoints] + [entryPoints.api] + address = ":8081" [entryPoints.http] address = ":8000" [entryPoints.https] @@ -16,5 +18,5 @@ logLevel = "DEBUG" prefix = "traefik" watch = true -[web] - address = ":8081" +[api] + entryPoint = "api" diff --git a/integration/fixtures/consul_catalog/simple.toml b/integration/fixtures/consul_catalog/simple.toml index 45bca454f..2912f2595 100644 --- a/integration/fixtures/consul_catalog/simple.toml +++ b/integration/fixtures/consul_catalog/simple.toml @@ -1,8 +1,7 @@ defaultEntryPoints = ["http"] logLevel = "DEBUG" -[web] - address = ":8080" +[api] [entryPoints] [entryPoints.http] diff --git a/integration/fixtures/docker/simple.toml b/integration/fixtures/docker/simple.toml index 804c6f3cc..d0640b357 100644 --- a/integration/fixtures/docker/simple.toml +++ b/integration/fixtures/docker/simple.toml @@ -6,7 +6,7 @@ logLevel = "DEBUG" [entryPoints.http] address = ":8000" -[web] +[api] [docker] diff --git a/integration/fixtures/dynamodb/simple.toml b/integration/fixtures/dynamodb/simple.toml index 38c162f4f..cc8e2af75 100644 --- a/integration/fixtures/dynamodb/simple.toml +++ b/integration/fixtures/dynamodb/simple.toml @@ -5,6 +5,8 @@ logLevel = "DEBUG" [entryPoints] [entryPoints.http] address = ":8080" + [entryPoints.api] + address = ":8081" [dynamodb] AccessKeyID = "key" @@ -12,5 +14,5 @@ logLevel = "DEBUG" Endpoint = "{{.DynamoURL}}" Region = "us-east-1" -[web] - address = ":8081" +[api] + entryPoint = "api" diff --git a/integration/fixtures/etcd/simple.toml b/integration/fixtures/etcd/simple.toml index 7e43665e4..1b3c333c8 100644 --- a/integration/fixtures/etcd/simple.toml +++ b/integration/fixtures/etcd/simple.toml @@ -5,6 +5,8 @@ logLevel = "DEBUG" [entryPoints] [entryPoints.http] address = ":8000" + [entryPoints.api] + address = ":8081" [etcd] @@ -13,5 +15,5 @@ logLevel = "DEBUG" watch = true useAPIV3 = {{.UseAPIV3}} -[web] - address = ":8081" +[api] + entryPoint = "api" \ No newline at end of file diff --git a/integration/fixtures/etcd/simple_https.toml b/integration/fixtures/etcd/simple_https.toml index b5d93290d..c964da3d9 100644 --- a/integration/fixtures/etcd/simple_https.toml +++ b/integration/fixtures/etcd/simple_https.toml @@ -3,6 +3,8 @@ defaultEntryPoints = ["http","https"] logLevel = "DEBUG" [entryPoints] + [entryPoints.api] + address = ":8081" [entryPoints.http] address = ":8000" [entryPoints.https] @@ -16,5 +18,6 @@ logLevel = "DEBUG" # prefix = "/traefik" # watch = true -[web] - address = ":8081" + +[api] + entryPoint = "api" \ No newline at end of file diff --git a/integration/fixtures/eureka/simple.toml b/integration/fixtures/eureka/simple.toml index 8a9ffd94c..e026549ce 100644 --- a/integration/fixtures/eureka/simple.toml +++ b/integration/fixtures/eureka/simple.toml @@ -10,5 +10,4 @@ logLevel = "DEBUG" [eureka] endpoint = "http://{{.EurekaHost}}:8761/eureka" delay = "1s" -[web] - address = ":8080" +[api] diff --git a/integration/fixtures/grpc/config.toml b/integration/fixtures/grpc/config.toml index 4a37aa80f..fa5231b6f 100644 --- a/integration/fixtures/grpc/config.toml +++ b/integration/fixtures/grpc/config.toml @@ -11,8 +11,7 @@ RootCAs = [ """{{ .CertContent }}""" ] keyFile = """{{ .KeyContent }}""" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/grpc/config_insecure.toml b/integration/fixtures/grpc/config_insecure.toml index 524ab20b7..bffc3d50d 100644 --- a/integration/fixtures/grpc/config_insecure.toml +++ b/integration/fixtures/grpc/config_insecure.toml @@ -11,8 +11,7 @@ InsecureSkipVerify = true keyFile = """{{ .KeyContent }}""" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/healthcheck/multiple-entrypoints-drr.toml b/integration/fixtures/healthcheck/multiple-entrypoints-drr.toml index 7ebf72621..4489c5749 100644 --- a/integration/fixtures/healthcheck/multiple-entrypoints-drr.toml +++ b/integration/fixtures/healthcheck/multiple-entrypoints-drr.toml @@ -8,8 +8,7 @@ logLevel = "DEBUG" [entryPoints.http2] address = ":9000" -[web] - address = ":8080" +[api] [file] [backends] diff --git a/integration/fixtures/healthcheck/multiple-entrypoints-wrr.toml b/integration/fixtures/healthcheck/multiple-entrypoints-wrr.toml index c6e4ddfe7..beb8bdb55 100644 --- a/integration/fixtures/healthcheck/multiple-entrypoints-wrr.toml +++ b/integration/fixtures/healthcheck/multiple-entrypoints-wrr.toml @@ -8,8 +8,7 @@ logLevel = "DEBUG" [entryPoints.http2] address = ":9000" -[web] - address = ":8080" +[api] [file] [backends] diff --git a/integration/fixtures/healthcheck/port_overload.toml b/integration/fixtures/healthcheck/port_overload.toml index b3c76db6c..3619988b6 100644 --- a/integration/fixtures/healthcheck/port_overload.toml +++ b/integration/fixtures/healthcheck/port_overload.toml @@ -6,8 +6,7 @@ logLevel = "DEBUG" [entryPoints.http] address = ":8000" -[web] - address = ":8080" +[api] [file] [backends] diff --git a/integration/fixtures/healthcheck/simple.toml b/integration/fixtures/healthcheck/simple.toml index 7cdf9c4a8..b28cce1b3 100644 --- a/integration/fixtures/healthcheck/simple.toml +++ b/integration/fixtures/healthcheck/simple.toml @@ -6,8 +6,7 @@ logLevel = "DEBUG" [entryPoints.http] address = ":8000" -[web] - address = ":8080" +[api] [file] [backends] diff --git a/integration/fixtures/https/clientca/https_1ca1config.toml b/integration/fixtures/https/clientca/https_1ca1config.toml index 320796be1..c85f4cd95 100644 --- a/integration/fixtures/https/clientca/https_1ca1config.toml +++ b/integration/fixtures/https/clientca/https_1ca1config.toml @@ -16,8 +16,7 @@ defaultEntryPoints = ["https"] certFile = "fixtures/https/snitest.org.cert" keyFile = "fixtures/https/snitest.org.key" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/clientca/https_2ca1config.toml b/integration/fixtures/https/clientca/https_2ca1config.toml index 741554de7..5b8c92617 100644 --- a/integration/fixtures/https/clientca/https_2ca1config.toml +++ b/integration/fixtures/https/clientca/https_2ca1config.toml @@ -15,8 +15,7 @@ defaultEntryPoints = ["https"] certFile = "fixtures/https/snitest.org.cert" keyFile = "fixtures/https/snitest.org.key" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/clientca/https_2ca2config.toml b/integration/fixtures/https/clientca/https_2ca2config.toml index fe904e245..4e3dff814 100644 --- a/integration/fixtures/https/clientca/https_2ca2config.toml +++ b/integration/fixtures/https/clientca/https_2ca2config.toml @@ -16,8 +16,7 @@ defaultEntryPoints = ["https"] certFile = "fixtures/https/snitest.org.cert" keyFile = "fixtures/https/snitest.org.key" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/dynamic_https_sni.toml b/integration/fixtures/https/dynamic_https_sni.toml index 3c5b84da4..d7a023e6d 100644 --- a/integration/fixtures/https/dynamic_https_sni.toml +++ b/integration/fixtures/https/dynamic_https_sni.toml @@ -10,8 +10,7 @@ defaultEntryPoints = ["https"] address = ":8443" [entryPoints.https02.tls] -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/https_sni.toml b/integration/fixtures/https/https_sni.toml index 855a69ba3..fc0b86a13 100644 --- a/integration/fixtures/https/https_sni.toml +++ b/integration/fixtures/https/https_sni.toml @@ -13,8 +13,7 @@ defaultEntryPoints = ["https"] certFile = "fixtures/https/snitest.org.cert" keyFile = "fixtures/https/snitest.org.key" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/rootcas/https.toml b/integration/fixtures/https/rootcas/https.toml index 8466b2f57..07e3b6203 100644 --- a/integration/fixtures/https/rootcas/https.toml +++ b/integration/fixtures/https/rootcas/https.toml @@ -24,8 +24,7 @@ fblo6RBxUQ== [entryPoints.http] address = ":8081" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/https/rootcas/https_with_file.toml b/integration/fixtures/https/rootcas/https_with_file.toml index a95571a36..c0815c37f 100644 --- a/integration/fixtures/https/rootcas/https_with_file.toml +++ b/integration/fixtures/https/rootcas/https_with_file.toml @@ -9,8 +9,7 @@ RootCAs = [ "fixtures/https/rootcas/local.crt"] [entryPoints.http] address = ":8081" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/log_rotation_config.toml b/integration/fixtures/log_rotation_config.toml index a77392f30..78a21d9e2 100644 --- a/integration/fixtures/log_rotation_config.toml +++ b/integration/fixtures/log_rotation_config.toml @@ -8,14 +8,16 @@ defaultEntryPoints = ["http"] [entryPoints] [entryPoints.http] address = ":8000" + [entryPoints.api] + address = ":7888" checkNewVersion = false ################################################################ -# Web configuration backend +# Api configuration backend ################################################################ -[web] -address = ":7888" +[api] +entryPoint = "api" ################################################################ # File configuration backend diff --git a/integration/fixtures/marathon/simple.toml b/integration/fixtures/marathon/simple.toml index c388ec5b9..f737e7d38 100644 --- a/integration/fixtures/marathon/simple.toml +++ b/integration/fixtures/marathon/simple.toml @@ -5,9 +5,11 @@ logLevel = "DEBUG" [entryPoints] [entryPoints.http] address = ":8000" +[entryPoints.api] + address = ":9090" -[web] -address = ":9090" +[api] + entryPoint = "api" [marathon] endpoint = "{{.MarathonURL}}" diff --git a/integration/fixtures/proxy-protocol/with.toml b/integration/fixtures/proxy-protocol/with.toml index 68f813994..5520e9f30 100644 --- a/integration/fixtures/proxy-protocol/with.toml +++ b/integration/fixtures/proxy-protocol/with.toml @@ -7,8 +7,7 @@ address = ":8000" [entryPoints.http.proxyProtocol] trustedIPs = ["{{.HaproxyIP}}"] -[web] -address = ":8080" +[api] [file] diff --git a/integration/fixtures/proxy-protocol/without.toml b/integration/fixtures/proxy-protocol/without.toml index 3188c64f7..40d7f7902 100644 --- a/integration/fixtures/proxy-protocol/without.toml +++ b/integration/fixtures/proxy-protocol/without.toml @@ -7,8 +7,7 @@ address = ":8000" [entryPoints.http.proxyProtocol] trustedIPs = ["1.2.3.4"] -[web] -address = ":8080" +[api] [file] diff --git a/integration/fixtures/simple_web.toml b/integration/fixtures/simple_web.toml index b05590cf2..55b88480e 100644 --- a/integration/fixtures/simple_web.toml +++ b/integration/fixtures/simple_web.toml @@ -5,5 +5,4 @@ defaultEntryPoints = ["http"] [entryPoints.http] address = ":8000" -[web] -address = ":8080" +[api] diff --git a/integration/fixtures/timeout/forwarding_timeouts.toml b/integration/fixtures/timeout/forwarding_timeouts.toml index 78c3d098a..97893ab59 100644 --- a/integration/fixtures/timeout/forwarding_timeouts.toml +++ b/integration/fixtures/timeout/forwarding_timeouts.toml @@ -8,8 +8,7 @@ defaultEntryPoints = ["http"] [accessLog] format = "json" -[web] -address = ":8080" +[api] [forwardingTimeouts] dialTimeout = "300ms" diff --git a/integration/fixtures/websocket/config.toml b/integration/fixtures/websocket/config.toml index e65a6f80a..6650a1bf5 100644 --- a/integration/fixtures/websocket/config.toml +++ b/integration/fixtures/websocket/config.toml @@ -7,8 +7,7 @@ logLevel = "DEBUG" address = ":8000" -[web] - address = ":8080" +[api] [file] diff --git a/integration/fixtures/websocket/config_https.toml b/integration/fixtures/websocket/config_https.toml index 73ef23f2c..21c40a473 100644 --- a/integration/fixtures/websocket/config_https.toml +++ b/integration/fixtures/websocket/config_https.toml @@ -11,8 +11,7 @@ InsecureSkipVerify=true certFile = "resources/tls/local.cert" keyFile = "resources/tls/local.key" -[web] - address = ":8080" +[api] [file] diff --git a/integration/resources/compose/addprefix.yml b/integration/resources/compose/addprefix.yml new file mode 100644 index 000000000..4b6f328d9 --- /dev/null +++ b/integration/resources/compose/addprefix.yml @@ -0,0 +1,5 @@ +whoami1: + image: emilevauge/whoami + labels: + - traefik.enable=true + - traefik.frontend.rule=AddPrefix:/whoami;PathPrefix:/ diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 6e31b0615..028e3f076 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -2,6 +2,7 @@ package docker import ( "context" + "io" "net" "net/http" "strconv" @@ -218,16 +219,22 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } eventsc, errc := dockerClient.Events(ctx, options) - for event := range eventsc { - if event.Action == "start" || - event.Action == "die" || - strings.HasPrefix(event.Action, "health_status") { - startStopHandle(event) + for { + select { + case event := <-eventsc: + if event.Action == "start" || + event.Action == "die" || + strings.HasPrefix(event.Action, "health_status") { + startStopHandle(event) + } + case err := <-errc: + if err == io.EOF { + log.Debug("Provider event stream closed") + } + + return err } } - if err := <-errc; err != nil { - return err - } } } return nil diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 0584870a5..75d6464af 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -82,11 +82,11 @@ func (p *Provider) newK8sClient() (Client, error) { } if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" { - log.Infof("Creating in-cluster Provider client%s\n", withEndpoint) + log.Infof("Creating in-cluster Provider client%s", withEndpoint) return NewInClusterClient(p.Endpoint) } - log.Infof("Creating cluster-external Provider client%s\n", withEndpoint) + log.Infof("Creating cluster-external Provider client%s", withEndpoint) return NewExternalClusterClient(p.Endpoint, p.Token, p.CertAuthFilePath) } diff --git a/server/server.go b/server/server.go index 52e10d441..55113e626 100644 --- a/server/server.go +++ b/server/server.go @@ -770,6 +770,10 @@ func (s *Server) addInternalPublicRoutes(entryPointName string, router *mux.Rout if s.globalConfiguration.Ping != nil && s.globalConfiguration.Ping.EntryPoint != "" && s.globalConfiguration.Ping.EntryPoint == entryPointName { s.globalConfiguration.Ping.AddRoutes(router) } + + if s.globalConfiguration.ACME != nil && s.globalConfiguration.ACME.HTTPChallenge != nil && s.globalConfiguration.ACME.HTTPChallenge.EntryPoint == entryPointName { + s.globalConfiguration.ACME.AddRoutes(router) + } } func (s *Server) prepareServer(entryPointName string, entryPoint *configuration.EntryPoint, router *middlewares.HandlerSwitcher, middlewares []negroni.Handler, internalMiddlewares []negroni.Handler) (*http.Server, net.Listener, error) { diff --git a/vendor/github.com/abbot/go-http-auth/digest.go b/vendor/github.com/abbot/go-http-auth/digest.go index 21b09334c..0ea5bacf9 100644 --- a/vendor/github.com/abbot/go-http-auth/digest.go +++ b/vendor/github.com/abbot/go-http-auth/digest.go @@ -39,7 +39,7 @@ type DigestAuth struct { ClientCacheTolerance int clients map[string]*digest_client - mutex sync.Mutex + mutex sync.RWMutex } // check that DigestAuth implements AuthenticatorInterface @@ -84,11 +84,16 @@ func (a *DigestAuth) Purge(count int) { (or requires reauthentication). */ func (a *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { + a.mutex.Lock() + defer a.mutex.Unlock() if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance { a.Purge(a.ClientCacheTolerance * 2) } + nonce := RandomKey() + a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + w.Header().Set(contentType, a.Headers.V().UnauthContentType) w.Header().Set(a.Headers.V().Authenticate, fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, @@ -118,8 +123,8 @@ func DigestAuthParams(authorization string) map[string]string { Authentication-Info response header. */ func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) { - da.mutex.Lock() - defer da.mutex.Unlock() + da.mutex.RLock() + defer da.mutex.RUnlock() username = "" authinfo = nil auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization)) @@ -242,6 +247,8 @@ func (a *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc { // NewContext returns a context carrying authentication information for the request. func (a *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context { + a.mutex.Lock() + defer a.mutex.Unlock() username, authinfo := a.CheckAuth(r) info := &Info{Username: username, ResponseHeaders: make(http.Header)} if username != "" {