Add support for custom CA certificates by certificate resolver

This commit is contained in:
Ludovic Fernandez 2024-09-09 17:24:04 +02:00 committed by GitHub
parent e222d5cb2f
commit ac1dad3d14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 210 additions and 4 deletions

View file

@ -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:

View file

@ -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```)

View file

@ -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```)

View file

@ -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"

View file

@ -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

View file

@ -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 {