OCSP stapling
This commit is contained in:
parent
2949995abc
commit
b39ee8ede5
@ -182,7 +182,9 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err
|
||||
|
||||
// ACME
|
||||
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(staticConfiguration.OCSP)
|
||||
routinesPool.GoCtx(tlsManager.Run)
|
||||
|
||||
httpChallengeProvider := acme.NewChallengeHTTP()
|
||||
|
||||
tlsChallengeProvider := acme.NewChallengeTLSALPN()
|
||||
|
71
docs/content/https/ocsp.md
Normal file
71
docs/content/https/ocsp.md
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "Traefik OCSP Documentation"
|
||||
description: "Learn how to configure Traefik to use OCSP. Read the technical documentation."
|
||||
---
|
||||
|
||||
# OCSP
|
||||
|
||||
Check certificate status and perform OCSP stapling.
|
||||
{: .subtitle }
|
||||
|
||||
## Overview
|
||||
|
||||
### OCSP Stapling
|
||||
|
||||
When OCSP is enabled, Traefik checks the status of every certificate in the store that provides an OCSP responder URL,
|
||||
including the default certificate, and staples the OCSP response to the TLS handshake.
|
||||
The OCSP check is performed when the certificate is loaded,
|
||||
and once every hour until it is successful at the halfway point before the update date.
|
||||
|
||||
### Caching
|
||||
|
||||
Traefik caches the OCSP response as long as the associated certificate is provided by the configuration.
|
||||
When a certificate is no longer provided,
|
||||
the OCSP response has a 24 hour TTL waiting to be provided again or eventually removed.
|
||||
The OCSP response is cached in memory and is not persisted between Traefik restarts.
|
||||
|
||||
## Configuration
|
||||
|
||||
### General
|
||||
|
||||
Enabling OCSP is part of the [static configuration](../getting-started/configuration-overview.md#the-static-configuration).
|
||||
It can be defined by using a file (YAML or TOML) or CLI arguments:
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
## Static configuration
|
||||
ocsp: {}
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[ocsp]
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--ocsp=true
|
||||
```
|
||||
|
||||
### Responder Overrides
|
||||
|
||||
The `responderOverrides` option defines the OCSP responder URLs to use instead of the one provided by the certificate.
|
||||
This is useful when you want to use a different OCSP responder.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
## Static configuration
|
||||
ocsp:
|
||||
responderOverrides:
|
||||
foo: bar
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[ocsp]
|
||||
[ocsp.responderOverrides]
|
||||
foo = "bar"
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
-ocsp.responderoverrides.foo=bar
|
||||
```
|
71
docs/content/reference/install-configuration/tls/ocsp.md
Normal file
71
docs/content/reference/install-configuration/tls/ocsp.md
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
title: "Traefik OCSP Documentation"
|
||||
description: "Learn how to configure Traefik to use OCSP. Read the technical documentation."
|
||||
---
|
||||
|
||||
# OCSP
|
||||
|
||||
Check certificate status and perform OCSP stapling.
|
||||
{: .subtitle }
|
||||
|
||||
## Overview
|
||||
|
||||
### OCSP Stapling
|
||||
|
||||
When OCSP is enabled, Traefik checks the status of every certificate in the store that provides an OCSP responder URL,
|
||||
including the default certificate, and staples the OCSP response to the TLS handshake.
|
||||
The OCSP check is performed when the certificate is loaded,
|
||||
and once every hour until it is successful at the halfway point before the update date.
|
||||
|
||||
### Caching
|
||||
|
||||
Traefik caches the OCSP response as long as the associated certificate is provided by the configuration.
|
||||
When a certificate is no longer provided,
|
||||
the OCSP response has a 24 hour TTL waiting to be provided again or eventually removed.
|
||||
The OCSP response is cached in memory and is not persisted between Traefik restarts.
|
||||
|
||||
## Configuration
|
||||
|
||||
### General
|
||||
|
||||
Enabling OCSP is part of the [static configuration](../getting-started/configuration-overview.md#the-static-configuration).
|
||||
It can be defined by using a file (YAML or TOML) or CLI arguments:
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
## Static configuration
|
||||
ocsp: {}
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[ocsp]
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--ocsp=true
|
||||
```
|
||||
|
||||
### Responder Overrides
|
||||
|
||||
The `responderOverrides` option defines the OCSP responder URLs to use instead of the one provided by the certificate.
|
||||
This is useful when you want to use a different OCSP responder.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
## Static configuration
|
||||
ocsp:
|
||||
responderOverrides:
|
||||
foo: bar
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[ocsp]
|
||||
[ocsp.responderOverrides]
|
||||
foo = "bar"
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
-ocsp.responderoverrides.foo=bar
|
||||
```
|
@ -642,6 +642,12 @@ Prefix to use for metrics collection. (Default: ```traefik```)
|
||||
`--metrics.statsd.pushinterval`:
|
||||
StatsD push interval. (Default: ```10```)
|
||||
|
||||
`--ocsp`:
|
||||
OCSP configuration. (Default: ```false```)
|
||||
|
||||
`--ocsp.responderoverrides.<name>`:
|
||||
Defines a map of OCSP responders to replace for querying OCSP servers.
|
||||
|
||||
`--ping`:
|
||||
Enable ping. (Default: ```false```)
|
||||
|
||||
|
@ -642,6 +642,12 @@ Prefix to use for metrics collection. (Default: ```traefik```)
|
||||
`TRAEFIK_METRICS_STATSD_PUSHINTERVAL`:
|
||||
StatsD push interval. (Default: ```10```)
|
||||
|
||||
`TRAEFIK_OCSP`:
|
||||
OCSP configuration. (Default: ```false```)
|
||||
|
||||
`TRAEFIK_OCSP_RESPONDEROVERRIDES_<NAME>`:
|
||||
Defines a map of OCSP responders to replace for querying OCSP servers.
|
||||
|
||||
`TRAEFIK_PING`:
|
||||
Enable ping. (Default: ```false```)
|
||||
|
||||
|
@ -609,3 +609,8 @@
|
||||
|
||||
[spiffe]
|
||||
workloadAPIAddr = "foobar"
|
||||
|
||||
[ocsp]
|
||||
[ocsp.responderOverrides]
|
||||
name0 = "foobar"
|
||||
name1 = "foobar"
|
||||
|
@ -675,3 +675,7 @@ core:
|
||||
defaultRuleSyntax: foobar
|
||||
spiffe:
|
||||
workloadAPIAddr: foobar
|
||||
ocsp:
|
||||
responderOverrides:
|
||||
name0: foobar
|
||||
name1: foobar
|
||||
|
@ -110,6 +110,7 @@ nav:
|
||||
- 'Let''s Encrypt': 'https/acme.md'
|
||||
- 'Tailscale': 'https/tailscale.md'
|
||||
- 'SPIFFE': 'https/spiffe.md'
|
||||
- 'OCSP': 'https/ocsp.md'
|
||||
- 'Middlewares':
|
||||
- 'Overview': 'middlewares/overview.md'
|
||||
- 'HTTP':
|
||||
@ -227,6 +228,7 @@ nav:
|
||||
- "ACME" : 'reference/install-configuration/tls/certificate-resolvers/acme.md'
|
||||
- "Tailscale" : 'reference/install-configuration/tls/certificate-resolvers/tailscale.md'
|
||||
- "SPIFFE" : 'reference/install-configuration/tls/spiffe.md'
|
||||
- "OCSP" : 'reference/install-configuration/tls/ocsp.md'
|
||||
- 'Observability':
|
||||
- 'Metrics' : 'reference/install-configuration/observability/metrics.md'
|
||||
- 'Tracing': 'reference/install-configuration/observability/tracing.md'
|
||||
|
2
go.mod
2
go.mod
@ -95,6 +95,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0
|
||||
go.opentelemetry.io/otel/trace v1.34.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/mod v0.23.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/sync v0.13.0
|
||||
@ -366,7 +367,6 @@ require (
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
|
11
integration/fixtures/ocsp/ca.crt
Normal file
11
integration/fixtures/ocsp/ca.crt
Normal file
@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBhjCCASygAwIBAgIBATAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB
|
||||
MB4XDTI1MDQyNDEzNTIzOFoXDTM1MDQyMjEzNTIzOFowEjEQMA4GA1UEAxMHVGVz
|
||||
dCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPp8MoNUBbUxp3jW6FcDH+lg
|
||||
Zft1SIpnGjkMVjLSbW9EzmRQ/oMRHQqJvE7wJbwDs/JUTigRtfZL0vOojnhHcPej
|
||||
czBxMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQW
|
||||
BBQXRlWLK295lmDy+931a4Ha8XVNNjAsBggrBgEFBQcBAQQgMB4wHAYIKwYBBQUH
|
||||
MAGGEG9jc3AuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwIDSAAwRQIgYH6lnce9jxcp
|
||||
YIVhY4z55rnOKXqaI/5rUQKwjJ3dRsUCIQDThtkFgOPT/67xOYCTCEVSMSTwh2Gq
|
||||
jbeucU+4c/InVg==
|
||||
-----END CERTIFICATE-----
|
5
integration/fixtures/ocsp/ca.key
Normal file
5
integration/fixtures/ocsp/ca.key
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIGV6FPfHeA42xfjVtpnyATG6tKCCu0QoY0OlBR/0xn2toAoGCCqGSM49
|
||||
AwEHoUQDQgAE+nwyg1QFtTGneNboVwMf6WBl+3VIimcaOQxWMtJtb0TOZFD+gxEd
|
||||
Com8TvAlvAOz8lROKBG19kvS86iOeEdw9w==
|
||||
-----END EC PRIVATE KEY-----
|
11
integration/fixtures/ocsp/default.crt
Normal file
11
integration/fixtures/ocsp/default.crt
Normal file
@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjzCCATWgAwIBAgIIGDlFgswljYAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMH
|
||||
VGVzdCBDQTAeFw0yNTA0MjQxMzUyMzhaFw0yNjA0MjQxMzUyMzhaMBgxFjAUBgNV
|
||||
BAMTDWRlZmF1bHQubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGABZ/
|
||||
zezTMQBwmmw3aifU0OkDQ4ZzxGG7dR93svJPgYnP7TpBVtPrxy0WgVZbbCHv0Srl
|
||||
PlpO9rFkKf3D4E6Qo28wbTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAf
|
||||
BgNVHSMEGDAWgBQXRlWLK295lmDy+931a4Ha8XVNNjAsBggrBgEFBQcBAQQgMB4w
|
||||
HAYIKwYBBQUHMAGGEG9jc3AuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwIDSAAwRQIh
|
||||
AJMF7RkU0BtNZlHf//PPgpPfDJybnYMIoX1Ek4I8JZ+QAiBpxjzeFE9jwqcJnx5X
|
||||
KnOJMbgfvJliZZgVSuXBbulzAA==
|
||||
-----END CERTIFICATE-----
|
5
integration/fixtures/ocsp/default.key
Normal file
5
integration/fixtures/ocsp/default.key
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIO4UluA82wXVkaVH0m6oFGWyC8mzVcc7H9MI0ltXgkNuoAoGCCqGSM49
|
||||
AwEHoUQDQgAEBgAWf83s0zEAcJpsN2on1NDpA0OGc8Rhu3Ufd7LyT4GJz+06QVbT
|
||||
68ctFoFWW2wh79Eq5T5aTvaxZCn9w+BOkA==
|
||||
-----END EC PRIVATE KEY-----
|
100
integration/fixtures/ocsp/gencert.go
Normal file
100
integration/fixtures/ocsp/gencert.go
Normal file
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// generate CA
|
||||
caKey, caCert := generateCA("Test CA")
|
||||
saveKeyAndCert("integration/fixtures/ocsp/ca.key", "integration/fixtures/ocsp/ca.crt", caKey, caCert)
|
||||
|
||||
// server certificate
|
||||
serverKey, serverCert := generateCert("server.local", caKey, caCert)
|
||||
saveKeyAndCert("integration/fixtures/ocsp/server.key", "integration/fixtures/ocsp/server.crt", serverKey, serverCert)
|
||||
|
||||
// default certificate
|
||||
defaultKey, defaultCert := generateCert("default.local", caKey, caCert)
|
||||
saveKeyAndCert("integration/fixtures/ocsp/default.key", "integration/fixtures/ocsp/default.crt", defaultKey, defaultCert)
|
||||
}
|
||||
|
||||
func generateCA(commonName string) (*ecdsa.PrivateKey, *x509.Certificate) {
|
||||
// generate a private key for the CA
|
||||
caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
|
||||
// create a self-signed CA certificate
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: commonName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), // 10 ans
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: 1,
|
||||
OCSPServer: []string{"ocsp.example.com"},
|
||||
}
|
||||
|
||||
caCertDER, _ := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
caCert, _ := x509.ParseCertificate(caCertDER)
|
||||
|
||||
return caKey, caCert
|
||||
}
|
||||
|
||||
func generateCert(commonName string, caKey *ecdsa.PrivateKey, caCert *x509.Certificate) (*ecdsa.PrivateKey, *x509.Certificate) {
|
||||
// create a private key for the certificate
|
||||
certKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
|
||||
// create a certificate signed by the CA
|
||||
certTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: commonName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(1 * 365 * 24 * time.Hour), // 1 an
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
OCSPServer: []string{"ocsp.example.com"},
|
||||
}
|
||||
|
||||
certDER, _ := x509.CreateCertificate(rand.Reader, certTemplate, caCert, &certKey.PublicKey, caKey)
|
||||
cert, _ := x509.ParseCertificate(certDER)
|
||||
|
||||
return certKey, cert
|
||||
}
|
||||
|
||||
func saveKeyAndCert(keyFile, certFile string, key *ecdsa.PrivateKey, cert *x509.Certificate) {
|
||||
// save the private key
|
||||
keyOut, _ := os.Create(keyFile)
|
||||
defer keyOut.Close()
|
||||
|
||||
// Marshal the private key to ASN.1 DER format
|
||||
privateKey, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKey})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// save the certificate
|
||||
certOut, _ := os.Create(certFile)
|
||||
defer certOut.Close()
|
||||
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
11
integration/fixtures/ocsp/server.crt
Normal file
11
integration/fixtures/ocsp/server.crt
Normal file
@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjjCCATSgAwIBAgIIGDlFgswgB3AwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMH
|
||||
VGVzdCBDQTAeFw0yNTA0MjQxMzUyMzhaFw0yNjA0MjQxMzUyMzhaMBcxFTATBgNV
|
||||
BAMTDHNlcnZlci5sb2NhbDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHpjZoVk
|
||||
Qh15gTa26KMJfvzfVgGHGicUDg1UYppKAMY83rxSXqRHcVFAFRqWDTgCQRy6hPq+
|
||||
6p5OwBziC2X/SOejbzBtMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMB8G
|
||||
A1UdIwQYMBaAFBdGVYsrb3mWYPL73fVrgdrxdU02MCwGCCsGAQUFBwEBBCAwHjAc
|
||||
BggrBgEFBQcwAYYQb2NzcC5leGFtcGxlLmNvbTAKBggqhkjOPQQDAgNIADBFAiEA
|
||||
mp5LQixMUFh5h8yF1EtFsi4MKrO+dzD68TqIhq1rKjUCIEbB++M8qO4gtqjv8d06
|
||||
AzSLTEfgNCmM574JI46YAKVx
|
||||
-----END CERTIFICATE-----
|
5
integration/fixtures/ocsp/server.key
Normal file
5
integration/fixtures/ocsp/server.key
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIFpVKKKxvw6cZe7hwRLHgXIsWiJYUQ66PKzO6iXINUH0oAoGCCqGSM49
|
||||
AwEHoUQDQgAEemNmhWRCHXmBNrboowl+/N9WAYcaJxQODVRimkoAxjzevFJepEdx
|
||||
UUAVGpYNOAJBHLqE+r7qnk7AHOILZf9I5w==
|
||||
-----END EC PRIVATE KEY-----
|
27
integration/fixtures/ocsp/simple.toml
Normal file
27
integration/fixtures/ocsp/simple.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.web]
|
||||
address = ":8000"
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
[ocsp.responderOverrides]
|
||||
ocsp.example.com = "{{ .ResponderURL }}"
|
||||
|
||||
[log]
|
||||
level="debug"
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
[[tls.certificates]]
|
||||
certFile = "fixtures/ocsp/server.crt"
|
||||
keyFile = "fixtures/ocsp/server.key"
|
||||
|
||||
[tls.stores]
|
||||
[tls.stores.default.defaultCertificate]
|
||||
certFile = "fixtures/ocsp/default.crt"
|
||||
keyFile = "fixtures/ocsp/default.key"
|
@ -3,7 +3,9 @@ package integration
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -24,6 +26,7 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/traefik/traefik/v3/integration/try"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// SimpleSuite tests suite.
|
||||
@ -1598,6 +1601,132 @@ func (s *SimpleSuite) TestMaxHeaderBytes() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestSimpleOCSP() {
|
||||
defaultCert, err := tls.LoadX509KeyPair("fixtures/ocsp/default.crt", "fixtures/ocsp/default.key")
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
serverCert, err := tls.LoadX509KeyPair("fixtures/ocsp/server.crt", "fixtures/ocsp/server.key")
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
defaultOCSPResponseTmpl := ocsp.Response{
|
||||
SerialNumber: defaultCert.Leaf.SerialNumber,
|
||||
Status: ocsp.Good,
|
||||
ThisUpdate: defaultCert.Leaf.NotBefore,
|
||||
NextUpdate: defaultCert.Leaf.NotAfter,
|
||||
}
|
||||
defaultOCSPResponse, err := ocsp.CreateResponse(defaultCert.Leaf, defaultCert.Leaf, defaultOCSPResponseTmpl, defaultCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
serverOCSPResponseTmpl := ocsp.Response{
|
||||
SerialNumber: serverCert.Leaf.SerialNumber,
|
||||
Status: ocsp.Good,
|
||||
ThisUpdate: serverCert.Leaf.NotBefore,
|
||||
NextUpdate: serverCert.Leaf.NotAfter,
|
||||
}
|
||||
serverOCSPResponse, err := ocsp.CreateResponse(serverCert.Leaf, serverCert.Leaf, serverOCSPResponseTmpl, serverCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
responderCalled := make(chan struct{})
|
||||
responder := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
ct := req.Header.Get("Content-Type")
|
||||
assert.Equal(s.T(), "application/ocsp-request", ct)
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(reqBytes)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
var ocspResponse []byte
|
||||
switch ocspReq.SerialNumber.String() {
|
||||
case defaultCert.Leaf.SerialNumber.String():
|
||||
ocspResponse = defaultOCSPResponse
|
||||
case serverCert.Leaf.SerialNumber.String():
|
||||
ocspResponse = serverOCSPResponse
|
||||
default:
|
||||
s.T().Fatalf("Unexpected OCSP request for serial number: %s", ocspReq.SerialNumber)
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
|
||||
_, err = rw.Write(ocspResponse)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
responderCalled <- struct{}{}
|
||||
}))
|
||||
s.T().Cleanup(responder.Close)
|
||||
|
||||
file := s.adaptFile("fixtures/ocsp/simple.toml", struct {
|
||||
ResponderURL string
|
||||
}{responder.URL})
|
||||
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
select {
|
||||
case <-responderCalled:
|
||||
case <-time.After(5 * time.Second):
|
||||
s.T().Fatal("OCSP responder was not called")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-responderCalled:
|
||||
case <-time.After(5 * time.Second):
|
||||
s.T().Fatal("OCSP responder was not called")
|
||||
}
|
||||
|
||||
// Check that the response is stapled.
|
||||
|
||||
// Create a TLS client configuration that checks for OCSP stapling for the default cert.
|
||||
var verifyCallCount int
|
||||
clientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "unknown",
|
||||
VerifyConnection: func(state tls.ConnectionState) error {
|
||||
s.T().Helper()
|
||||
|
||||
verifyCallCount++
|
||||
assert.Equal(s.T(), "default.local", state.PeerCertificates[0].Subject.CommonName)
|
||||
assert.Equal(s.T(), defaultOCSPResponse, state.OCSPResponse)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Connect to the server and verify OCSP stapling.
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", "127.0.0.1:8000", clientConfig)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
_ = conn.Close()
|
||||
})
|
||||
|
||||
assert.Equal(s.T(), 1, verifyCallCount)
|
||||
|
||||
// Create a TLS client configuration that checks for OCSP stapling for a cert in the store.
|
||||
verifyCallCount = 0
|
||||
clientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "server.local",
|
||||
VerifyConnection: func(state tls.ConnectionState) error {
|
||||
s.T().Helper()
|
||||
|
||||
verifyCallCount++
|
||||
assert.Equal(s.T(), "server.local", state.PeerCertificates[0].Subject.CommonName)
|
||||
assert.Equal(s.T(), serverOCSPResponse, state.OCSPResponse)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Connect to the server and verify OCSP stapling.
|
||||
conn, err = tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", "127.0.0.1:8000", clientConfig)
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
s.T().Cleanup(func() {
|
||||
_ = conn.Close()
|
||||
})
|
||||
|
||||
assert.Equal(s.T(), 1, verifyCallCount)
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestSanitizePath() {
|
||||
s.createComposeProject("base")
|
||||
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/provider/kv/zk"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/nomad"
|
||||
"github.com/traefik/traefik/v3/pkg/provider/rest"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
@ -80,6 +81,8 @@ type Configuration struct {
|
||||
Core *Core `description:"Core controls." json:"core,omitempty" toml:"core,omitempty" yaml:"core,omitempty" export:"true"`
|
||||
|
||||
Spiffe *SpiffeClientConfig `description:"SPIFFE integration configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" export:"true"`
|
||||
|
||||
OCSP *tls.OCSPConfig `description:"OCSP configuration." json:"ocsp,omitempty" toml:"ocsp,omitempty" yaml:"ocsp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
}
|
||||
|
||||
// Core configures Traefik core behavior.
|
||||
@ -424,6 +427,14 @@ func (c *Configuration) ValidateConfiguration() error {
|
||||
return errors.New("API basePath must be a valid absolute path")
|
||||
}
|
||||
|
||||
if c.OCSP != nil {
|
||||
for responderURL, url := range c.OCSP.ResponderOverrides {
|
||||
if url == "" {
|
||||
return fmt.Errorf("OCSP responder override value for %s cannot be empty", responderURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -325,7 +325,7 @@ func TestRouterManager_Get(t *testing.T) {
|
||||
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
@ -712,7 +712,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(), nil, test.tlsOptions, nil)
|
||||
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
@ -794,7 +794,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
||||
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
@ -873,7 +873,7 @@ func BenchmarkRouterServe(b *testing.B) {
|
||||
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(b, err)
|
||||
|
@ -347,7 +347,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
||||
dialerManager := tcp2.NewDialerManager(nil)
|
||||
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
|
||||
serviceManager := tcp.NewManager(conf, dialerManager)
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
tlsManager.UpdateConfigs(
|
||||
t.Context(),
|
||||
map[string]traefiktls.Store{},
|
||||
@ -659,7 +659,7 @@ func TestDomainFronting(t *testing.T) {
|
||||
|
||||
serviceManager := tcp.NewManager(conf, tcp2.NewDialerManager(nil))
|
||||
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{})
|
||||
|
||||
httpsHandler := map[string]http.Handler{
|
||||
|
@ -172,7 +172,7 @@ func Test_Routing(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Creates the tlsManager and defines the TLS 1.0 and 1.2 TLSOptions.
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager := traefiktls.NewManager(nil)
|
||||
tlsManager.UpdateConfigs(
|
||||
t.Context(),
|
||||
map[string]traefiktls.Store{
|
||||
|
@ -55,7 +55,7 @@ func TestReuseService(t *testing.T) {
|
||||
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
tlsManager := tls.NewManager(nil)
|
||||
|
||||
dialerManager := tcp.NewDialerManager(nil)
|
||||
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
|
||||
@ -193,7 +193,7 @@ func TestServerResponseEmptyBackend(t *testing.T) {
|
||||
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
tlsManager := tls.NewManager(nil)
|
||||
|
||||
dialerManager := tcp.NewDialerManager(nil)
|
||||
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
|
||||
@ -239,7 +239,7 @@ func TestInternalServices(t *testing.T) {
|
||||
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, nil, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
tlsManager := tls.NewManager(nil)
|
||||
|
||||
dialerManager := tcp.NewDialerManager(nil)
|
||||
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -76,68 +75,6 @@ type Certificate struct {
|
||||
KeyFile types.FileOrContent `json:"keyFile,omitempty" toml:"keyFile,omitempty" yaml:"keyFile,omitempty" loggable:"false"`
|
||||
}
|
||||
|
||||
// AppendCertificate appends a Certificate to a certificates map keyed by store name.
|
||||
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, storeName string) error {
|
||||
certContent, err := c.CertFile.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read CertFile : %w", err)
|
||||
}
|
||||
|
||||
keyContent, err := c.KeyFile.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read KeyFile : %w", err)
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(certContent, keyContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate TLS certificate : %w", err)
|
||||
}
|
||||
|
||||
parsedCert, _ := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
|
||||
var SANs []string
|
||||
if parsedCert.Subject.CommonName != "" {
|
||||
SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
|
||||
}
|
||||
if parsedCert.DNSNames != nil {
|
||||
for _, dnsName := range parsedCert.DNSNames {
|
||||
if dnsName != parsedCert.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(dnsName))
|
||||
}
|
||||
}
|
||||
}
|
||||
if parsedCert.IPAddresses != nil {
|
||||
for _, ip := range parsedCert.IPAddresses {
|
||||
if ip.String() != parsedCert.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(ip.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guarantees the order to produce a unique cert key.
|
||||
sort.Strings(SANs)
|
||||
certKey := strings.Join(SANs, ",")
|
||||
|
||||
certExists := false
|
||||
if certs[storeName] == nil {
|
||||
certs[storeName] = make(map[string]*tls.Certificate)
|
||||
} else {
|
||||
for domains := range certs[storeName] {
|
||||
if domains == certKey {
|
||||
certExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if certExists {
|
||||
log.Debug().Msgf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
|
||||
} else {
|
||||
log.Debug().Msgf("Adding certificate for domain(s) %s", certKey)
|
||||
certs[storeName][certKey] = &tlsCert
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCertificate returns a tls.Certificate matching the configured CertFile and KeyFile.
|
||||
func (c *Certificate) GetCertificate() (tls.Certificate, error) {
|
||||
certContent, err := c.CertFile.Read()
|
||||
@ -169,24 +106,6 @@ func (c *Certificate) GetCertificateFromBytes() (tls.Certificate, error) {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// Set is the method to set the flag value, part of the flag.Value interface.
|
||||
// Set's argument is a string to be parsed to set the flag.
|
||||
// It's a comma-separated list, so we split it.
|
||||
func (c *Certificates) Set(value string) error {
|
||||
certificates := strings.Split(value, ";")
|
||||
for _, certificate := range certificates {
|
||||
files := strings.Split(certificate, ",")
|
||||
if len(files) != 2 {
|
||||
return fmt.Errorf("bad certificates format: %s", value)
|
||||
}
|
||||
*c = append(*c, Certificate{
|
||||
CertFile: types.FileOrContent(files[0]),
|
||||
KeyFile: types.FileOrContent(files[1]),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTruncatedCertificateName truncates the certificate name.
|
||||
func (c *Certificate) GetTruncatedCertificateName() string {
|
||||
certName := c.CertFile.String()
|
||||
|
@ -2,7 +2,7 @@ package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -13,57 +13,40 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/safe"
|
||||
)
|
||||
|
||||
// CertificateData holds runtime data for runtime TLS certificate handling.
|
||||
type CertificateData struct {
|
||||
Hash string
|
||||
Certificate *tls.Certificate
|
||||
}
|
||||
|
||||
// CertificateStore store for dynamic certificates.
|
||||
type CertificateStore struct {
|
||||
DynamicCerts *safe.Safe
|
||||
DefaultCertificate *tls.Certificate
|
||||
DefaultCertificate *CertificateData
|
||||
CertCache *cache.Cache
|
||||
|
||||
ocspStapler *ocspStapler
|
||||
}
|
||||
|
||||
// NewCertificateStore create a store for dynamic certificates.
|
||||
func NewCertificateStore() *CertificateStore {
|
||||
s := &safe.Safe{}
|
||||
s.Set(make(map[string]*tls.Certificate))
|
||||
func NewCertificateStore(ocspStapler *ocspStapler) *CertificateStore {
|
||||
var dynamicCerts safe.Safe
|
||||
dynamicCerts.Set(make(map[string]*CertificateData))
|
||||
|
||||
return &CertificateStore{
|
||||
DynamicCerts: s,
|
||||
DynamicCerts: &dynamicCerts,
|
||||
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
||||
ocspStapler: ocspStapler,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CertificateStore) getDefaultCertificateDomains() []string {
|
||||
var allCerts []string
|
||||
|
||||
if c.DefaultCertificate == nil {
|
||||
return allCerts
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Could not parse default certificate")
|
||||
return allCerts
|
||||
}
|
||||
|
||||
if len(x509Cert.Subject.CommonName) > 0 {
|
||||
allCerts = append(allCerts, x509Cert.Subject.CommonName)
|
||||
}
|
||||
|
||||
allCerts = append(allCerts, x509Cert.DNSNames...)
|
||||
|
||||
for _, ipSan := range x509Cert.IPAddresses {
|
||||
allCerts = append(allCerts, ipSan.String())
|
||||
}
|
||||
|
||||
return allCerts
|
||||
}
|
||||
|
||||
// GetAllDomains return a slice with all the certificate domain.
|
||||
func (c *CertificateStore) GetAllDomains() []string {
|
||||
allDomains := c.getDefaultCertificateDomains()
|
||||
|
||||
// Get dynamic certificates
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for domain := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
for domain := range c.DynamicCerts.Get().(map[string]*CertificateData) {
|
||||
allDomains = append(allDomains, domain)
|
||||
}
|
||||
}
|
||||
@ -71,6 +54,23 @@ func (c *CertificateStore) GetAllDomains() []string {
|
||||
return allDomains
|
||||
}
|
||||
|
||||
// GetDefaultCertificate returns the default certificate.
|
||||
func (c *CertificateStore) GetDefaultCertificate() *tls.Certificate {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.ocspStapler != nil && c.DefaultCertificate.Hash != "" {
|
||||
if staple, ok := c.ocspStapler.GetStaple(c.DefaultCertificate.Hash); ok {
|
||||
// We are updating the OCSPStaple of the certificate without any synchronization
|
||||
// as this should not cause any issue.
|
||||
c.DefaultCertificate.Certificate.OCSPStaple = staple
|
||||
}
|
||||
}
|
||||
|
||||
return c.DefaultCertificate.Certificate
|
||||
}
|
||||
|
||||
// GetBestCertificate returns the best match certificate, and caches the response.
|
||||
func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *tls.Certificate {
|
||||
if c == nil {
|
||||
@ -87,12 +87,21 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
|
||||
}
|
||||
|
||||
if cert, ok := c.CertCache.Get(serverName); ok {
|
||||
return cert.(*tls.Certificate)
|
||||
certificateData := cert.(*CertificateData)
|
||||
if c.ocspStapler != nil && certificateData.Hash != "" {
|
||||
if staple, ok := c.ocspStapler.GetStaple(certificateData.Hash); ok {
|
||||
// We are updating the OCSPStaple of the certificate without any synchronization
|
||||
// as this should not cause any issue.
|
||||
certificateData.Certificate.OCSPStaple = staple
|
||||
}
|
||||
}
|
||||
|
||||
matchedCerts := map[string]*tls.Certificate{}
|
||||
return certificateData.Certificate
|
||||
}
|
||||
|
||||
matchedCerts := map[string]*CertificateData{}
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for domains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
for domains, cert := range c.DynamicCerts.Get().(map[string]*CertificateData) {
|
||||
for _, certDomain := range strings.Split(domains, ",") {
|
||||
if matchDomain(serverName, certDomain) {
|
||||
matchedCerts[certDomain] = cert
|
||||
@ -110,15 +119,25 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
|
||||
sort.Strings(keys)
|
||||
|
||||
// cache best match
|
||||
c.CertCache.SetDefault(serverName, matchedCerts[keys[len(keys)-1]])
|
||||
return matchedCerts[keys[len(keys)-1]]
|
||||
certificateData := matchedCerts[keys[len(keys)-1]]
|
||||
c.CertCache.SetDefault(serverName, certificateData)
|
||||
|
||||
if c.ocspStapler != nil && certificateData.Hash != "" {
|
||||
if staple, ok := c.ocspStapler.GetStaple(certificateData.Hash); ok {
|
||||
// We are updating the OCSPStaple of the certificate without any synchronization
|
||||
// as this should not cause any issue.
|
||||
certificateData.Certificate.OCSPStaple = staple
|
||||
}
|
||||
}
|
||||
|
||||
return certificateData.Certificate
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertificate returns the first certificate matching all the given domains.
|
||||
func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
|
||||
func (c *CertificateStore) GetCertificate(domains []string) *CertificateData {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@ -127,11 +146,11 @@ func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
|
||||
domainsKey := strings.Join(domains, ",")
|
||||
|
||||
if cert, ok := c.CertCache.Get(domainsKey); ok {
|
||||
return cert.(*tls.Certificate)
|
||||
return cert.(*CertificateData)
|
||||
}
|
||||
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*CertificateData) {
|
||||
if domainsKey == certDomains {
|
||||
c.CertCache.SetDefault(domainsKey, cert)
|
||||
return cert
|
||||
@ -163,6 +182,91 @@ func (c *CertificateStore) ResetCache() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CertificateStore) getDefaultCertificateDomains() []string {
|
||||
if c.DefaultCertificate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultCert := c.DefaultCertificate.Certificate.Leaf
|
||||
|
||||
var allCerts []string
|
||||
if len(defaultCert.Subject.CommonName) > 0 {
|
||||
allCerts = append(allCerts, defaultCert.Subject.CommonName)
|
||||
}
|
||||
|
||||
allCerts = append(allCerts, defaultCert.DNSNames...)
|
||||
|
||||
for _, ipSan := range defaultCert.IPAddresses {
|
||||
allCerts = append(allCerts, ipSan.String())
|
||||
}
|
||||
|
||||
return allCerts
|
||||
}
|
||||
|
||||
// appendCertificate appends a Certificate to a certificates map keyed by store name.
|
||||
func appendCertificate(certs map[string]map[string]*CertificateData, subjectAltNames []string, storeName string, cert *CertificateData) {
|
||||
// Guarantees the order to produce a unique cert key.
|
||||
sort.Strings(subjectAltNames)
|
||||
certKey := strings.Join(subjectAltNames, ",")
|
||||
|
||||
certExists := false
|
||||
if certs[storeName] == nil {
|
||||
certs[storeName] = make(map[string]*CertificateData)
|
||||
} else {
|
||||
for domains := range certs[storeName] {
|
||||
if domains == certKey {
|
||||
certExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if certExists {
|
||||
log.Debug().Msgf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
|
||||
} else {
|
||||
log.Debug().Msgf("Adding certificate for domain(s) %s", certKey)
|
||||
|
||||
certs[storeName][certKey] = cert
|
||||
}
|
||||
}
|
||||
|
||||
func parseCertificate(cert *Certificate) (tls.Certificate, []string, error) {
|
||||
certContent, err := cert.CertFile.Read()
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, fmt.Errorf("unable to read CertFile: %w", err)
|
||||
}
|
||||
|
||||
keyContent, err := cert.KeyFile.Read()
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, fmt.Errorf("unable to read KeyFile: %w", err)
|
||||
}
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certContent, keyContent)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, fmt.Errorf("unable to generate TLS certificate: %w", err)
|
||||
}
|
||||
|
||||
var SANs []string
|
||||
if tlsCert.Leaf.Subject.CommonName != "" {
|
||||
SANs = append(SANs, strings.ToLower(tlsCert.Leaf.Subject.CommonName))
|
||||
}
|
||||
if tlsCert.Leaf.DNSNames != nil {
|
||||
for _, dnsName := range tlsCert.Leaf.DNSNames {
|
||||
if dnsName != tlsCert.Leaf.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(dnsName))
|
||||
}
|
||||
}
|
||||
}
|
||||
if tlsCert.Leaf.IPAddresses != nil {
|
||||
for _, ip := range tlsCert.Leaf.IPAddresses {
|
||||
if ip.String() != tlsCert.Leaf.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(ip.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tlsCert, SANs, err
|
||||
}
|
||||
|
||||
// matchDomain returns whether the server name matches the cert domain.
|
||||
// The server name, from TLS SNI, must not have trailing dots (https://datatracker.ietf.org/doc/html/rfc6066#section-3).
|
||||
// This is enforced by https://github.com/golang/go/blob/d3d7998756c33f69706488cade1cd2b9b10a4c7f/src/crypto/tls/handshake_messages.go#L423-L427.
|
||||
|
@ -58,12 +58,12 @@ func TestGetBestCertificate(t *testing.T) {
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynamicMap := map[string]*tls.Certificate{}
|
||||
dynamicMap := map[string]*CertificateData{}
|
||||
|
||||
if test.dynamicCert != "" {
|
||||
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
|
||||
require.NoError(t, err)
|
||||
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
|
||||
dynamicMap[strings.ToLower(test.dynamicCert)] = &CertificateData{Certificate: cert}
|
||||
}
|
||||
|
||||
store := &CertificateStore{
|
||||
|
206
pkg/tls/ocsp.go
Normal file
206
pkg/tls/ocsp.go
Normal file
@ -0,0 +1,206 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const defaultCacheDuration = 24 * time.Hour
|
||||
|
||||
type ocspEntry struct {
|
||||
leaf *x509.Certificate
|
||||
issuer *x509.Certificate
|
||||
responders []string
|
||||
nextUpdate time.Time
|
||||
staple []byte
|
||||
}
|
||||
|
||||
// ocspStapler retrieves staples from OCSP responders and store them in an in-memory cache.
|
||||
// It also updates the staples on a regular basis and before they expire.
|
||||
type ocspStapler struct {
|
||||
client *http.Client
|
||||
cache cache.Cache
|
||||
forceStapleUpdates chan struct{}
|
||||
responderOverrides map[string]string
|
||||
}
|
||||
|
||||
// newOCSPStapler creates a new ocspStapler cache.
|
||||
func newOCSPStapler(responderOverrides map[string]string) *ocspStapler {
|
||||
return &ocspStapler{
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
cache: *cache.New(defaultCacheDuration, 5*time.Minute),
|
||||
forceStapleUpdates: make(chan struct{}, 1),
|
||||
responderOverrides: responderOverrides,
|
||||
}
|
||||
}
|
||||
|
||||
// Run updates the OCSP staples every hours.
|
||||
func (o *ocspStapler) Run(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-o.forceStapleUpdates:
|
||||
o.updateStaples(ctx)
|
||||
|
||||
case <-ticker.C:
|
||||
o.updateStaples(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ForceStapleUpdates triggers staple updates in the background instead of waiting for the Run routine to update them.
|
||||
func (o *ocspStapler) ForceStapleUpdates() {
|
||||
select {
|
||||
case o.forceStapleUpdates <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// GetStaple retrieves the OCSP staple for the corresponding to the given key (public certificate hash).
|
||||
func (o *ocspStapler) GetStaple(key string) ([]byte, bool) {
|
||||
if item, ok := o.cache.Get(key); ok && item != nil {
|
||||
if entry, ok := item.(*ocspEntry); ok {
|
||||
return entry.staple, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Upsert creates a new entry for the given certificate.
|
||||
// The ocspStapler will then be responsible from retrieving and updating the corresponding OCSP obtainStaple.
|
||||
func (o *ocspStapler) Upsert(key string, leaf, issuer *x509.Certificate) error {
|
||||
if len(leaf.OCSPServer) == 0 {
|
||||
return errors.New("leaf certificate does not contain an OCSP server")
|
||||
}
|
||||
|
||||
if item, ok := o.cache.Get(key); ok {
|
||||
o.cache.Set(key, item, cache.NoExpiration)
|
||||
return nil
|
||||
}
|
||||
|
||||
var responders []string
|
||||
for _, url := range leaf.OCSPServer {
|
||||
if len(o.responderOverrides) > 0 {
|
||||
if newURL, ok := o.responderOverrides[url]; ok {
|
||||
url = newURL
|
||||
}
|
||||
}
|
||||
responders = append(responders, url)
|
||||
}
|
||||
|
||||
o.cache.Set(key, &ocspEntry{
|
||||
leaf: leaf,
|
||||
issuer: issuer,
|
||||
responders: responders,
|
||||
}, cache.NoExpiration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetTTL resets the expiration time for all items having no expiration.
|
||||
// This allows setting a TTL for certificates that do not exist anymore in the dynamic configuration.
|
||||
// For certificates that are still provided by the dynamic configuration,
|
||||
// their expiration time will be unset when calling the Upsert method.
|
||||
func (o *ocspStapler) ResetTTL() {
|
||||
for key, item := range o.cache.Items() {
|
||||
if item.Expiration > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
o.cache.Set(key, item.Object, defaultCacheDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ocspStapler) updateStaples(ctx context.Context) {
|
||||
for _, item := range o.cache.Items() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
entry := item.Object.(*ocspEntry)
|
||||
|
||||
if entry.staple != nil && time.Now().Before(entry.nextUpdate) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := o.updateStaple(ctx, entry); err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to retieve OCSP staple for: %s", entry.leaf.Subject.CommonName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// obtainStaple obtains the OCSP stable for the given leaf certificate.
|
||||
func (o *ocspStapler) updateStaple(ctx context.Context, entry *ocspEntry) error {
|
||||
ocspReq, err := ocsp.CreateRequest(entry.leaf, entry.issuer, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating OCSP request: %w", err)
|
||||
}
|
||||
|
||||
for _, responder := range entry.responders {
|
||||
logger := log.With().Str("responder", responder).Logger()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, responder, bytes.NewReader(ocspReq))
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating OCSP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/ocsp-request")
|
||||
|
||||
res, err := o.client.Do(req)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("Unable to obtain OCSP response")
|
||||
continue
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode/100 != 2 {
|
||||
logger.Debug().Msgf("Unable to obtain OCSP response due to status code: %d", res.StatusCode)
|
||||
continue
|
||||
}
|
||||
|
||||
ocspResBytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("Unable to read OCSP response bytes")
|
||||
continue
|
||||
}
|
||||
|
||||
ocspRes, err := ocsp.ParseResponseForCert(ocspResBytes, entry.leaf, entry.issuer)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("Unable to parse OCSP response")
|
||||
continue
|
||||
}
|
||||
|
||||
entry.staple = ocspResBytes
|
||||
|
||||
// As per RFC 6960, the nextUpdate field is optional.
|
||||
if ocspRes.NextUpdate.IsZero() {
|
||||
// NextUpdate is not set, the staple should be updated on the next update.
|
||||
entry.nextUpdate = time.Now()
|
||||
} else {
|
||||
entry.nextUpdate = ocspRes.ThisUpdate.Add(ocspRes.NextUpdate.Sub(ocspRes.ThisUpdate) / 2)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("no OCSP staple obtained from any responders")
|
||||
}
|
485
pkg/tls/ocsp_test.go
Normal file
485
pkg/tls/ocsp_test.go
Normal file
@ -0,0 +1,485 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const certWithOCSPServer = `-----BEGIN CERTIFICATE-----
|
||||
MIIBgjCCASegAwIBAgICIAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD
|
||||
QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMCAxHjAcBgNVBAMTFU9D
|
||||
U1AgVGVzdCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIoe
|
||||
I/bjo34qony8LdRJD+Jhuk8/S8YHXRHl6rH9t5VFCFtX8lIPN/Ll1zCrQ2KB3Wlb
|
||||
fxSgiQyLrCpZyrdhVPSjXzBdMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU+Eo3
|
||||
5sST4LRrwS4dueIdGBZ5d7IwLAYIKwYBBQUHAQEEIDAeMBwGCCsGAQUFBzABhhBv
|
||||
Y3NwLmV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0kAMEYCIQDg94xY/+/VepESdvTT
|
||||
ykCwiWOS2aCpjyryrKpwMKkR0AIhAPc/+ZEz4W10OENxC1t+NUTvS8JbEGOwulkZ
|
||||
z9yfaLuD
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const certWithoutOCSPServer = `-----BEGIN CERTIFICATE-----
|
||||
MIIBUzCB+aADAgECAgIgADAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB
|
||||
MB4XDTIzMDEwMTEyMDAwMFoXDTIzMDIwMTEyMDAwMFowIDEeMBwGA1UEAxMVT0NT
|
||||
UCBUZXN0IENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEih4j
|
||||
9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg838uXXMKtDYoHdaVt/
|
||||
FKCJDIusKlnKt2FU9KMxMC8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT4Sjfm
|
||||
xJPgtGvBLh254h0YFnl3sjAKBggqhkjOPQQDAgNJADBGAiEA3rWetLGblfSuNZKf
|
||||
5CpZxhj3A0BjEocEh+2P+nAgIdUCIQDIgptabR1qTLQaF2u0hJsEX2IKuIUvYWH3
|
||||
6Lb92+zIHg==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// certKey is the private key for both certWithOCSPServer and certWithoutOCSPServer.
|
||||
const certKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEINnVcgrSNh4HlThWlZpegq14M8G/p9NVDtdVjZrseUGLoAoGCCqGSM49
|
||||
AwEHoUQDQgAEih4j9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg83
|
||||
8uXXMKtDYoHdaVt/FKCJDIusKlnKt2FU9A==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
// caCert is the issuing certificate for certWithOCSPServer and certWithoutOCSPServer.
|
||||
const caCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIBazCCARGgAwIBAgICEAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD
|
||||
QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMBIxEDAOBgNVBAMTB1Rl
|
||||
c3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASdKexSor/aeazDM57UHhAX
|
||||
rCkJxUeF2BWf0lZYCRxc3f0GdrEsVvjJW8+/E06eAzDCGSdM/08Nvun1nb6AmAlt
|
||||
o1cwVTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwkwDwYDVR0T
|
||||
AQH/BAUwAwEB/zAdBgNVHQ4EFgQU+Eo35sST4LRrwS4dueIdGBZ5d7IwCgYIKoZI
|
||||
zj0EAwIDSAAwRQIgGbA39+kETTB/YMLBFoC2fpZe1cDWfFB7TUdfINUqdH4CIQCR
|
||||
ByUFC8A+hRNkK5YNH78bgjnKk/88zUQF5ONy4oPGdQ==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const caKey = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIDJ59ptjq3MzILH4zn5IKoH1sYn+zrUeq2kD8+DD2x+OoAoGCCqGSM49
|
||||
AwEHoUQDQgAEnSnsUqK/2nmswzOe1B4QF6wpCcVHhdgVn9JWWAkcXN39BnaxLFb4
|
||||
yVvPvxNOngMwwhknTP9PDb7p9Z2+gJgJbQ==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
func TestOCSPStapler_Upsert(t *testing.T) {
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Upsert a certificate without an OCSP server should raise an error.
|
||||
leafCertWithoutOCSPServer, err := tls.X509KeyPair([]byte(certWithoutOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ocspStapler.Upsert("foo", leafCertWithoutOCSPServer.Leaf, issuerCert.Leaf)
|
||||
require.Error(t, err)
|
||||
|
||||
// Upsert a certificate with an OCSP server.
|
||||
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
|
||||
require.NoError(t, err)
|
||||
|
||||
i, ok := ocspStapler.cache.Get("foo")
|
||||
require.True(t, ok)
|
||||
|
||||
e, ok := i.(*ocspEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, leafCert.Leaf, e.leaf)
|
||||
assert.Equal(t, issuerCert.Leaf, e.issuer)
|
||||
assert.Nil(t, e.staple)
|
||||
assert.Equal(t, []string{"ocsp.example.com"}, e.responders)
|
||||
assert.Equal(t, int64(0), ocspStapler.cache.Items()["foo"].Expiration)
|
||||
|
||||
// Upsert an existing entry to make sure that the existing staple is preserved.
|
||||
e.staple = []byte("foo")
|
||||
e.nextUpdate = time.Now()
|
||||
e.responders = []string{"foo.com"}
|
||||
|
||||
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
|
||||
require.NoError(t, err)
|
||||
|
||||
i, ok = ocspStapler.cache.Get("foo")
|
||||
require.True(t, ok)
|
||||
|
||||
e, ok = i.(*ocspEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, leafCert.Leaf, e.leaf)
|
||||
assert.Equal(t, issuerCert.Leaf, e.issuer)
|
||||
assert.Equal(t, []byte("foo"), e.staple)
|
||||
assert.NotZero(t, e.nextUpdate)
|
||||
assert.Equal(t, []string{"foo.com"}, e.responders)
|
||||
assert.Equal(t, int64(0), ocspStapler.cache.Items()["foo"].Expiration)
|
||||
}
|
||||
|
||||
func TestOCSPStapler_Upsert_withResponderOverrides(t *testing.T) {
|
||||
ocspStapler := newOCSPStapler(map[string]string{
|
||||
"ocsp.example.com": "foo.com",
|
||||
})
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
|
||||
require.NoError(t, err)
|
||||
|
||||
i, ok := ocspStapler.cache.Get("foo")
|
||||
require.True(t, ok)
|
||||
|
||||
e, ok := i.(*ocspEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, leafCert.Leaf, e.leaf)
|
||||
assert.Equal(t, issuerCert.Leaf, e.issuer)
|
||||
assert.Nil(t, e.staple)
|
||||
assert.Equal(t, []string{"foo.com"}, e.responders)
|
||||
}
|
||||
|
||||
func TestOCSPStapler_ResetTTL(t *testing.T) {
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
ocspStapler.cache.Set("foo", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{"foo.com"},
|
||||
nextUpdate: time.Now(),
|
||||
staple: []byte("foo"),
|
||||
}, cache.NoExpiration)
|
||||
|
||||
ocspStapler.cache.Set("bar", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{"bar.com"},
|
||||
nextUpdate: time.Now(),
|
||||
staple: []byte("bar"),
|
||||
}, time.Hour)
|
||||
|
||||
wantBarExpiration := ocspStapler.cache.Items()["bar"].Expiration
|
||||
|
||||
ocspStapler.ResetTTL()
|
||||
|
||||
item, ok := ocspStapler.cache.Items()["foo"]
|
||||
require.True(t, ok)
|
||||
|
||||
e, ok := item.Object.(*ocspEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Positive(t, item.Expiration)
|
||||
assert.Equal(t, leafCert.Leaf, e.leaf)
|
||||
assert.Equal(t, issuerCert.Leaf, e.issuer)
|
||||
assert.Equal(t, []byte("foo"), e.staple)
|
||||
assert.NotZero(t, e.nextUpdate)
|
||||
assert.Equal(t, []string{"foo.com"}, e.responders)
|
||||
|
||||
item, ok = ocspStapler.cache.Items()["bar"]
|
||||
require.True(t, ok)
|
||||
|
||||
e, ok = item.Object.(*ocspEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, wantBarExpiration, item.Expiration)
|
||||
assert.Equal(t, leafCert.Leaf, e.leaf)
|
||||
assert.Equal(t, issuerCert.Leaf, e.issuer)
|
||||
assert.Equal(t, []byte("bar"), e.staple)
|
||||
assert.NotZero(t, e.nextUpdate)
|
||||
assert.Equal(t, []string{"bar.com"}, e.responders)
|
||||
}
|
||||
|
||||
func TestOCSPStapler_GetStaple(t *testing.T) {
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
|
||||
// Get an un-existing staple.
|
||||
staple, exists := ocspStapler.GetStaple("foo")
|
||||
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, staple)
|
||||
|
||||
// Get an existing staple.
|
||||
ocspStapler.cache.Set("foo", &ocspEntry{staple: []byte("foo")}, cache.NoExpiration)
|
||||
|
||||
staple, exists = ocspStapler.GetStaple("foo")
|
||||
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, []byte("foo"), staple)
|
||||
}
|
||||
|
||||
func TestOCSPStapler_updateStaple(t *testing.T) {
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
|
||||
require.NoError(t, err)
|
||||
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
|
||||
require.NoError(t, err)
|
||||
stapleUpdate := thisUpdate.Add(nextUpdate.Sub(thisUpdate) / 2)
|
||||
|
||||
ocspResponseTmpl := ocsp.Response{
|
||||
SerialNumber: leafCert.Leaf.SerialNumber,
|
||||
TBSResponseData: []byte("foo"),
|
||||
ThisUpdate: thisUpdate,
|
||||
NextUpdate: nextUpdate,
|
||||
}
|
||||
|
||||
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := func(rw http.ResponseWriter, req *http.Request) {
|
||||
ct := req.Header.Get("Content-Type")
|
||||
assert.Equal(t, "application/ocsp-request", ct)
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ocsp.ParseRequest(reqBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
|
||||
_, err = rw.Write(ocspResponse)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
responder := httptest.NewServer(http.HandlerFunc(handler))
|
||||
t.Cleanup(responder.Close)
|
||||
|
||||
responderStatusNotOK := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(responderStatusNotOK.Close)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
entry *ocspEntry
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
desc: "no responder",
|
||||
entry: &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "wrong responder",
|
||||
entry: &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{"http://foo.bar"},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "not ok status responder",
|
||||
entry: &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responderStatusNotOK.URL},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
desc: "one wrong responder, one ok",
|
||||
entry: &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{"http://foo.bar", responder.URL},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ok responder",
|
||||
entry: &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responder.URL},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
ocspStapler.client = &http.Client{Timeout: time.Second}
|
||||
|
||||
err = ocspStapler.updateStaple(t.Context(), test.entry)
|
||||
if test.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ocspResponse, test.entry.staple)
|
||||
assert.Equal(t, stapleUpdate.UTC(), test.entry.nextUpdate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCSPStapler_updateStaple_withoutNextUpdate(t *testing.T) {
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
|
||||
require.NoError(t, err)
|
||||
|
||||
ocspResponseTmpl := ocsp.Response{
|
||||
SerialNumber: leafCert.Leaf.SerialNumber,
|
||||
TBSResponseData: []byte("foo"),
|
||||
ThisUpdate: thisUpdate,
|
||||
}
|
||||
|
||||
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := func(rw http.ResponseWriter, req *http.Request) {
|
||||
ct := req.Header.Get("Content-Type")
|
||||
assert.Equal(t, "application/ocsp-request", ct)
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ocsp.ParseRequest(reqBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
|
||||
_, err = rw.Write(ocspResponse)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
responder := httptest.NewServer(http.HandlerFunc(handler))
|
||||
t.Cleanup(responder.Close)
|
||||
|
||||
responderStatusNotOK := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(responderStatusNotOK.Close)
|
||||
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
ocspStapler.client = &http.Client{Timeout: time.Second}
|
||||
|
||||
entry := &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responder.URL},
|
||||
}
|
||||
err = ocspStapler.updateStaple(t.Context(), entry)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ocspResponse, entry.staple)
|
||||
assert.NotZero(t, entry.nextUpdate)
|
||||
assert.Greater(t, time.Now(), entry.nextUpdate)
|
||||
}
|
||||
|
||||
func TestOCSPStapler_updateStaples(t *testing.T) {
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
|
||||
require.NoError(t, err)
|
||||
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
|
||||
require.NoError(t, err)
|
||||
stapleUpdate := thisUpdate.Add(nextUpdate.Sub(thisUpdate) / 2)
|
||||
|
||||
ocspResponseTmpl := ocsp.Response{
|
||||
SerialNumber: leafCert.Leaf.SerialNumber,
|
||||
TBSResponseData: []byte("foo"),
|
||||
ThisUpdate: thisUpdate,
|
||||
NextUpdate: nextUpdate,
|
||||
}
|
||||
|
||||
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := func(rw http.ResponseWriter, req *http.Request) {
|
||||
ct := req.Header.Get("Content-Type")
|
||||
assert.Equal(t, "application/ocsp-request", ct)
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ocsp.ParseRequest(reqBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
|
||||
_, err = rw.Write(ocspResponse)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
responder := httptest.NewServer(http.HandlerFunc(handler))
|
||||
t.Cleanup(responder.Close)
|
||||
|
||||
ocspStapler := newOCSPStapler(nil)
|
||||
ocspStapler.client = &http.Client{Timeout: time.Second}
|
||||
|
||||
// nil staple entry
|
||||
ocspStapler.cache.Set("nilStaple", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responder.URL},
|
||||
nextUpdate: time.Now().Add(-time.Hour),
|
||||
}, cache.NoExpiration)
|
||||
// staple entry with nextUpdate in the past
|
||||
ocspStapler.cache.Set("toUpdate", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responder.URL},
|
||||
staple: []byte("foo"),
|
||||
nextUpdate: time.Now().Add(-time.Hour),
|
||||
}, cache.NoExpiration)
|
||||
// staple entry with nextUpdate in the future
|
||||
inOneHour := time.Now().Add(time.Hour)
|
||||
ocspStapler.cache.Set("noUpdate", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
responders: []string{responder.URL},
|
||||
staple: []byte("foo"),
|
||||
nextUpdate: inOneHour,
|
||||
}, cache.NoExpiration)
|
||||
|
||||
ocspStapler.updateStaples(t.Context())
|
||||
|
||||
nilStaple, ok := ocspStapler.cache.Get("nilStaple")
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, ocspResponse, nilStaple.(*ocspEntry).staple)
|
||||
assert.Equal(t, stapleUpdate.UTC(), nilStaple.(*ocspEntry).nextUpdate)
|
||||
|
||||
toUpdate, ok := ocspStapler.cache.Get("toUpdate")
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, ocspResponse, toUpdate.(*ocspEntry).staple)
|
||||
assert.Equal(t, stapleUpdate.UTC(), nilStaple.(*ocspEntry).nextUpdate)
|
||||
|
||||
noUpdate, ok := ocspStapler.cache.Get("noUpdate")
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, []byte("foo"), noUpdate.(*ocspEntry).staple)
|
||||
assert.Equal(t, inOneHour, noUpdate.(*ocspEntry).nextUpdate)
|
||||
}
|
@ -6,7 +6,9 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -43,6 +45,11 @@ func getCipherSuites() []string {
|
||||
return ciphers
|
||||
}
|
||||
|
||||
// OCSPConfig contains the OCSP configuration.
|
||||
type OCSPConfig struct {
|
||||
ResponderOverrides map[string]string `description:"Defines a map of OCSP responders to replace for querying OCSP servers." json:"responderOverrides,omitempty" toml:"responderOverrides,omitempty" yaml:"responderOverrides,omitempty"`
|
||||
}
|
||||
|
||||
// Manager is the TLS option/store/configuration factory.
|
||||
type Manager struct {
|
||||
lock sync.RWMutex
|
||||
@ -50,16 +57,33 @@ type Manager struct {
|
||||
stores map[string]*CertificateStore
|
||||
configs map[string]Options
|
||||
certs []*CertAndStores
|
||||
|
||||
// As of today, the TLS manager contains and is responsible for creating/starting the OCSP ocspStapler.
|
||||
// It would likely have been a Configuration listener but this implies that certs are re-parsed.
|
||||
// But this would probably have impact on resource consumption.
|
||||
ocspStapler *ocspStapler
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
func NewManager(ocspConfig *OCSPConfig) *Manager {
|
||||
manager := &Manager{
|
||||
stores: map[string]*CertificateStore{},
|
||||
configs: map[string]Options{
|
||||
"default": DefaultTLSOptions,
|
||||
},
|
||||
}
|
||||
|
||||
if ocspConfig != nil {
|
||||
manager.ocspStapler = newOCSPStapler(ocspConfig.ResponderOverrides)
|
||||
}
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
func (m *Manager) Run(ctx context.Context) {
|
||||
if m.ocspStapler != nil {
|
||||
m.ocspStapler.Run(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfigs updates the TLS* configuration options.
|
||||
@ -91,7 +115,14 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
||||
m.storesConfig[tlsalpn01.ACMETLS1Protocol] = Store{}
|
||||
}
|
||||
|
||||
storesCertificates := make(map[string]map[string]*tls.Certificate)
|
||||
storesCertificates := make(map[string]map[string]*CertificateData)
|
||||
|
||||
// Define the TTL for all the cache entries with no TTL.
|
||||
// This will discard entries that are not used anymore.
|
||||
if m.ocspStapler != nil {
|
||||
m.ocspStapler.ResetTTL()
|
||||
}
|
||||
|
||||
for _, conf := range certs {
|
||||
if len(conf.Stores) == 0 {
|
||||
log.Ctx(ctx).Debug().MsgFunc(func() string {
|
||||
@ -101,24 +132,49 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
||||
conf.Stores = []string{DefaultTLSStoreName}
|
||||
}
|
||||
|
||||
for _, store := range conf.Stores {
|
||||
logger := log.Ctx(ctx).With().Str(logs.TLSStoreName, store).Logger()
|
||||
cert, SANs, err := parseCertificate(&conf.Certificate)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msgf("Unable to parse certificate %s", conf.Certificate.GetTruncatedCertificateName())
|
||||
continue
|
||||
}
|
||||
|
||||
var certHash string
|
||||
if m.ocspStapler != nil && len(cert.Leaf.OCSPServer) > 0 {
|
||||
certHash = hashRawCert(cert.Leaf.Raw)
|
||||
|
||||
issuer := cert.Leaf
|
||||
if len(cert.Certificate) > 1 {
|
||||
issuer, err = x509.ParseCertificate(cert.Certificate[1])
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msgf("Unable to parse issuer certificate %s", conf.Certificate.GetTruncatedCertificateName())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.ocspStapler.Upsert(certHash, cert.Leaf, issuer); err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msgf("Unable to upsert OCSP certificate %s", conf.Certificate.GetTruncatedCertificateName())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
certData := &CertificateData{
|
||||
Certificate: &cert,
|
||||
Hash: certHash,
|
||||
}
|
||||
|
||||
for _, store := range conf.Stores {
|
||||
if _, ok := m.storesConfig[store]; !ok {
|
||||
m.storesConfig[store] = Store{}
|
||||
}
|
||||
|
||||
err := conf.Certificate.AppendCertificate(storesCertificates, store)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Unable to append certificate %s to store", conf.Certificate.GetTruncatedCertificateName())
|
||||
}
|
||||
appendCertificate(storesCertificates, SANs, store, certData)
|
||||
}
|
||||
}
|
||||
|
||||
m.stores = make(map[string]*CertificateStore)
|
||||
|
||||
for storeName, storeConfig := range m.storesConfig {
|
||||
st := NewCertificateStore()
|
||||
st := NewCertificateStore(m.ocspStapler)
|
||||
m.stores[storeName] = st
|
||||
|
||||
if certs, ok := storesCertificates[storeName]; ok {
|
||||
@ -133,13 +189,17 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
||||
logger := log.Ctx(ctx).With().Str(logs.TLSStoreName, storeName).Logger()
|
||||
ctxStore := logger.WithContext(ctx)
|
||||
|
||||
certificate, err := getDefaultCertificate(ctxStore, storeConfig, st)
|
||||
certificate, err := m.getDefaultCertificate(ctxStore, storeConfig, st)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error while creating certificate store")
|
||||
}
|
||||
|
||||
st.DefaultCertificate = certificate
|
||||
}
|
||||
|
||||
if m.ocspStapler != nil {
|
||||
m.ocspStapler.ForceStapleUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeDomains sanitizes the domain definition Main and SANS,
|
||||
@ -226,7 +286,8 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Serving default certificate for request: %q", domainToCheck)
|
||||
return store.DefaultCertificate, nil
|
||||
|
||||
return store.GetDefaultCertificate(), nil
|
||||
}
|
||||
|
||||
return tlsConfig, err
|
||||
@ -245,8 +306,8 @@ func (m *Manager) GetServerCertificates() []*x509.Certificate {
|
||||
|
||||
// We iterate over all the certificates.
|
||||
if defaultStore.DynamicCerts != nil && defaultStore.DynamicCerts.Get() != nil {
|
||||
for _, cert := range defaultStore.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
for _, cert := range defaultStore.DynamicCerts.Get().(map[string]*CertificateData) {
|
||||
x509Cert, err := x509.ParseCertificate(cert.Certificate.Certificate[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -256,7 +317,7 @@ func (m *Manager) GetServerCertificates() []*x509.Certificate {
|
||||
}
|
||||
|
||||
if defaultStore.DefaultCertificate != nil {
|
||||
x509Cert, err := x509.ParseCertificate(defaultStore.DefaultCertificate.Certificate[0])
|
||||
x509Cert, err := x509.ParseCertificate(defaultStore.DefaultCertificate.Certificate.Certificate[0])
|
||||
if err != nil {
|
||||
return certificates
|
||||
}
|
||||
@ -289,9 +350,9 @@ func (m *Manager) GetStore(storeName string) *CertificateStore {
|
||||
return m.getStore(storeName)
|
||||
}
|
||||
|
||||
func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*tls.Certificate, error) {
|
||||
func (m *Manager) getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*CertificateData, error) {
|
||||
if tlsStore.DefaultCertificate != nil {
|
||||
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
|
||||
cert, err := m.buildDefaultCertificate(tlsStore.DefaultCertificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -304,22 +365,65 @@ func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateS
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultCertificate := &CertificateData{
|
||||
Certificate: defaultCert,
|
||||
}
|
||||
|
||||
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)
|
||||
return defaultCertificate, 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 defaultCertificate, fmt.Errorf("unable to find certificate for domains %q: falling back to the internal generated certificate", strings.Join(domains, ","))
|
||||
}
|
||||
|
||||
return defaultACMECert, nil
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Debug().Msg("No default certificate, fallback to the internal generated certificate")
|
||||
return defaultCert, nil
|
||||
return defaultCertificate, nil
|
||||
}
|
||||
|
||||
func (m *Manager) buildDefaultCertificate(defaultCertificate *Certificate) (*CertificateData, error) {
|
||||
certFile, err := defaultCertificate.CertFile.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cert file content: %w", err)
|
||||
}
|
||||
|
||||
keyFile, err := defaultCertificate.KeyFile.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key file content: %w", err)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load X509 key pair: %w", err)
|
||||
}
|
||||
|
||||
var certHash string
|
||||
if m.ocspStapler != nil && len(cert.Leaf.OCSPServer) > 0 {
|
||||
certHash = hashRawCert(cert.Leaf.Raw)
|
||||
|
||||
issuer := cert.Leaf
|
||||
if len(cert.Certificate) > 1 {
|
||||
issuer, err = x509.ParseCertificate(cert.Certificate[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing issuer certificate %s: %w", defaultCertificate.GetTruncatedCertificateName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.ocspStapler.Upsert(certHash, cert.Leaf, issuer); err != nil {
|
||||
return nil, fmt.Errorf("upserting OCSP certificate %s: %w", defaultCertificate.GetTruncatedCertificateName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return &CertificateData{
|
||||
Certificate: &cert,
|
||||
Hash: certHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI.
|
||||
@ -412,20 +516,10 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) {
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func buildDefaultCertificate(defaultCertificate *Certificate) (*tls.Certificate, error) {
|
||||
certFile, err := defaultCertificate.CertFile.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cert file content: %w", err)
|
||||
}
|
||||
func hashRawCert(rawCert []byte) string {
|
||||
hasher := fnv.New64()
|
||||
|
||||
keyFile, err := defaultCertificate.KeyFile.Read()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key file content: %w", err)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load X509 key pair: %w", err)
|
||||
}
|
||||
return &cert, nil
|
||||
// purposely ignoring the error, as no error can be returned from the implementation.
|
||||
_, _ = hasher.Write(rawCert)
|
||||
return strconv.FormatUint(hasher.Sum64(), 16)
|
||||
}
|
||||
|
@ -1,14 +1,22 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs
|
||||
@ -76,10 +84,10 @@ func TestTLSInStore(t *testing.T) {
|
||||
},
|
||||
}}
|
||||
|
||||
tlsManager := NewManager()
|
||||
tlsManager := NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(), nil, nil, dynamicConfigs)
|
||||
|
||||
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate)
|
||||
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*CertificateData)
|
||||
if len(certs) == 0 {
|
||||
t.Fatal("got error: default store must have TLS certificates.")
|
||||
}
|
||||
@ -93,7 +101,7 @@ func TestTLSInvalidStore(t *testing.T) {
|
||||
},
|
||||
}}
|
||||
|
||||
tlsManager := NewManager()
|
||||
tlsManager := NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(),
|
||||
map[string]Store{
|
||||
"default": {
|
||||
@ -104,7 +112,7 @@ func TestTLSInvalidStore(t *testing.T) {
|
||||
},
|
||||
}, nil, dynamicConfigs)
|
||||
|
||||
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate)
|
||||
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*CertificateData)
|
||||
if len(certs) == 0 {
|
||||
t.Fatal("got error: default store must have TLS certificates.")
|
||||
}
|
||||
@ -157,7 +165,7 @@ func TestManager_Get(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlsManager := NewManager()
|
||||
tlsManager := NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(), nil, tlsConfigs, dynamicConfigs)
|
||||
|
||||
for _, test := range testCases {
|
||||
@ -296,7 +304,7 @@ func TestClientAuth(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlsManager := NewManager()
|
||||
tlsManager := NewManager(nil)
|
||||
tlsManager.UpdateConfigs(t.Context(), nil, tlsConfigs, nil)
|
||||
|
||||
for _, test := range testCases {
|
||||
@ -323,8 +331,108 @@ func TestClientAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_UpdateConfigs_OCSPConfig(t *testing.T) {
|
||||
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
|
||||
require.NoError(t, err)
|
||||
|
||||
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
|
||||
require.NoError(t, err)
|
||||
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
|
||||
require.NoError(t, err)
|
||||
|
||||
ocspResponseTmpl := ocsp.Response{
|
||||
SerialNumber: leafCert.Leaf.SerialNumber,
|
||||
TBSResponseData: []byte("foo"),
|
||||
ThisUpdate: thisUpdate,
|
||||
NextUpdate: nextUpdate,
|
||||
}
|
||||
|
||||
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
|
||||
require.NoError(t, err)
|
||||
|
||||
responderCall := make(chan struct{})
|
||||
|
||||
handler := func(rw http.ResponseWriter, req *http.Request) {
|
||||
ct := req.Header.Get("Content-Type")
|
||||
assert.Equal(t, "application/ocsp-request", ct)
|
||||
|
||||
reqBytes, err := io.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ocsp.ParseRequest(reqBytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
|
||||
_, err = rw.Write(ocspResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
responderCall <- struct{}{}
|
||||
}
|
||||
|
||||
responder := httptest.NewServer(http.HandlerFunc(handler))
|
||||
t.Cleanup(responder.Close)
|
||||
|
||||
testContext, cancel := context.WithCancel(t.Context())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tlsManager := NewManager(&OCSPConfig{
|
||||
ResponderOverrides: map[string]string{
|
||||
"ocsp.example.com": responder.URL,
|
||||
},
|
||||
})
|
||||
|
||||
go tlsManager.Run(testContext)
|
||||
|
||||
tlsManager.ocspStapler.cache.Set("existing", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
staple: []byte("foo"),
|
||||
nextUpdate: time.Now().Add(time.Hour),
|
||||
}, cache.NoExpiration)
|
||||
tlsManager.ocspStapler.cache.Set("existingWithTTL", &ocspEntry{
|
||||
leaf: leafCert.Leaf,
|
||||
issuer: issuerCert.Leaf,
|
||||
staple: []byte("foo"),
|
||||
nextUpdate: time.Now().Add(time.Hour),
|
||||
}, 2*defaultCacheDuration)
|
||||
|
||||
tlsManager.UpdateConfigs(testContext, nil, nil, []*CertAndStores{
|
||||
{
|
||||
Certificate: Certificate{
|
||||
CertFile: certWithOCSPServer,
|
||||
KeyFile: certKey,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Asserting that UpdateConfigs resets the expiration for existing entries.
|
||||
_, expiration, ok := tlsManager.ocspStapler.cache.GetWithExpiration("existing")
|
||||
require.True(t, ok)
|
||||
assert.Greater(t, expiration, time.Now())
|
||||
// But not for entries with TTL already set.
|
||||
_, expiration, ok = tlsManager.ocspStapler.cache.GetWithExpiration("existingWithTTL")
|
||||
require.True(t, ok)
|
||||
assert.Greater(t, expiration, time.Now().Add(defaultCacheDuration))
|
||||
|
||||
select {
|
||||
case <-responderCall:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("Timeout waiting for OCSP responder call")
|
||||
}
|
||||
|
||||
assert.Len(t, tlsManager.ocspStapler.cache.Items(), 3)
|
||||
|
||||
certHash := hashRawCert(leafCert.Leaf.Raw)
|
||||
_, ok = tlsManager.ocspStapler.cache.Get(certHash)
|
||||
require.True(t, ok)
|
||||
}
|
||||
|
||||
func TestManager_Get_DefaultValues(t *testing.T) {
|
||||
tlsManager := NewManager()
|
||||
tlsManager := NewManager(nil)
|
||||
|
||||
// Ensures we won't break things for Traefik users when updating Go
|
||||
config, _ := tlsManager.Get("default", "default")
|
||||
|
Loading…
x
Reference in New Issue
Block a user