ACME Default Certificate
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com> Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
parent
693d5da1b9
commit
a002ccfce3
22 changed files with 767 additions and 253 deletions
|
@ -157,7 +157,75 @@ data:
|
||||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
```
|
```
|
||||||
|
|
||||||
If no default certificate is provided, Traefik generates and uses a self-signed certificate.
|
If no `defaultCertificate` is provided, Traefik will use the generated one.
|
||||||
|
|
||||||
|
### ACME Default Certificate
|
||||||
|
|
||||||
|
You can configure Traefik to use an ACME provider (like Let's Encrypt) to generate the default certificate.
|
||||||
|
The configuration to resolve the default certificate should be defined in a TLS store:
|
||||||
|
|
||||||
|
!!! important "Precedence with the `defaultGeneratedCert` option"
|
||||||
|
|
||||||
|
The `defaultGeneratedCert` definition takes precedence over the ACME default certificate configuration.
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
# Dynamic configuration
|
||||||
|
|
||||||
|
tls:
|
||||||
|
stores:
|
||||||
|
default:
|
||||||
|
defaultGeneratedCert:
|
||||||
|
resolver: myresolver
|
||||||
|
domain:
|
||||||
|
main: example.org
|
||||||
|
sans:
|
||||||
|
- foo.example.org
|
||||||
|
- bar.example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
# Dynamic configuration
|
||||||
|
|
||||||
|
[tls.stores]
|
||||||
|
[tls.stores.default.defaultGeneratedCert]
|
||||||
|
resolver = "myresolver"
|
||||||
|
[tls.stores.default.defaultGeneratedCert.domain]
|
||||||
|
main = "example.org"
|
||||||
|
sans = ["foo.example.org", "bar.example.org"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: TLSStore
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
spec:
|
||||||
|
defaultGeneratedCert:
|
||||||
|
resolver: myresolver
|
||||||
|
domain:
|
||||||
|
main: example.org
|
||||||
|
sans:
|
||||||
|
- foo.example.org
|
||||||
|
- bar.example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Docker"
|
||||||
|
## Dynamic configuration
|
||||||
|
labels:
|
||||||
|
- "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver"
|
||||||
|
- "traefik.tls.stores.default.defaultgeneratedcert.domain.main=example.org"
|
||||||
|
- "traefik.tls.stores.default.defaultgeneratedcert.domain.sans=foo.example.org, bar.example.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json tab="Marathon"
|
||||||
|
labels: {
|
||||||
|
"traefik.tls.stores.default.defaultgeneratedcert.resolver": "myresolver",
|
||||||
|
"traefik.tls.stores.default.defaultgeneratedcert.domain.main": "example.org",
|
||||||
|
"traefik.tls.stores.default.defaultgeneratedcert.domain.sans": "foo.example.org, bar.example.org",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## TLS Options
|
## TLS Options
|
||||||
|
|
||||||
|
|
|
@ -201,3 +201,13 @@
|
||||||
- "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar"
|
- "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar"
|
||||||
- "traefik.udp.routers.udprouter1.service=foobar"
|
- "traefik.udp.routers.udprouter1.service=foobar"
|
||||||
- "traefik.udp.services.udpservice01.loadbalancer.server.port=foobar"
|
- "traefik.udp.services.udpservice01.loadbalancer.server.port=foobar"
|
||||||
|
- "traefik.tls.stores.Store0.defaultcertificate.certfile=foobar"
|
||||||
|
- "traefik.tls.stores.Store0.defaultcertificate.keyfile=foobar"
|
||||||
|
- "traefik.tls.stores.Store0.defaultgeneratedcert.domain.main=foobar"
|
||||||
|
- "traefik.tls.stores.Store0.defaultgeneratedcert.domain.sans=foobar, foobar"
|
||||||
|
- "traefik.tls.stores.Store0.defaultgeneratedcert.resolver=foobar"
|
||||||
|
- "traefik.tls.stores.Store1.defaultcertificate.certfile=foobar"
|
||||||
|
- "traefik.tls.stores.Store1.defaultcertificate.keyfile=foobar"
|
||||||
|
- "traefik.tls.stores.Store1.defaultgeneratedcert.domain.main=foobar"
|
||||||
|
- "traefik.tls.stores.Store1.defaultgeneratedcert.domain.sans=foobar, foobar"
|
||||||
|
- "traefik.tls.stores.Store1.defaultgeneratedcert.resolver=foobar"
|
||||||
|
|
|
@ -463,7 +463,17 @@
|
||||||
[tls.stores.Store0.defaultCertificate]
|
[tls.stores.Store0.defaultCertificate]
|
||||||
certFile = "foobar"
|
certFile = "foobar"
|
||||||
keyFile = "foobar"
|
keyFile = "foobar"
|
||||||
|
[tls.stores.Store0.defaultGeneratedCert]
|
||||||
|
resolver = "foobar"
|
||||||
|
[tls.stores.Store0.defaultGeneratedCert.domain]
|
||||||
|
main = "foobar"
|
||||||
|
sans = ["foobar", "foobar"]
|
||||||
[tls.stores.Store1]
|
[tls.stores.Store1]
|
||||||
[tls.stores.Store1.defaultCertificate]
|
[tls.stores.Store1.defaultCertificate]
|
||||||
certFile = "foobar"
|
certFile = "foobar"
|
||||||
keyFile = "foobar"
|
keyFile = "foobar"
|
||||||
|
[tls.stores.Store1.defaultGeneratedCert]
|
||||||
|
resolver = "foobar"
|
||||||
|
[tls.stores.Store1.defaultGeneratedCert.domain]
|
||||||
|
main = "foobar"
|
||||||
|
sans = ["foobar", "foobar"]
|
||||||
|
|
|
@ -518,7 +518,21 @@ tls:
|
||||||
defaultCertificate:
|
defaultCertificate:
|
||||||
certFile: foobar
|
certFile: foobar
|
||||||
keyFile: foobar
|
keyFile: foobar
|
||||||
|
defaultGeneratedCert:
|
||||||
|
resolver: foobar
|
||||||
|
domain:
|
||||||
|
main: foobar
|
||||||
|
sans:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
Store1:
|
Store1:
|
||||||
defaultCertificate:
|
defaultCertificate:
|
||||||
certFile: foobar
|
certFile: foobar
|
||||||
keyFile: foobar
|
keyFile: foobar
|
||||||
|
defaultGeneratedCert:
|
||||||
|
resolver: foobar
|
||||||
|
domain:
|
||||||
|
main: foobar
|
||||||
|
sans:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
|
|
@ -1870,6 +1870,27 @@ spec:
|
||||||
required:
|
required:
|
||||||
- secretName
|
- secretName
|
||||||
type: object
|
type: object
|
||||||
|
defaultGeneratedCert:
|
||||||
|
description: DefaultGeneratedCert defines the default generated certificate
|
||||||
|
configuration.
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Domain is the domain definition for the DefaultCertificate.
|
||||||
|
properties:
|
||||||
|
main:
|
||||||
|
description: Main defines the main domain name.
|
||||||
|
type: string
|
||||||
|
sans:
|
||||||
|
description: SANs defines the subject alternative domain names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
resolver:
|
||||||
|
description: Resolver is the name of the resolver that will be
|
||||||
|
used to issue the DefaultCertificate.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- metadata
|
- metadata
|
||||||
|
|
|
@ -319,8 +319,16 @@
|
||||||
| `traefik/tls/options/Options1/sniStrict` | `true` |
|
| `traefik/tls/options/Options1/sniStrict` | `true` |
|
||||||
| `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` |
|
| `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` |
|
||||||
| `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` |
|
| `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/main` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/sans/0` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/sans/1` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store0/defaultGeneratedCert/resolver` | `foobar` |
|
||||||
| `traefik/tls/stores/Store1/defaultCertificate/certFile` | `foobar` |
|
| `traefik/tls/stores/Store1/defaultCertificate/certFile` | `foobar` |
|
||||||
| `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `foobar` |
|
| `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/main` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/sans/0` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/sans/1` | `foobar` |
|
||||||
|
| `traefik/tls/stores/Store1/defaultGeneratedCert/resolver` | `foobar` |
|
||||||
| `traefik/udp/routers/UDPRouter0/entryPoints/0` | `foobar` |
|
| `traefik/udp/routers/UDPRouter0/entryPoints/0` | `foobar` |
|
||||||
| `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` |
|
| `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` |
|
||||||
| `traefik/udp/routers/UDPRouter0/service` | `foobar` |
|
| `traefik/udp/routers/UDPRouter0/service` | `foobar` |
|
||||||
|
|
|
@ -201,3 +201,13 @@
|
||||||
"traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar",
|
"traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar",
|
||||||
"traefik.udp.routers.udprouter1.service": "foobar",
|
"traefik.udp.routers.udprouter1.service": "foobar",
|
||||||
"traefik.udp.services.udpservice01.loadbalancer.server.port": "foobar",
|
"traefik.udp.services.udpservice01.loadbalancer.server.port": "foobar",
|
||||||
|
"traefik.tls.stores.Store0.defaultcertificate.certfile": "foobar",
|
||||||
|
"traefik.tls.stores.Store0.defaultcertificate.keyfile": "foobar",
|
||||||
|
"traefik.tls.stores.Store0.defaultgeneratedcert.domain.main": "foobar",
|
||||||
|
"traefik.tls.stores.Store0.defaultgeneratedcert.domain.sans": "foobar, foobar",
|
||||||
|
"traefik.tls.stores.Store0.defaultgeneratedcert.resolver": "foobar",
|
||||||
|
"traefik.tls.stores.Store1.defaultcertificate.certfile": "foobar",
|
||||||
|
"traefik.tls.stores.Store1.defaultcertificate.keyfile": "foobar",
|
||||||
|
"traefik.tls.stores.Store1.defaultgeneratedcert.domain.main": "foobar",
|
||||||
|
"traefik.tls.stores.Store1.defaultgeneratedcert.domain.sans": "foobar, foobar",
|
||||||
|
"traefik.tls.stores.Store1.defaultgeneratedcert.resolver": "foobar",
|
||||||
|
|
|
@ -63,6 +63,27 @@ spec:
|
||||||
required:
|
required:
|
||||||
- secretName
|
- secretName
|
||||||
type: object
|
type: object
|
||||||
|
defaultGeneratedCert:
|
||||||
|
description: DefaultGeneratedCert defines the default generated certificate
|
||||||
|
configuration.
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Domain is the domain definition for the DefaultCertificate.
|
||||||
|
properties:
|
||||||
|
main:
|
||||||
|
description: Main defines the main domain name.
|
||||||
|
type: string
|
||||||
|
sans:
|
||||||
|
description: SANs defines the subject alternative domain names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
resolver:
|
||||||
|
description: Resolver is the name of the resolver that will be
|
||||||
|
used to issue the DefaultCertificate.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- metadata
|
- metadata
|
||||||
|
|
|
@ -40,6 +40,7 @@ type acmeTestCase struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateModel struct {
|
type templateModel struct {
|
||||||
|
Domain types.Domain
|
||||||
Domains []types.Domain
|
Domains []types.Domain
|
||||||
PortHTTP string
|
PortHTTP string
|
||||||
PortHTTPS string
|
PortHTTPS string
|
||||||
|
@ -149,6 +150,29 @@ func (s *AcmeSuite) TestHTTP01Domains(c *check.C) {
|
||||||
s.retrieveAcmeCertificate(c, testCase)
|
s.retrieveAcmeCertificate(c, testCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AcmeSuite) TestHTTP01StoreDomains(c *check.C) {
|
||||||
|
testCase := acmeTestCase{
|
||||||
|
traefikConfFilePath: "fixtures/acme/acme_store_domains.toml",
|
||||||
|
subCases: []subCases{{
|
||||||
|
host: acmeDomain,
|
||||||
|
expectedCommonName: acmeDomain,
|
||||||
|
expectedAlgorithm: x509.RSA,
|
||||||
|
}},
|
||||||
|
template: templateModel{
|
||||||
|
Domain: types.Domain{
|
||||||
|
Main: "traefik.acme.wtf",
|
||||||
|
},
|
||||||
|
Acme: map[string]static.CertificateResolver{
|
||||||
|
"default": {ACME: &acme.Configuration{
|
||||||
|
HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.retrieveAcmeCertificate(c, testCase)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AcmeSuite) TestHTTP01DomainsInSAN(c *check.C) {
|
func (s *AcmeSuite) TestHTTP01DomainsInSAN(c *check.C) {
|
||||||
testCase := acmeTestCase{
|
testCase := acmeTestCase{
|
||||||
traefikConfFilePath: "fixtures/acme/acme_domains.toml",
|
traefikConfFilePath: "fixtures/acme/acme_domains.toml",
|
||||||
|
|
60
integration/fixtures/acme/acme_store_domains.toml
Normal file
60
integration/fixtures/acme/acme_store_domains.toml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "DEBUG"
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.web]
|
||||||
|
address = "{{ .PortHTTP }}"
|
||||||
|
[entryPoints.websecure]
|
||||||
|
address = "{{ .PortHTTPS }}"
|
||||||
|
|
||||||
|
{{range $name, $resolvers := .Acme }}
|
||||||
|
|
||||||
|
[certificatesResolvers.{{ $name }}.acme]
|
||||||
|
email = "test@traefik.io"
|
||||||
|
storage = "/tmp/acme.json"
|
||||||
|
keyType = "{{ $resolvers.ACME.KeyType }}"
|
||||||
|
caServer = "{{ $resolvers.ACME.CAServer }}"
|
||||||
|
|
||||||
|
{{if $resolvers.ACME.HTTPChallenge }}
|
||||||
|
[certificatesResolvers.{{ $name }}.acme.httpChallenge]
|
||||||
|
entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if $resolvers.ACME.TLSChallenge }}
|
||||||
|
[certificatesResolvers.{{ $name }}.acme.tlsChallenge]
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
## dynamic configuration ##
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.test.loadBalancer]
|
||||||
|
[[http.services.test.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:9010"
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.test]
|
||||||
|
entryPoints = ["websecure"]
|
||||||
|
rule = "PathPrefix(`/`)"
|
||||||
|
service = "test"
|
||||||
|
[http.routers.test.tls]
|
||||||
|
|
||||||
|
[tls.stores]
|
||||||
|
[tls.stores.default.defaultGeneratedCert]
|
||||||
|
resolver = "default"
|
||||||
|
[tls.stores.default.defaultGeneratedCert.domain]
|
||||||
|
main = "{{ .Domain.Main }}"
|
||||||
|
sans = [{{range .Domain.SANs }}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
|
@ -1870,6 +1870,27 @@ spec:
|
||||||
required:
|
required:
|
||||||
- secretName
|
- secretName
|
||||||
type: object
|
type: object
|
||||||
|
defaultGeneratedCert:
|
||||||
|
description: DefaultGeneratedCert defines the default generated certificate
|
||||||
|
configuration.
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: Domain is the domain definition for the DefaultCertificate.
|
||||||
|
properties:
|
||||||
|
main:
|
||||||
|
description: Main defines the main domain name.
|
||||||
|
type: string
|
||||||
|
sans:
|
||||||
|
description: SANs defines the subject alternative domain names.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
resolver:
|
||||||
|
description: Resolver is the name of the resolver that will be
|
||||||
|
used to issue the DefaultCertificate.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- metadata
|
- metadata
|
||||||
|
|
|
@ -325,7 +325,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificate(c *check.C) {
|
||||||
|
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
|
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
|
||||||
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
|
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
|
||||||
|
|
||||||
proto := cs.NegotiatedProtocol
|
proto := cs.NegotiatedProtocol
|
||||||
c.Assert(proto, checker.Equals, "h2")
|
c.Assert(proto, checker.Equals, "h2")
|
||||||
|
@ -360,7 +360,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI(c *check.C) {
|
||||||
|
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
|
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
|
||||||
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
|
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
|
||||||
|
|
||||||
proto := cs.NegotiatedProtocol
|
proto := cs.NegotiatedProtocol
|
||||||
c.Assert(proto, checker.Equals, "h2")
|
c.Assert(proto, checker.Equals, "h2")
|
||||||
|
@ -397,7 +397,7 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate(c *check.C) {
|
||||||
|
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
|
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
|
||||||
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
|
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
|
||||||
|
|
||||||
proto := cs.NegotiatedProtocol
|
proto := cs.NegotiatedProtocol
|
||||||
c.Assert(proto, checker.Equals, "h2")
|
c.Assert(proto, checker.Equals, "h2")
|
||||||
|
@ -434,7 +434,7 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate(c *check.C) {
|
||||||
|
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
|
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
|
||||||
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
|
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
|
||||||
|
|
||||||
proto := cs.NegotiatedProtocol
|
proto := cs.NegotiatedProtocol
|
||||||
c.Assert(proto, checker.Equals, "h2")
|
c.Assert(proto, checker.Equals, "h2")
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -29,8 +30,8 @@ import (
|
||||||
"github.com/traefik/traefik/v2/pkg/version"
|
"github.com/traefik/traefik/v2/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// oscpMustStaple enables OSCP stapling as from https://github.com/go-acme/lego/issues/270.
|
// ocspMustStaple enables OCSP stapling as from https://github.com/go-acme/lego/issues/270.
|
||||||
var oscpMustStaple = false
|
var ocspMustStaple = false
|
||||||
|
|
||||||
// Configuration holds ACME configuration provided by users.
|
// Configuration holds ACME configuration provided by users.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
|
@ -100,9 +101,10 @@ type Provider struct {
|
||||||
HTTPChallengeProvider challenge.Provider
|
HTTPChallengeProvider challenge.Provider
|
||||||
|
|
||||||
certificates []*CertAndStore
|
certificates []*CertAndStore
|
||||||
|
certificatesMu sync.RWMutex
|
||||||
|
|
||||||
account *Account
|
account *Account
|
||||||
client *lego.Client
|
client *lego.Client
|
||||||
certsChan chan *CertAndStore
|
|
||||||
configurationChan chan<- dynamic.Message
|
configurationChan chan<- dynamic.Message
|
||||||
tlsManager *traefiktls.Manager
|
tlsManager *traefiktls.Manager
|
||||||
clientMutex sync.Mutex
|
clientMutex sync.Mutex
|
||||||
|
@ -152,7 +154,10 @@ func (p *Provider) Init() error {
|
||||||
p.account = nil
|
p.account = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.certificatesMu.Lock()
|
||||||
p.certificates, err = p.Store.GetCertificates(p.ResolverName)
|
p.certificates, err = p.Store.GetCertificates(p.ResolverName)
|
||||||
|
p.certificatesMu.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get ACME certificates : %w", err)
|
return fmt.Errorf("unable to get ACME certificates : %w", err)
|
||||||
}
|
}
|
||||||
|
@ -195,11 +200,15 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||||
|
|
||||||
p.pool = pool
|
p.pool = pool
|
||||||
|
|
||||||
p.watchCertificate(ctx)
|
|
||||||
p.watchNewDomains(ctx)
|
p.watchNewDomains(ctx)
|
||||||
|
|
||||||
p.configurationChan = configurationChan
|
p.configurationChan = configurationChan
|
||||||
p.refreshCertificates()
|
|
||||||
|
p.certificatesMu.RLock()
|
||||||
|
msg := p.buildMessage()
|
||||||
|
p.certificatesMu.RUnlock()
|
||||||
|
|
||||||
|
p.configurationChan <- msg
|
||||||
|
|
||||||
renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration)
|
renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration)
|
||||||
log.FromContext(ctx).Debugf("Attempt to renew certificates %q before expiry and check every %q",
|
log.FromContext(ctx).Debugf("Attempt to renew certificates %q before expiry and check every %q",
|
||||||
|
@ -365,12 +374,14 @@ func (p *Provider) register(ctx context.Context, client *lego.Client) (*registra
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
|
func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
log.FromContext(ctx).Debug("No domain parsed in provider ACME")
|
logger.Debug("No domain parsed in provider ACME")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.FromContext(ctx).Debugf("Try to challenge certificate for domain %v found in HostSNI rule", domains)
|
logger.Debugf("Trying to challenge certificate for domain %v found in HostSNI rule", domains)
|
||||||
|
|
||||||
var domain types.Domain
|
var domain types.Domain
|
||||||
if len(domains) > 0 {
|
if len(domains) > 0 {
|
||||||
|
@ -380,14 +391,22 @@ func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStor
|
||||||
}
|
}
|
||||||
|
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
|
dom, cert, err := p.resolveCertificate(ctx, domain, tlsStore)
|
||||||
log.FromContext(ctx).Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, tlsStore)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Error adding certificate for domain")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) watchNewDomains(ctx context.Context) {
|
func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
|
ctx = log.With(ctx, log.Str(log.ProviderName, p.ResolverName+".acme"))
|
||||||
p.pool.GoCtx(func(ctxPool context.Context) {
|
p.pool.GoCtx(func(ctxPool context.Context) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -402,31 +421,26 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
logger := log.FromContext(ctxRouter)
|
logger := log.FromContext(ctxRouter)
|
||||||
|
|
||||||
if len(route.TLS.Domains) > 0 {
|
if len(route.TLS.Domains) > 0 {
|
||||||
for _, domain := range route.TLS.Domains {
|
|
||||||
if domain.Main != dns01.UnFqdn(domain.Main) {
|
|
||||||
logger.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
|
|
||||||
}
|
|
||||||
for _, san := range domain.SANs {
|
|
||||||
if san != dns01.UnFqdn(san) {
|
|
||||||
logger.Warnf("FQDN detected, please remove the trailing dot: %s", san)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
|
||||||
for i := 0; i < len(domains); i++ {
|
for i := 0; i < len(domains); i++ {
|
||||||
domain := domains[i]
|
domain := domains[i]
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
if _, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName); err != nil {
|
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
||||||
log.WithoutContext().WithField(log.ProviderName, p.ResolverName+".acme").
|
if err != nil {
|
||||||
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
|
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domains %q", strings.Join(domain.ToStrArray(), ","))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Error adding certificate for domain")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
|
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error parsing domains in provider ACME: %v", err)
|
logger.WithError(err).Errorf("Error parsing domains in provider ACME")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
|
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
|
||||||
|
@ -434,33 +448,99 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.HTTP != nil {
|
||||||
for routerName, route := range config.HTTP.Routers {
|
for routerName, route := range config.HTTP.Routers {
|
||||||
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
|
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
|
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
|
||||||
|
logger := log.FromContext(ctxRouter)
|
||||||
|
|
||||||
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 := 0; i < len(domains); i++ {
|
for i := 0; i < len(domains); i++ {
|
||||||
domain := domains[i]
|
domain := domains[i]
|
||||||
safe.Go(func() {
|
safe.Go(func() {
|
||||||
if _, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName); err != nil {
|
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
|
||||||
log.WithoutContext().WithField(log.ProviderName, p.ResolverName+".acme").
|
if err != nil {
|
||||||
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
|
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domains %q", strings.Join(domain.ToStrArray(), ","))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Error adding certificate for domain")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
domains, err := httpmuxer.ParseDomains(route.Rule)
|
domains, err := httpmuxer.ParseDomains(route.Rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
|
logger.WithError(err).Errorf("Error parsing domains in provider ACME")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
|
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.TLS == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for tlsStoreName, tlsStore := range config.TLS.Stores {
|
||||||
|
ctxTLSStore := log.With(ctx, log.Str(log.TLSStoreName, tlsStoreName))
|
||||||
|
logger := log.FromContext(ctxTLSStore)
|
||||||
|
|
||||||
|
if tlsStore.DefaultCertificate != nil && tlsStore.DefaultGeneratedCert != nil {
|
||||||
|
logger.Warn("defaultCertificate and defaultGeneratedCert cannot be defined at the same time.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gives precedence to the user defined default certificate.
|
||||||
|
if tlsStore.DefaultCertificate != nil || tlsStore.DefaultGeneratedCert == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsStore.DefaultGeneratedCert.Domain == nil || tlsStore.DefaultGeneratedCert.Resolver == "" {
|
||||||
|
logger.Warn("default generated certificate domain or resolver is missing.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsStore.DefaultGeneratedCert.Resolver != p.ResolverName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
validDomains, err := p.sanitizeDomains(ctx, *tlsStore.DefaultGeneratedCert.Domain)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Errorf("domains validation: %s", strings.Join(tlsStore.DefaultGeneratedCert.Domain.ToStrArray(), ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.certExists(validDomains) {
|
||||||
|
logger.Debug("Default ACME certificate generation is not required.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
safe.Go(func() {
|
||||||
|
cert, err := p.resolveDefaultCertificate(ctx, validDomains)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domain %q", strings.Join(validDomains, ","))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := types.Domain{
|
||||||
|
Main: validDomains[0],
|
||||||
|
}
|
||||||
|
if len(validDomains) > 0 {
|
||||||
|
domain.SANs = validDomains[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Error adding certificate for domain")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
case <-ctxPool.Done():
|
case <-ctxPool.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -468,22 +548,30 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (*certificate.Resource, error) {
|
func (p *Provider) resolveDefaultCertificate(ctx context.Context, domains []string) (*certificate.Resource, error) {
|
||||||
domains, err := p.getValidDomains(ctx, domain)
|
logger := log.FromContext(ctx)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if provided certificates are not already in progress and lock them if needed
|
p.resolvingDomainsMutex.Lock()
|
||||||
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
|
|
||||||
if len(uncheckedDomains) == 0 {
|
sort.Strings(domains)
|
||||||
|
domainKey := strings.Join(domains, ",")
|
||||||
|
|
||||||
|
if _, ok := p.resolvingDomains[domainKey]; ok {
|
||||||
|
p.resolvingDomainsMutex.Unlock()
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer p.removeResolvingDomains(uncheckedDomains)
|
p.resolvingDomains[domainKey] = struct{}{}
|
||||||
|
|
||||||
logger := log.FromContext(ctx)
|
for _, certDomain := range domains {
|
||||||
logger.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
|
p.resolvingDomains[certDomain] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.resolvingDomainsMutex.Unlock()
|
||||||
|
|
||||||
|
defer p.removeResolvingDomains(append(domains, domainKey))
|
||||||
|
|
||||||
|
logger.Debugf("Loading ACME certificates %+v...", domains)
|
||||||
|
|
||||||
client, err := p.getClient()
|
client, err := p.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -493,31 +581,74 @@ func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain,
|
||||||
request := certificate.ObtainRequest{
|
request := certificate.ObtainRequest{
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
Bundle: true,
|
Bundle: true,
|
||||||
MustStaple: oscpMustStaple,
|
MustStaple: ocspMustStaple,
|
||||||
PreferredChain: p.PreferredChain,
|
PreferredChain: p.PreferredChain,
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := client.Certificate.Obtain(request)
|
cert, err := client.Certificate.Obtain(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", uncheckedDomains, err)
|
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", domains, err)
|
||||||
}
|
}
|
||||||
if cert == nil {
|
if cert == nil {
|
||||||
return nil, fmt.Errorf("domains %v do not generate a certificate", uncheckedDomains)
|
return nil, fmt.Errorf("unable to generate a certificate for the domains %v", domains)
|
||||||
}
|
}
|
||||||
if len(cert.Certificate) == 0 || len(cert.PrivateKey) == 0 {
|
if len(cert.Certificate) == 0 || len(cert.PrivateKey) == 0 {
|
||||||
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, cert)
|
return nil, fmt.Errorf("certificate for domains %v is empty: %v", domains, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Default certificate obtained for domains %+v", domains)
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (types.Domain, *certificate.Resource, error) {
|
||||||
|
domains, err := p.sanitizeDomains(ctx, domain)
|
||||||
|
if err != nil {
|
||||||
|
return types.Domain{}, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if provided certificates are not already in progress and lock them if needed
|
||||||
|
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
|
||||||
|
if len(uncheckedDomains) == 0 {
|
||||||
|
return types.Domain{}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer p.removeResolvingDomains(uncheckedDomains)
|
||||||
|
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
logger.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
|
||||||
|
|
||||||
|
client, err := p.getClient()
|
||||||
|
if err != nil {
|
||||||
|
return types.Domain{}, nil, fmt.Errorf("cannot get ACME client %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := certificate.ObtainRequest{
|
||||||
|
Domains: domains,
|
||||||
|
Bundle: true,
|
||||||
|
MustStaple: ocspMustStaple,
|
||||||
|
PreferredChain: p.PreferredChain,
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := client.Certificate.Obtain(request)
|
||||||
|
if err != nil {
|
||||||
|
return types.Domain{}, nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", uncheckedDomains, err)
|
||||||
|
}
|
||||||
|
if cert == nil {
|
||||||
|
return types.Domain{}, nil, fmt.Errorf("unable to generate a certificate for the domains %v", uncheckedDomains)
|
||||||
|
}
|
||||||
|
if len(cert.Certificate) == 0 || len(cert.PrivateKey) == 0 {
|
||||||
|
return types.Domain{}, nil, fmt.Errorf("certificate for domains %v is empty: %v", uncheckedDomains, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Certificates obtained for domains %+v", uncheckedDomains)
|
logger.Debugf("Certificates obtained for domains %+v", uncheckedDomains)
|
||||||
|
|
||||||
if len(uncheckedDomains) > 1 {
|
|
||||||
domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]}
|
|
||||||
} else {
|
|
||||||
domain = types.Domain{Main: uncheckedDomains[0]}
|
domain = types.Domain{Main: uncheckedDomains[0]}
|
||||||
|
if len(uncheckedDomains) > 1 {
|
||||||
|
domain.SANs = uncheckedDomains[1:]
|
||||||
}
|
}
|
||||||
p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, tlsStore)
|
|
||||||
|
|
||||||
return cert, nil
|
return domain, cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
|
func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
|
||||||
|
@ -529,8 +660,28 @@ func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key []byte, tlsStore string) {
|
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key []byte, tlsStore string) error {
|
||||||
p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore}
|
p.certificatesMu.Lock()
|
||||||
|
defer p.certificatesMu.Unlock()
|
||||||
|
|
||||||
|
cert := Certificate{Certificate: certificate, Key: key, Domain: domain}
|
||||||
|
|
||||||
|
certUpdated := false
|
||||||
|
for _, domainsCertificate := range p.certificates {
|
||||||
|
if reflect.DeepEqual(domain, domainsCertificate.Certificate.Domain) {
|
||||||
|
domainsCertificate.Certificate = cert
|
||||||
|
certUpdated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !certUpdated {
|
||||||
|
p.certificates = append(p.certificates, &CertAndStore{Certificate: cert, Store: tlsStore})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.configurationChan <- p.buildMessage()
|
||||||
|
|
||||||
|
return p.Store.SaveCertificates(p.ResolverName, p.certificates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCertificateRenewDurations returns renew durations calculated from the given certificatesDuration in hours.
|
// getCertificateRenewDurations returns renew durations calculated from the given certificatesDuration in hours.
|
||||||
|
@ -608,45 +759,7 @@ func deleteUnnecessaryDomains(ctx context.Context, domains []types.Domain) []typ
|
||||||
return newDomains
|
return newDomains
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) watchCertificate(ctx context.Context) {
|
func (p *Provider) buildMessage() dynamic.Message {
|
||||||
p.certsChan = make(chan *CertAndStore)
|
|
||||||
|
|
||||||
p.pool.GoCtx(func(ctxPool context.Context) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case cert := <-p.certsChan:
|
|
||||||
certUpdated := false
|
|
||||||
for _, domainsCertificate := range p.certificates {
|
|
||||||
if reflect.DeepEqual(cert.Domain, domainsCertificate.Certificate.Domain) {
|
|
||||||
domainsCertificate.Certificate = cert.Certificate
|
|
||||||
certUpdated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !certUpdated {
|
|
||||||
p.certificates = append(p.certificates, cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := p.saveCertificates()
|
|
||||||
if err != nil {
|
|
||||||
log.FromContext(ctx).Error(err)
|
|
||||||
}
|
|
||||||
case <-ctxPool.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) saveCertificates() error {
|
|
||||||
err := p.Store.SaveCertificates(p.ResolverName, p.certificates)
|
|
||||||
|
|
||||||
p.refreshCertificates()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) refreshCertificates() {
|
|
||||||
conf := dynamic.Message{
|
conf := dynamic.Message{
|
||||||
ProviderName: p.ResolverName + ".acme",
|
ProviderName: p.ResolverName + ".acme",
|
||||||
Configuration: &dynamic.Configuration{
|
Configuration: &dynamic.Configuration{
|
||||||
|
@ -670,20 +783,31 @@ func (p *Provider) refreshCertificates() {
|
||||||
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
|
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.configurationChan <- conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Duration) {
|
func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Duration) {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
logger.Info("Testing certificate renew...")
|
logger.Info("Testing certificate renew...")
|
||||||
|
|
||||||
|
p.certificatesMu.RLock()
|
||||||
|
|
||||||
|
var certificates []*CertAndStore
|
||||||
for _, cert := range p.certificates {
|
for _, cert := range p.certificates {
|
||||||
crt, err := getX509Certificate(ctx, &cert.Certificate)
|
crt, err := getX509Certificate(ctx, &cert.Certificate)
|
||||||
// If there's an error, we assume the cert is broken, and needs update
|
// If there's an error, we assume the cert is broken, and needs update
|
||||||
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(renewPeriod)) {
|
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(renewPeriod)) {
|
||||||
|
certificates = append(certificates, cert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.certificatesMu.RUnlock()
|
||||||
|
|
||||||
|
for _, cert := range certificates {
|
||||||
client, err := p.getClient()
|
client, err := p.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("Error renewing certificate from LE : %+v, %v", cert.Domain, err)
|
logger.WithError(err).Infof("Error renewing certificate from LE : %+v", cert.Domain)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -693,9 +817,9 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
|
||||||
Domain: cert.Domain.Main,
|
Domain: cert.Domain.Main,
|
||||||
PrivateKey: cert.Key,
|
PrivateKey: cert.Key,
|
||||||
Certificate: cert.Certificate.Certificate,
|
Certificate: cert.Certificate.Certificate,
|
||||||
}, true, oscpMustStaple, p.PreferredChain)
|
}, true, ocspMustStaple, p.PreferredChain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Error renewing certificate from LE: %v, %v", cert.Domain, err)
|
logger.WithError(err).Errorf("Error renewing certificate from LE: %v", cert.Domain)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -704,7 +828,9 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
|
err = p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithError(err).Error("Error adding certificate for domain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -712,17 +838,24 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
|
||||||
// Get provided certificate which check a domains list (Main and SANs)
|
// Get provided certificate which check a domains list (Main and SANs)
|
||||||
// from static and dynamic provided certificates.
|
// from static and dynamic provided certificates.
|
||||||
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string {
|
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string {
|
||||||
p.resolvingDomainsMutex.Lock()
|
|
||||||
defer p.resolvingDomainsMutex.Unlock()
|
|
||||||
|
|
||||||
log.FromContext(ctx).Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
|
log.FromContext(ctx).Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
|
||||||
|
|
||||||
allDomains := p.tlsManager.GetStore(tlsStore).GetAllDomains()
|
var allDomains []string
|
||||||
|
store := p.tlsManager.GetStore(tlsStore)
|
||||||
|
if store != nil {
|
||||||
|
allDomains = append(allDomains, store.GetAllDomains()...)
|
||||||
|
}
|
||||||
|
|
||||||
// Get ACME certificates
|
// Get ACME certificates
|
||||||
|
|
||||||
|
p.certificatesMu.RLock()
|
||||||
for _, cert := range p.certificates {
|
for _, cert := range p.certificates {
|
||||||
allDomains = append(allDomains, strings.Join(cert.Domain.ToStrArray(), ","))
|
allDomains = append(allDomains, strings.Join(cert.Domain.ToStrArray(), ","))
|
||||||
}
|
}
|
||||||
|
p.certificatesMu.RUnlock()
|
||||||
|
|
||||||
|
p.resolvingDomainsMutex.Lock()
|
||||||
|
defer p.resolvingDomainsMutex.Unlock()
|
||||||
|
|
||||||
// Get currently resolved domains
|
// Get currently resolved domains
|
||||||
for domain := range p.resolvingDomains {
|
for domain := range p.resolvingDomains {
|
||||||
|
@ -761,7 +894,7 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
|
||||||
|
|
||||||
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.Key)
|
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to load TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","), err)
|
logger.WithError(err).Errorf("Failed to load TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -769,43 +902,62 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
|
||||||
if crt == nil {
|
if crt == nil {
|
||||||
crt, err = x509.ParseCertificate(tlsCert.Certificate[0])
|
crt, err = x509.ParseCertificate(tlsCert.Certificate[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to parse TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","), err)
|
logger.WithError(err).Errorf("Failed to parse TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return crt, err
|
return crt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it.
|
// sanitizeDomains checks if given domain is allowed to generate a ACME certificate and return it.
|
||||||
func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain) ([]string, error) {
|
func (p *Provider) sanitizeDomains(ctx context.Context, domain types.Domain) ([]string, error) {
|
||||||
domains := domain.ToStrArray()
|
domains := domain.ToStrArray()
|
||||||
if len(domains) == 0 {
|
if len(domains) == 0 {
|
||||||
return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given")
|
return nil, errors.New("no domain was given")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(domain.Main, "*") {
|
var cleanDomains []string
|
||||||
|
for _, dom := range domains {
|
||||||
|
if strings.HasPrefix(dom, "*") {
|
||||||
if p.DNSChallenge == nil {
|
if p.DNSChallenge == nil {
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(domain.Main, "*.*") {
|
if strings.HasPrefix(dom, "*.*") {
|
||||||
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
|
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cleanDomains []string
|
canonicalDomain := types.CanonicalDomain(dom)
|
||||||
for _, domain := range domains {
|
|
||||||
canonicalDomain := types.CanonicalDomain(domain)
|
|
||||||
cleanDomain := dns01.UnFqdn(canonicalDomain)
|
cleanDomain := dns01.UnFqdn(canonicalDomain)
|
||||||
if canonicalDomain != cleanDomain {
|
if canonicalDomain != cleanDomain {
|
||||||
log.FromContext(ctx).Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain)
|
log.FromContext(ctx).Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanDomains = append(cleanDomains, cleanDomain)
|
cleanDomains = append(cleanDomains, cleanDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanDomains, nil
|
return cleanDomains, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// certExists returns whether a certificate already exists for given domains.
|
||||||
|
func (p *Provider) certExists(validDomains []string) bool {
|
||||||
|
p.certificatesMu.RLock()
|
||||||
|
defer p.certificatesMu.RUnlock()
|
||||||
|
|
||||||
|
sort.Strings(validDomains)
|
||||||
|
|
||||||
|
for _, cert := range p.certificates {
|
||||||
|
domains := cert.Certificate.Domain.ToStrArray()
|
||||||
|
sort.Strings(domains)
|
||||||
|
if reflect.DeepEqual(domains, validDomains) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool {
|
func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool {
|
||||||
for _, certDomains := range existentDomains {
|
for _, certDomains := range existentDomains {
|
||||||
for _, certDomain := range strings.Split(certDomains, ",") {
|
for _, certDomain := range strings.Split(certDomains, ",") {
|
||||||
|
|
|
@ -188,7 +188,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetValidDomain(t *testing.T) {
|
func TestProvider_sanitizeDomains(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
domains types.Domain
|
domains types.Domain
|
||||||
|
@ -214,7 +214,7 @@ func TestGetValidDomain(t *testing.T) {
|
||||||
desc: "no domain",
|
desc: "no domain",
|
||||||
domains: types.Domain{},
|
domains: types.Domain{},
|
||||||
dnsChallenge: nil,
|
dnsChallenge: nil,
|
||||||
expectedErr: "unable to generate a certificate in ACME provider when no domain is given",
|
expectedErr: "no domain was given",
|
||||||
expectedDomains: nil,
|
expectedDomains: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -254,7 +254,7 @@ func TestGetValidDomain(t *testing.T) {
|
||||||
|
|
||||||
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
|
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
|
||||||
|
|
||||||
domains, err := acmeProvider.getValidDomains(context.Background(), test.domains)
|
domains, err := acmeProvider.sanitizeDomains(context.Background(), test.domains)
|
||||||
|
|
||||||
if len(test.expectedErr) > 0 {
|
if len(test.expectedErr) > 0 {
|
||||||
assert.EqualError(t, err, test.expectedErr, "Unexpected error.")
|
assert.EqualError(t, err, test.expectedErr, "Unexpected error.")
|
||||||
|
|
|
@ -942,6 +942,13 @@ func buildTLSStores(ctx context.Context, client Client) (map[string]tls.Store, m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.Spec.DefaultGeneratedCert != nil {
|
||||||
|
tlsStore.DefaultGeneratedCert = &tls.GeneratedCert{
|
||||||
|
Resolver: t.Spec.DefaultGeneratedCert.Resolver,
|
||||||
|
Domain: t.Spec.DefaultGeneratedCert.Domain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil {
|
if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil {
|
||||||
logger.Errorf("Failed to load certificates: %v", err)
|
logger.Errorf("Failed to load certificates: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/traefik/traefik/v2/pkg/tls"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +28,10 @@ type TLSStore struct {
|
||||||
type TLSStoreSpec struct {
|
type TLSStoreSpec struct {
|
||||||
// DefaultCertificate defines the default certificate configuration.
|
// DefaultCertificate defines the default certificate configuration.
|
||||||
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty"`
|
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty"`
|
||||||
|
|
||||||
|
// DefaultGeneratedCert defines the default generated certificate configuration.
|
||||||
|
DefaultGeneratedCert *tls.GeneratedCert `json:"defaultGeneratedCert,omitempty"`
|
||||||
|
|
||||||
// Certificates is a list of secret names, each secret holding a key/certificate pair to add to the store.
|
// Certificates is a list of secret names, each secret holding a key/certificate pair to add to the store.
|
||||||
Certificates []Certificate `json:"certificates,omitempty"`
|
Certificates []Certificate `json:"certificates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
dynamic "github.com/traefik/traefik/v2/pkg/config/dynamic"
|
dynamic "github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||||
|
tls "github.com/traefik/traefik/v2/pkg/tls"
|
||||||
types "github.com/traefik/traefik/v2/pkg/types"
|
types "github.com/traefik/traefik/v2/pkg/types"
|
||||||
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
|
@ -1450,6 +1451,11 @@ func (in *TLSStoreSpec) DeepCopyInto(out *TLSStoreSpec) {
|
||||||
*out = new(Certificate)
|
*out = new(Certificate)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.DefaultGeneratedCert != nil {
|
||||||
|
in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert
|
||||||
|
*out = new(tls.GeneratedCert)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
if in.Certificates != nil {
|
if in.Certificates != nil {
|
||||||
in, out := &in.Certificates, &out.Certificates
|
in, out := &in.Certificates, &out.Certificates
|
||||||
*out = make([]Certificate, len(*in))
|
*out = make([]Certificate, len(*in))
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/traefik/traefik/v2/pkg/log"
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
"github.com/traefik/traefik/v2/pkg/tls/generate"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -101,55 +100,8 @@ func (f FileOrContent) Read() ([]byte, error) {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTLSConfig creates a TLS config from Certificate structures.
|
// AppendCertificate appends a Certificate to a certificates map keyed by store name.
|
||||||
func (c *Certificates) CreateTLSConfig(entryPointName string) (*tls.Config, error) {
|
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, storeName string) error {
|
||||||
config := &tls.Config{}
|
|
||||||
domainsCertificates := make(map[string]map[string]*tls.Certificate)
|
|
||||||
|
|
||||||
if c.isEmpty() {
|
|
||||||
config.Certificates = []tls.Certificate{}
|
|
||||||
|
|
||||||
cert, err := generate.DefaultCertificate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Certificates = append(config.Certificates, *cert)
|
|
||||||
} else {
|
|
||||||
for _, certificate := range *c {
|
|
||||||
err := certificate.AppendCertificate(domainsCertificates, entryPointName)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to add a certificate to the entryPoint %q : %v", entryPointName, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, certDom := range domainsCertificates {
|
|
||||||
for _, cert := range certDom {
|
|
||||||
config.Certificates = append(config.Certificates, *cert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isEmpty checks if the certificates list is empty.
|
|
||||||
func (c *Certificates) isEmpty() bool {
|
|
||||||
if len(*c) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
var key int
|
|
||||||
for _, cert := range *c {
|
|
||||||
if len(cert.CertFile.String()) != 0 && len(cert.KeyFile.String()) != 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
key++
|
|
||||||
}
|
|
||||||
return key == len(*c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendCertificate appends a Certificate to a certificates map keyed by entrypoint.
|
|
||||||
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, ep string) error {
|
|
||||||
certContent, err := c.CertFile.Read()
|
certContent, err := c.CertFile.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read CertFile : %w", err)
|
return fmt.Errorf("unable to read CertFile : %w", err)
|
||||||
|
@ -171,7 +123,6 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
||||||
SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
|
SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
|
||||||
}
|
}
|
||||||
if parsedCert.DNSNames != nil {
|
if parsedCert.DNSNames != nil {
|
||||||
sort.Strings(parsedCert.DNSNames)
|
|
||||||
for _, dnsName := range parsedCert.DNSNames {
|
for _, dnsName := range parsedCert.DNSNames {
|
||||||
if dnsName != parsedCert.Subject.CommonName {
|
if dnsName != parsedCert.Subject.CommonName {
|
||||||
SANs = append(SANs, strings.ToLower(dnsName))
|
SANs = append(SANs, strings.ToLower(dnsName))
|
||||||
|
@ -185,13 +136,16 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guarantees the order to produce a unique cert key.
|
||||||
|
sort.Strings(SANs)
|
||||||
certKey := strings.Join(SANs, ",")
|
certKey := strings.Join(SANs, ",")
|
||||||
|
|
||||||
certExists := false
|
certExists := false
|
||||||
if certs[ep] == nil {
|
if certs[storeName] == nil {
|
||||||
certs[ep] = make(map[string]*tls.Certificate)
|
certs[storeName] = make(map[string]*tls.Certificate)
|
||||||
} else {
|
} else {
|
||||||
for domains := range certs[ep] {
|
for domains := range certs[storeName] {
|
||||||
if domains == certKey {
|
if domains == certKey {
|
||||||
certExists = true
|
certExists = true
|
||||||
break
|
break
|
||||||
|
@ -199,10 +153,10 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if certExists {
|
if certExists {
|
||||||
log.Debugf("Skipping addition of certificate for domain(s) %q, to EntryPoint %s, as it already exists for this Entrypoint.", certKey, ep)
|
log.Debugf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Adding certificate for domain(s) %s", certKey)
|
log.Debugf("Adding certificate for domain(s) %s", certKey)
|
||||||
certs[ep][certKey] = &tlsCert
|
certs[storeName][certKey] = &tlsCert
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -22,8 +22,11 @@ type CertificateStore struct {
|
||||||
|
|
||||||
// NewCertificateStore create a store for dynamic certificates.
|
// NewCertificateStore create a store for dynamic certificates.
|
||||||
func NewCertificateStore() *CertificateStore {
|
func NewCertificateStore() *CertificateStore {
|
||||||
|
s := &safe.Safe{}
|
||||||
|
s.Set(make(map[string]*tls.Certificate))
|
||||||
|
|
||||||
return &CertificateStore{
|
return &CertificateStore{
|
||||||
DynamicCerts: &safe.Safe{},
|
DynamicCerts: s,
|
||||||
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,6 +117,45 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCertificate returns the first certificate matching all the given domains.
|
||||||
|
func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(domains)
|
||||||
|
domainsKey := strings.Join(domains, ",")
|
||||||
|
|
||||||
|
if cert, ok := c.CertCache.Get(domainsKey); ok {
|
||||||
|
return cert.(*tls.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||||
|
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||||
|
if domainsKey == certDomains {
|
||||||
|
c.CertCache.SetDefault(domainsKey, cert)
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchedDomains []string
|
||||||
|
for _, certDomain := range strings.Split(certDomains, ",") {
|
||||||
|
for _, checkDomain := range domains {
|
||||||
|
if certDomain == checkDomain {
|
||||||
|
matchedDomains = append(matchedDomains, certDomain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matchedDomains) == len(domains) {
|
||||||
|
c.CertCache.SetDefault(domainsKey, cert)
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ResetCache clears the cache in the store.
|
// ResetCache clears the cache in the store.
|
||||||
func (c CertificateStore) ResetCache() {
|
func (c CertificateStore) ResetCache() {
|
||||||
if c.CertCache != nil {
|
if c.CertCache != nil {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
|
import "github.com/traefik/traefik/v2/pkg/types"
|
||||||
|
|
||||||
const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
|
const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -37,6 +39,17 @@ func (o *Options) SetDefaults() {
|
||||||
// Store holds the options for a given Store.
|
// Store holds the options for a given Store.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty" toml:"defaultCertificate,omitempty" yaml:"defaultCertificate,omitempty" export:"true"`
|
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty" toml:"defaultCertificate,omitempty" yaml:"defaultCertificate,omitempty" export:"true"`
|
||||||
|
DefaultGeneratedCert *GeneratedCert `json:"defaultGeneratedCert,omitempty" toml:"defaultGeneratedCert,omitempty" yaml:"defaultGeneratedCert,omitempty" export:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
||||||
|
// GeneratedCert defines the default generated certificate configuration.
|
||||||
|
type GeneratedCert struct {
|
||||||
|
// Resolver is the name of the resolver that will be used to issue the DefaultCertificate.
|
||||||
|
Resolver string `json:"resolver,omitempty" toml:"resolver,omitempty" yaml:"resolver,omitempty" export:"true"`
|
||||||
|
// Domain is the domain definition for the DefaultCertificate.
|
||||||
|
Domain *types.Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty" export:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
|
|
@ -6,8 +6,10 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/traefik/traefik/v2/pkg/log"
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
|
@ -81,17 +83,6 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
||||||
m.storesConfig[tlsalpn01.ACMETLS1Protocol] = Store{}
|
m.storesConfig[tlsalpn01.ACMETLS1Protocol] = Store{}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stores = make(map[string]*CertificateStore)
|
|
||||||
for storeName, storeConfig := range m.storesConfig {
|
|
||||||
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, storeName))
|
|
||||||
store, err := buildCertificateStore(ctxStore, storeConfig, storeName)
|
|
||||||
if err != nil {
|
|
||||||
log.FromContext(ctxStore).Errorf("Error while creating certificate store: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.stores[storeName] = store
|
|
||||||
}
|
|
||||||
|
|
||||||
storesCertificates := make(map[string]map[string]*tls.Certificate)
|
storesCertificates := make(map[string]map[string]*tls.Certificate)
|
||||||
for _, conf := range certs {
|
for _, conf := range certs {
|
||||||
if len(conf.Stores) == 0 {
|
if len(conf.Stores) == 0 {
|
||||||
|
@ -99,24 +90,66 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
||||||
log.FromContext(ctx).Debugf("No store is defined to add the certificate %s, it will be added to the default store.",
|
log.FromContext(ctx).Debugf("No store is defined to add the certificate %s, it will be added to the default store.",
|
||||||
conf.Certificate.GetTruncatedCertificateName())
|
conf.Certificate.GetTruncatedCertificateName())
|
||||||
}
|
}
|
||||||
conf.Stores = []string{"default"}
|
conf.Stores = []string{DefaultTLSStoreName}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, store := range conf.Stores {
|
for _, store := range conf.Stores {
|
||||||
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, store))
|
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, store))
|
||||||
if err := conf.Certificate.AppendCertificate(storesCertificates, store); err != nil {
|
|
||||||
|
if _, ok := m.storesConfig[store]; !ok {
|
||||||
|
m.storesConfig[store] = Store{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conf.Certificate.AppendCertificate(storesCertificates, store)
|
||||||
|
if err != nil {
|
||||||
log.FromContext(ctxStore).Errorf("Unable to append certificate %s to store: %v", conf.Certificate.GetTruncatedCertificateName(), err)
|
log.FromContext(ctxStore).Errorf("Unable to append certificate %s to store: %v", conf.Certificate.GetTruncatedCertificateName(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for storeName, certs := range storesCertificates {
|
m.stores = make(map[string]*CertificateStore)
|
||||||
st, ok := m.stores[storeName]
|
|
||||||
if !ok {
|
for storeName, storeConfig := range m.storesConfig {
|
||||||
st, _ = buildCertificateStore(context.Background(), Store{}, storeName)
|
st := NewCertificateStore()
|
||||||
m.stores[storeName] = st
|
m.stores[storeName] = st
|
||||||
}
|
|
||||||
|
if certs, ok := storesCertificates[storeName]; ok {
|
||||||
st.DynamicCerts.Set(certs)
|
st.DynamicCerts.Set(certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a default cert for the ACME store does not make any sense, so generating one is a waste.
|
||||||
|
if storeName == tlsalpn01.ACMETLS1Protocol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, storeName))
|
||||||
|
|
||||||
|
certificate, err := getDefaultCertificate(ctxStore, storeConfig, st)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctxStore).Errorf("Error while creating certificate store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
st.DefaultCertificate = certificate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeDomains sanitizes the domain definition Main and SANS,
|
||||||
|
// and returns them as a slice.
|
||||||
|
// This func apply the same sanitization as the ACME provider do before resolving certificates.
|
||||||
|
func sanitizeDomains(domain types.Domain) ([]string, error) {
|
||||||
|
domains := domain.ToStrArray()
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return nil, errors.New("no domain was given")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleanDomains []string
|
||||||
|
for _, domain := range domains {
|
||||||
|
canonicalDomain := types.CanonicalDomain(domain)
|
||||||
|
cleanDomain := dns01.UnFqdn(canonicalDomain)
|
||||||
|
cleanDomains = append(cleanDomains, cleanDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanDomains, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the TLS configuration to use for a given store / configuration.
|
// Get gets the TLS configuration to use for a given store / configuration.
|
||||||
|
@ -234,32 +267,37 @@ func (m *Manager) GetStore(storeName string) *CertificateStore {
|
||||||
return m.getStore(storeName)
|
return m.getStore(storeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCertificateStore(ctx context.Context, tlsStore Store, storename string) (*CertificateStore, error) {
|
func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*tls.Certificate, error) {
|
||||||
certificateStore := NewCertificateStore()
|
|
||||||
certificateStore.DynamicCerts.Set(make(map[string]*tls.Certificate))
|
|
||||||
|
|
||||||
if tlsStore.DefaultCertificate != nil {
|
if tlsStore.DefaultCertificate != nil {
|
||||||
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
|
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return certificateStore, err
|
return nil, err
|
||||||
}
|
|
||||||
certificateStore.DefaultCertificate = cert
|
|
||||||
return certificateStore, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// a default cert for the ACME store does not make any sense, so generating one
|
return cert, nil
|
||||||
// is a waste.
|
|
||||||
if storename == tlsalpn01.ACMETLS1Protocol {
|
|
||||||
return certificateStore, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.FromContext(ctx).Debug("No default certificate, generating one")
|
defaultCert, err := generate.DefaultCertificate()
|
||||||
cert, err := generate.DefaultCertificate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return certificateStore, err
|
return nil, err
|
||||||
}
|
}
|
||||||
certificateStore.DefaultCertificate = cert
|
|
||||||
return certificateStore, nil
|
if tlsStore.DefaultGeneratedCert != nil && tlsStore.DefaultGeneratedCert.Domain != nil && tlsStore.DefaultGeneratedCert.Resolver != "" {
|
||||||
|
domains, err := sanitizeDomains(*tlsStore.DefaultGeneratedCert.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return defaultCert, fmt.Errorf("falling back to the internal generated certificate because invalid domains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultACMECert := st.GetCertificate(domains)
|
||||||
|
if defaultACMECert == nil {
|
||||||
|
return defaultCert, fmt.Errorf("unable to find certificate for domains %q: falling back to the internal generated certificate", strings.Join(domains, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultACMECert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.FromContext(ctx).Debug("No default certificate, fallback to the internal generated certificate")
|
||||||
|
return defaultCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI.
|
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI.
|
||||||
|
|
|
@ -29,6 +29,10 @@ THE SOFTWARE.
|
||||||
|
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
types "github.com/traefik/traefik/v2/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *CertAndStores) DeepCopyInto(out *CertAndStores) {
|
func (in *CertAndStores) DeepCopyInto(out *CertAndStores) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -72,6 +76,27 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *GeneratedCert) DeepCopyInto(out *GeneratedCert) {
|
||||||
|
*out = *in
|
||||||
|
if in.Domain != nil {
|
||||||
|
in, out := &in.Domain, &out.Domain
|
||||||
|
*out = new(types.Domain)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedCert.
|
||||||
|
func (in *GeneratedCert) DeepCopy() *GeneratedCert {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(GeneratedCert)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *Options) DeepCopyInto(out *Options) {
|
func (in *Options) DeepCopyInto(out *Options) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -112,6 +137,11 @@ func (in *Store) DeepCopyInto(out *Store) {
|
||||||
*out = new(Certificate)
|
*out = new(Certificate)
|
||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
|
if in.DefaultGeneratedCert != nil {
|
||||||
|
in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert
|
||||||
|
*out = new(GeneratedCert)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue