diff --git a/docs/content/https/acme.md b/docs/content/https/acme.md index b73e6cc2f..daaaa2100 100644 --- a/docs/content/https/acme.md +++ b/docs/content/https/acme.md @@ -709,6 +709,109 @@ certificatesResolvers: # ... ``` +### `caCertificates` + +_Optional, Default=[]_ + +The `caCertificates` option specifies the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + +```yaml tab="File (YAML)" +certificatesResolvers: + myresolver: + acme: + # ... + caCertificates: + - path/certificates1.pem + - path/certificates2.pem + # ... +``` + +```toml tab="File (TOML)" +[certificatesResolvers.myresolver.acme] + # ... + caCertificates = [ "path/certificates1.pem", "path/certificates2.pem" ] + # ... +``` + +```bash tab="CLI" +# ... +--certificatesresolvers.myresolver.acme.caCertificates="path/certificates1.pem,path/certificates2.pem" +# ... +``` + +??? note "LEGO Environment Variable" + + It can be defined globally by using the environment variable `LEGO_CA_CERTIFICATES`. + This environment variable is neither a fallback nor an override of the configuration option. + +### `caSystemCertPool` + +_Optional, Default=false_ + +The `caSystemCertPool` option defines if the certificates pool must use a copy of the system cert pool. + +```yaml tab="File (YAML)" +certificatesResolvers: + myresolver: + acme: + # ... + caSystemCertPool: true + # ... +``` + +```toml tab="File (TOML)" +[certificatesResolvers.myresolver.acme] + # ... + caSystemCertPool = true + # ... +``` + +```bash tab="CLI" +# ... +--certificatesresolvers.myresolver.acme.caSystemCertPool=true +# ... +``` + +??? note "LEGO Environment Variable" + + It can be defined globally by using the environment variable `LEGO_CA_SYSTEM_CERT_POOL`. + `LEGO_CA_SYSTEM_CERT_POOL` is ignored if `LEGO_CA_CERTIFICATES` is not set or empty. + This environment variable is neither a fallback nor an override of the configuration option. + +### `caServerName` + +_Optional, Default=""_ + +The `caServerName` option specifies the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + +```yaml tab="File (YAML)" +certificatesResolvers: + myresolver: + acme: + # ... + caServerName: "my-server" + # ... +``` + +```toml tab="File (TOML)" +[certificatesResolvers.myresolver.acme] + # ... + caServerName = "my-server" + # ... +``` + +```bash tab="CLI" +# ... +--certificatesresolvers.myresolver.acme.caServerName="my-server" +# ... +``` + +??? note "LEGO Environment Variable" + + It can be defined globally by using the environment variable `LEGO_CA_SERVER_NAME`. + `LEGO_CA_SERVER_NAME` is ignored if `LEGO_CA_CERTIFICATES` is not set or empty. + This environment variable is neither a fallback nor an override of the configuration option. + ## Fallback If Let's Encrypt is not reachable, the following certificates will apply: diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index a2171dfa8..b0c02c565 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```) `--certificatesresolvers.`: Certificates resolvers configuration. (Default: ```false```) +`--certificatesresolvers..acme.cacertificates`: +Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + `--certificatesresolvers..acme.caserver`: CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```) +`--certificatesresolvers..acme.caservername`: +Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + +`--certificatesresolvers..acme.casystemcertpool`: +Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```) + `--certificatesresolvers..acme.certificatesduration`: Certificates' duration in hours. (Default: ```2160```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index b7cd5ee3c..2f4c19971 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```) `TRAEFIK_CERTIFICATESRESOLVERS_`: Certificates resolvers configuration. (Default: ```false```) +`TRAEFIK_CERTIFICATESRESOLVERS__ACME_CACERTIFICATES`: +Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + `TRAEFIK_CERTIFICATESRESOLVERS__ACME_CASERVER`: CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```) +`TRAEFIK_CERTIFICATESRESOLVERS__ACME_CASERVERNAME`: +Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list. + +`TRAEFIK_CERTIFICATESRESOLVERS__ACME_CASYSTEMCERTPOOL`: +Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```) + `TRAEFIK_CERTIFICATESRESOLVERS__ACME_CERTIFICATESDURATION`: Certificates' duration in hours. (Default: ```2160```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index daccb6dee..f92cd3763 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -441,6 +441,9 @@ storage = "foobar" keyType = "foobar" certificatesDuration = 42 + caCertificates = ["foobar", "foobar"] + caSystemCertPool = true + caServerName = "foobar" [certificatesResolvers.CertificateResolver0.acme.eab] kid = "foobar" hmacEncoded = "foobar" @@ -461,6 +464,9 @@ storage = "foobar" keyType = "foobar" certificatesDuration = 42 + caCertificates = ["foobar", "foobar"] + caSystemCertPool = true + caServerName = "foobar" [certificatesResolvers.CertificateResolver1.acme.eab] kid = "foobar" hmacEncoded = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 2919824c3..12b5efccc 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -483,6 +483,11 @@ certificatesResolvers: kid: foobar hmacEncoded: foobar certificatesDuration: 42 + caCertificates: + - foobar + - foobar + caSystemCertPool: true + caServerName: foobar dnsChallenge: provider: foobar delayBeforeCheck: 42s @@ -505,6 +510,11 @@ certificatesResolvers: kid: foobar hmacEncoded: foobar certificatesDuration: 42 + caCertificates: + - foobar + - foobar + caSystemCertPool: true + caServerName: foobar dnsChallenge: provider: foobar delayBeforeCheck: 42s diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index 7df870c59..912b4073e 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -6,9 +6,13 @@ import ( "crypto/x509" "errors" "fmt" + "net" + "net/http" "net/url" + "os" "reflect" "sort" + "strconv" "strings" "sync" "time" @@ -43,6 +47,10 @@ type Configuration struct { EAB *EAB `description:"External Account Binding to use." json:"eab,omitempty" toml:"eab,omitempty" yaml:"eab,omitempty"` CertificatesDuration int `description:"Certificates' duration in hours." json:"certificatesDuration,omitempty" toml:"certificatesDuration,omitempty" yaml:"certificatesDuration,omitempty" export:"true"` + CACertificates []string `description:"Specify the paths to PEM encoded CA Certificates that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list." json:"caCertificates,omitempty" toml:"caCertificates,omitempty" yaml:"caCertificates,omitempty"` + CASystemCertPool bool `description:"Define if the certificates pool must use a copy of the system cert pool." json:"caSystemCertPool,omitempty" toml:"caSystemCertPool,omitempty" yaml:"caSystemCertPool,omitempty" export:"true"` + CAServerName string `description:"Specify the CA server name that can be used to authenticate an ACME server with an HTTPS certificate not issued by a CA in the system-wide trusted root list." json:"caServerName,omitempty" toml:"caServerName,omitempty" yaml:"caServerName,omitempty" export:"true"` + DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` TLSChallenge *TLSChallenge `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` @@ -261,6 +269,11 @@ func (p *Provider) getClient() (*lego.Client, error) { config.Certificate.KeyType = GetKeyType(ctx, p.KeyType) config.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version) + config.HTTPClient, err = p.createHTTPClient() + if err != nil { + return nil, fmt.Errorf("creating HTTP client: %w", err) + } + client, err := lego.NewClient(config) if err != nil { return nil, err @@ -340,6 +353,64 @@ func (p *Provider) getClient() (*lego.Client, error) { return p.client, nil } +func (p *Provider) createHTTPClient() (*http.Client, error) { + tlsConfig, err := p.createClientTLSConfig() + if err != nil { + return nil, fmt.Errorf("creating client TLS config: %w", err) + } + + return &http.Client{ + Timeout: 2 * time.Minute, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 30 * time.Second, + ResponseHeaderTimeout: 30 * time.Second, + TLSClientConfig: tlsConfig, + }, + }, nil +} + +func (p *Provider) createClientTLSConfig() (*tls.Config, error) { + if len(p.CACertificates) > 0 || p.CAServerName != "" { + certPool, err := lego.CreateCertPool(p.CACertificates, p.CASystemCertPool) + if err != nil { + return nil, fmt.Errorf("creating cert pool with custom certificates: %w", err) + } + + return &tls.Config{ + ServerName: p.CAServerName, + RootCAs: certPool, + }, nil + } + + // Compatibility layer with the lego. + // https://github.com/go-acme/lego/blob/834a9089f143e3407b3f5c8b93a0e285ba231fe2/lego/client_config.go#L24-L34 + // https://github.com/go-acme/lego/blob/834a9089f143e3407b3f5c8b93a0e285ba231fe2/lego/client_config.go#L97-L113 + + serverName := os.Getenv("LEGO_CA_SERVER_NAME") + customCACertsPath := os.Getenv("LEGO_CA_CERTIFICATES") + + if customCACertsPath == "" && serverName == "" { + return nil, nil + } + + useSystemCertPool, _ := strconv.ParseBool(os.Getenv("LEGO_CA_SYSTEM_CERT_POOL")) + + certPool, err := lego.CreateCertPool(strings.Split(customCACertsPath, string(os.PathListSeparator)), useSystemCertPool) + if err != nil { + return nil, fmt.Errorf("creating cert pool: %w", err) + } + + return &tls.Config{ + ServerName: serverName, + RootCAs: certPool, + }, nil +} + func (p *Provider) initAccount(ctx context.Context) (*Account, error) { if p.account == nil || len(p.account.Email) == 0 { var err error @@ -424,8 +495,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) { if len(route.TLS.Domains) > 0 { domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains) - for i := range len(domains) { - domain := domains[i] + for _, domain := range domains { safe.Go(func() { dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName) if err != nil { @@ -461,8 +531,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) { if len(route.TLS.Domains) > 0 { domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains) - for i := range len(domains) { - domain := domains[i] + for _, domain := range domains { safe.Go(func() { dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName) if err != nil {