Add support for custom CA certificates by certificate resolver
This commit is contained in:
parent
e222d5cb2f
commit
ac1dad3d14
6 changed files with 210 additions and 4 deletions
|
@ -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
|
## Fallback
|
||||||
|
|
||||||
If Let's Encrypt is not reachable, the following certificates will apply:
|
If Let's Encrypt is not reachable, the following certificates will apply:
|
||||||
|
|
|
@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```)
|
||||||
`--certificatesresolvers.<name>`:
|
`--certificatesresolvers.<name>`:
|
||||||
Certificates resolvers configuration. (Default: ```false```)
|
Certificates resolvers configuration. (Default: ```false```)
|
||||||
|
|
||||||
|
`--certificatesresolvers.<name>.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.<name>.acme.caserver`:
|
`--certificatesresolvers.<name>.acme.caserver`:
|
||||||
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
|
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
|
||||||
|
|
||||||
|
`--certificatesresolvers.<name>.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.<name>.acme.casystemcertpool`:
|
||||||
|
Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```)
|
||||||
|
|
||||||
`--certificatesresolvers.<name>.acme.certificatesduration`:
|
`--certificatesresolvers.<name>.acme.certificatesduration`:
|
||||||
Certificates' duration in hours. (Default: ```2160```)
|
Certificates' duration in hours. (Default: ```2160```)
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,18 @@ Activate API directly on the entryPoint named traefik. (Default: ```false```)
|
||||||
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>`:
|
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>`:
|
||||||
Certificates resolvers configuration. (Default: ```false```)
|
Certificates resolvers configuration. (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_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_<NAME>_ACME_CASERVER`:
|
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASERVER`:
|
||||||
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
|
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
|
||||||
|
|
||||||
|
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_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_<NAME>_ACME_CASYSTEMCERTPOOL`:
|
||||||
|
Define if the certificates pool must use a copy of the system cert pool. (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CERTIFICATESDURATION`:
|
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CERTIFICATESDURATION`:
|
||||||
Certificates' duration in hours. (Default: ```2160```)
|
Certificates' duration in hours. (Default: ```2160```)
|
||||||
|
|
||||||
|
|
|
@ -441,6 +441,9 @@
|
||||||
storage = "foobar"
|
storage = "foobar"
|
||||||
keyType = "foobar"
|
keyType = "foobar"
|
||||||
certificatesDuration = 42
|
certificatesDuration = 42
|
||||||
|
caCertificates = ["foobar", "foobar"]
|
||||||
|
caSystemCertPool = true
|
||||||
|
caServerName = "foobar"
|
||||||
[certificatesResolvers.CertificateResolver0.acme.eab]
|
[certificatesResolvers.CertificateResolver0.acme.eab]
|
||||||
kid = "foobar"
|
kid = "foobar"
|
||||||
hmacEncoded = "foobar"
|
hmacEncoded = "foobar"
|
||||||
|
@ -461,6 +464,9 @@
|
||||||
storage = "foobar"
|
storage = "foobar"
|
||||||
keyType = "foobar"
|
keyType = "foobar"
|
||||||
certificatesDuration = 42
|
certificatesDuration = 42
|
||||||
|
caCertificates = ["foobar", "foobar"]
|
||||||
|
caSystemCertPool = true
|
||||||
|
caServerName = "foobar"
|
||||||
[certificatesResolvers.CertificateResolver1.acme.eab]
|
[certificatesResolvers.CertificateResolver1.acme.eab]
|
||||||
kid = "foobar"
|
kid = "foobar"
|
||||||
hmacEncoded = "foobar"
|
hmacEncoded = "foobar"
|
||||||
|
|
|
@ -483,6 +483,11 @@ certificatesResolvers:
|
||||||
kid: foobar
|
kid: foobar
|
||||||
hmacEncoded: foobar
|
hmacEncoded: foobar
|
||||||
certificatesDuration: 42
|
certificatesDuration: 42
|
||||||
|
caCertificates:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
caSystemCertPool: true
|
||||||
|
caServerName: foobar
|
||||||
dnsChallenge:
|
dnsChallenge:
|
||||||
provider: foobar
|
provider: foobar
|
||||||
delayBeforeCheck: 42s
|
delayBeforeCheck: 42s
|
||||||
|
@ -505,6 +510,11 @@ certificatesResolvers:
|
||||||
kid: foobar
|
kid: foobar
|
||||||
hmacEncoded: foobar
|
hmacEncoded: foobar
|
||||||
certificatesDuration: 42
|
certificatesDuration: 42
|
||||||
|
caCertificates:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
caSystemCertPool: true
|
||||||
|
caServerName: foobar
|
||||||
dnsChallenge:
|
dnsChallenge:
|
||||||
provider: foobar
|
provider: foobar
|
||||||
delayBeforeCheck: 42s
|
delayBeforeCheck: 42s
|
||||||
|
|
|
@ -6,9 +6,13 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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.Certificate.KeyType = GetKeyType(ctx, p.KeyType)
|
||||||
config.UserAgent = fmt.Sprintf("containous-traefik/%s", version.Version)
|
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)
|
client, err := lego.NewClient(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -340,6 +353,64 @@ func (p *Provider) getClient() (*lego.Client, error) {
|
||||||
return p.client, nil
|
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) {
|
func (p *Provider) initAccount(ctx context.Context) (*Account, error) {
|
||||||
if p.account == nil || len(p.account.Email) == 0 {
|
if p.account == nil || len(p.account.Email) == 0 {
|
||||||
var err error
|
var err error
|
||||||
|
@ -424,8 +495,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
|
|
||||||
if len(route.TLS.Domains) > 0 {
|
if len(route.TLS.Domains) > 0 {
|
||||||
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
||||||
for i := range len(domains) {
|
for _, domain := range domains {
|
||||||
domain := domains[i]
|
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -461,8 +531,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
|
|
||||||
if len(route.TLS.Domains) > 0 {
|
if len(route.TLS.Domains) > 0 {
|
||||||
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
||||||
for i := range len(domains) {
|
for _, domain := range domains {
|
||||||
domain := domains[i]
|
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue