diff --git a/go.mod b/go.mod index b81231230..12dcf613c 100644 --- a/go.mod +++ b/go.mod @@ -72,6 +72,7 @@ require ( github.com/vulcand/predicate v1.2.0 go.elastic.co/apm v1.13.1 go.elastic.co/apm/module/apmot v1.13.1 + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f golang.org/x/mod v0.4.2 golang.org/x/net v0.0.0-20220927171203-f486391704dc golang.org/x/text v0.3.7 @@ -323,7 +324,6 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.18.1 // indirect - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index adba5a4ee..a41da2198 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1154,7 +1154,9 @@ func (in *ServersTransport) DeepCopyInto(out *ServersTransport) { if in.Certificates != nil { in, out := &in.Certificates, &out.Certificates *out = make(tls.Certificates, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.ForwardingTimeouts != nil { in, out := &in.ForwardingTimeouts, &out.ForwardingTimeouts diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go index be211ab04..429b87e3f 100644 --- a/pkg/provider/acme/provider.go +++ b/pkg/provider/acme/provider.go @@ -781,6 +781,9 @@ func (p *Provider) buildMessage() dynamic.Message { Certificate: traefiktls.Certificate{ CertFile: traefiktls.FileOrContent(cert.Certificate.Certificate), KeyFile: traefiktls.FileOrContent(cert.Key), + OCSP: traefiktls.OCSPConfig{ + DisableStapling: true, + }, }, Stores: []string{cert.Store}, } diff --git a/pkg/provider/consulcatalog/connect_tls.go b/pkg/provider/consulcatalog/connect_tls.go index 5b7a92739..27bc8f828 100644 --- a/pkg/provider/consulcatalog/connect_tls.go +++ b/pkg/provider/consulcatalog/connect_tls.go @@ -26,6 +26,9 @@ func (c *connectCert) getLeaf() traefiktls.Certificate { return traefiktls.Certificate{ CertFile: traefiktls.FileOrContent(c.leaf.cert), KeyFile: traefiktls.FileOrContent(c.leaf.key), + OCSP: traefiktls.OCSPConfig{ + DisableStapling: false, + }, } } diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index ff6c6b588..7e6d45b50 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -328,6 +328,9 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) certs = append(certs, tls.Certificate{ CertFile: tls.FileOrContent(tlsSecret), KeyFile: tls.FileOrContent(tlsKey), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }) } @@ -940,6 +943,9 @@ func buildTLSStores(ctx context.Context, client Client) (map[string]tls.Store, m tlsStore.DefaultCertificate = &tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, } } @@ -1026,6 +1032,9 @@ func getTLS(k8sClient Client, secretName, namespace string) (*tls.CertAndStores, Certificate: tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, }, nil } diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 7b2bc5b72..ce846f8c5 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -1357,6 +1357,9 @@ func getTLS(k8sClient Client, secretName v1alpha2.ObjectName, namespace string) Certificate: tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, }, nil } diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 5605f97b9..d6cc3aac1 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -428,6 +428,9 @@ func getCertificates(ctx context.Context, ingress *networkingv1.Ingress, k8sClie Certificate: tls.Certificate{ CertFile: tls.FileOrContent(cert), KeyFile: tls.FileOrContent(key), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, } } diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 83cea3ad9..c77a05365 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -829,6 +829,9 @@ func Test_buildConfiguration(t *testing.T) { Certificate: tls.Certificate{ CertFile: tls.FileOrContent("foobar"), KeyFile: tls.FileOrContent("foobar"), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, Stores: []string{ "foobar", @@ -839,6 +842,9 @@ func Test_buildConfiguration(t *testing.T) { Certificate: tls.Certificate{ CertFile: tls.FileOrContent("foobar"), KeyFile: tls.FileOrContent("foobar"), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, Stores: []string{ "foobar", @@ -903,12 +909,18 @@ func Test_buildConfiguration(t *testing.T) { DefaultCertificate: &tls.Certificate{ CertFile: tls.FileOrContent("foobar"), KeyFile: tls.FileOrContent("foobar"), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, }, "Store1": { DefaultCertificate: &tls.Certificate{ CertFile: tls.FileOrContent("foobar"), KeyFile: tls.FileOrContent("foobar"), + OCSP: tls.OCSPConfig{ + DisableStapling: false, + }, }, }, }, diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index 8339f58c3..914818566 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -348,7 +348,8 @@ "certificates": [ { "certFile": "xxxx", - "keyFile": "xxxx" + "keyFile": "xxxx", + "ocsp": {} } ], "maxIdleConnsPerHost": 42, @@ -447,6 +448,7 @@ { "certFile": "xxxx", "keyFile": "xxxx", + "ocsp": {}, "stores": [ "foo" ] @@ -470,7 +472,8 @@ "foo": { "defaultCertificate": { "certFile": "xxxx", - "keyFile": "xxxx" + "keyFile": "xxxx", + "ocsp": {} } } } diff --git a/pkg/tls/certificate.go b/pkg/tls/certificate.go index 25559a22e..ea0036dfa 100644 --- a/pkg/tls/certificate.go +++ b/pkg/tls/certificate.go @@ -1,16 +1,22 @@ package tls import ( + "bytes" "crypto/tls" "crypto/x509" "errors" "fmt" + "io" + "io/ioutil" + "net/http" "net/url" "os" "sort" "strings" + "time" "github.com/traefik/traefik/v2/pkg/log" + "golang.org/x/crypto/ocsp" ) var ( @@ -45,11 +51,22 @@ var ( } ) +// OCSPConfig configures how OCSP is handled. +type OCSPConfig struct { + DisableStapling bool `json:"disableStapling,omitempty" toml:"disableStapling,omitempty" yaml:"disableStapling,omitempty"` +} + // Certificate holds a SSL cert/key pair // Certs and Key could be either a file path, or the file content itself. type Certificate struct { CertFile FileOrContent `json:"certFile,omitempty" toml:"certFile,omitempty" yaml:"certFile,omitempty"` KeyFile FileOrContent `json:"keyFile,omitempty" toml:"keyFile,omitempty" yaml:"keyFile,omitempty" loggable:"false"` + OCSP OCSPConfig `json:"ocsp,omitempty" toml:"ocsp,omitempty" yaml:"ocsp,omitempty" label:"allowEmpty" file:"allowEmpty"` + + Certificate *tls.Certificate `json:"-" toml:"-" yaml:"-"` + SANs []string `json:"-" toml:"-" yaml:"-"` + OCSPServer []string `json:"-" toml:"-" yaml:"-"` + OCSPResponse *ocsp.Response `json:"-" toml:"-" yaml:"-"` } // Certificates defines traefik certificates type @@ -101,7 +118,7 @@ func (f FileOrContent) Read() ([]byte, error) { } // 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 { +func (c *Certificate) AppendCertificate(certs map[string]map[string]*Certificate, storeName string) error { certContent, err := c.CertFile.Read() if err != nil { return fmt.Errorf("unable to read CertFile : %w", err) @@ -143,7 +160,7 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi certExists := false if certs[storeName] == nil { - certs[storeName] = make(map[string]*tls.Certificate) + certs[storeName] = make(map[string]*Certificate) } else { for domains := range certs[storeName] { if domains == certKey { @@ -152,16 +169,98 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi } } } + if certExists { log.Debugf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName) } else { log.Debugf("Adding certificate for domain(s) %s", certKey) - certs[storeName][certKey] = &tlsCert + + certs[storeName][certKey] = &Certificate{ + Certificate: &tlsCert, + SANs: SANs, + OCSPServer: parsedCert.OCSPServer, + OCSP: OCSPConfig{ + DisableStapling: false, + }, + } } return err } +func getOCSPForCert(certificate *Certificate, issuedCertificate *x509.Certificate, issuerCertificate *x509.Certificate) ([]byte, *ocsp.Response, error) { + if len(certificate.OCSPServer) == 0 { + return nil, nil, fmt.Errorf("no OCSP server specified in certificate") + } + + respURL := certificate.OCSPServer[0] + ocspReq, err := ocsp.CreateRequest(issuedCertificate, issuerCertificate, nil) + if err != nil { + return nil, nil, fmt.Errorf("creating OCSP request: %w", err) + } + + reader := bytes.NewReader(ocspReq) + req, err := http.Post(respURL, "application/ocsp-request", reader) + if err != nil { + return nil, nil, fmt.Errorf("making OCSP request: %w", err) + } + defer req.Body.Close() + + ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("reading OCSP response: %w", err) + } + + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCertificate) + if err != nil { + return nil, nil, fmt.Errorf("parsing OCSP response: %w", err) + } + + return ocspResBytes, ocspRes, nil +} + +// StapleOCSP populates the ocsp response of the certificate if needed and not disabled by configuration. +func (c *Certificate) StapleOCSP() error { + if c.OCSP.DisableStapling { + return nil + } + + ocspResponse := c.OCSPResponse + if ocspResponse != nil && time.Now().Before(ocspResponse.ThisUpdate.Add(ocspResponse.NextUpdate.Sub(ocspResponse.ThisUpdate)/2)) { + return nil + } + + leaf, _ := x509.ParseCertificate(c.Certificate.Certificate[0]) + var issuerCertificate *x509.Certificate + if len(c.Certificate.Certificate) == 1 { + issuerCertificate = leaf + } else { + ic, err := x509.ParseCertificate(c.Certificate.Certificate[1]) + if err != nil { + return fmt.Errorf("cannot parse issuer certificate for %v: %w", c.SANs, err) + } + + issuerCertificate = ic + } + + ocspBytes, ocspResp, ocspErr := getOCSPForCert(c, leaf, issuerCertificate) + if ocspErr != nil { + return fmt.Errorf("no OCSP stapling for %v: %w", c.SANs, ocspErr) + } + + log.WithoutContext().Debugf("ocsp response: %v", ocspResp) + if ocspResp.Status == ocsp.Good { + if ocspResp.NextUpdate.After(leaf.NotAfter) { + return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)", c.SANs, leaf.NotAfter.Sub(ocspResp.NextUpdate)) + } + + c.Certificate.OCSPStaple = ocspBytes + c.OCSPResponse = ocspResp + } + + return nil +} + // GetCertificate returns a tls.Certificate matching the configured CertFile and KeyFile. func (c *Certificate) GetCertificate() (tls.Certificate, error) { certContent, err := c.CertFile.Read() @@ -231,6 +330,9 @@ func (c *Certificates) Set(value string) error { *c = append(*c, Certificate{ CertFile: FileOrContent(files[0]), KeyFile: FileOrContent(files[1]), + OCSP: OCSPConfig{ + DisableStapling: false, + }, }) } return nil diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 1a954a0e0..65fae2a9d 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -63,7 +63,7 @@ func (c CertificateStore) GetAllDomains() []string { // 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]*Certificate) { allDomains = append(allDomains, domain) } } @@ -72,7 +72,7 @@ func (c CertificateStore) GetAllDomains() []string { } // GetBestCertificate returns the best match certificate, and caches the response. -func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *tls.Certificate { +func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *Certificate { if c == nil { return nil } @@ -87,12 +87,12 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) } if cert, ok := c.CertCache.Get(serverName); ok { - return cert.(*tls.Certificate) + return cert.(*Certificate) } - matchedCerts := map[string]*tls.Certificate{} + matchedCerts := map[string]*Certificate{} 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]*Certificate) { for _, certDomain := range strings.Split(domains, ",") { if matchDomain(serverName, certDomain) { matchedCerts[certDomain] = cert diff --git a/pkg/tls/certificate_store_test.go b/pkg/tls/certificate_store_test.go index a80516142..8fc414db8 100644 --- a/pkg/tls/certificate_store_test.go +++ b/pkg/tls/certificate_store_test.go @@ -59,7 +59,7 @@ func TestGetBestCertificate(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - dynamicMap := map[string]*tls.Certificate{} + dynamicMap := map[string]*Certificate{} if test.dynamicCert != "" { cert, err := loadTestCert(test.dynamicCert, test.uppercase) @@ -72,7 +72,7 @@ func TestGetBestCertificate(t *testing.T) { CertCache: cache.New(1*time.Hour, 10*time.Minute), } - var expected *tls.Certificate + var expected *Certificate if test.expectedCert != "" { cert, err := loadTestCert(test.expectedCert, test.uppercase) require.NoError(t, err) @@ -89,7 +89,7 @@ func TestGetBestCertificate(t *testing.T) { } } -func loadTestCert(certName string, uppercase bool) (*tls.Certificate, error) { +func loadTestCert(certName string, uppercase bool) (*Certificate, error) { replacement := "wildcard" if uppercase { replacement = "uppercase_wildcard" @@ -103,5 +103,8 @@ func loadTestCert(certName string, uppercase bool) (*tls.Certificate, error) { return nil, err } - return &staticCert, nil + return &Certificate{ + Certificate: &staticCert, + SANs: []string{}, + }, nil } diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 0c7daa992..6102fa4be 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -201,12 +201,17 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) { return nil, nil } - return certificate, nil + return certificate.Certificate, nil } bestCertificate := store.GetBestCertificate(clientHello) if bestCertificate != nil { - return bestCertificate, nil + err := bestCertificate.StapleOCSP() + if err != nil { + log.WithoutContext().Warnf("ocsp - error during stable: %w", err) + } + + return bestCertificate.Certificate, nil } if sniStrict { @@ -236,8 +241,8 @@ func (m *Manager) GetCertificates() []*x509.Certificate { // We iterate over all the certificates. for _, store := range m.stores { if store.DynamicCerts != nil && store.DynamicCerts.Get() != nil { - for _, cert := range store.DynamicCerts.Get().(map[string]*tls.Certificate) { - x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + for _, cert := range store.DynamicCerts.Get().(map[string]*Certificate) { + x509Cert, err := x509.ParseCertificate(cert.Certificate.Certificate[0]) if err != nil { continue } diff --git a/pkg/tls/tlsmanager_test.go b/pkg/tls/tlsmanager_test.go index 7f3853299..5035108a7 100644 --- a/pkg/tls/tlsmanager_test.go +++ b/pkg/tls/tlsmanager_test.go @@ -79,7 +79,7 @@ func TestTLSInStore(t *testing.T) { tlsManager := NewManager() tlsManager.UpdateConfigs(context.Background(), nil, nil, dynamicConfigs) - certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate) + certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*Certificate) if len(certs) == 0 { t.Fatal("got error: default store must have TLS certificates.") } @@ -104,7 +104,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]*Certificate) if len(certs) == 0 { t.Fatal("got error: default store must have TLS certificates.") } diff --git a/pkg/tls/zz_generated.deepcopy.go b/pkg/tls/zz_generated.deepcopy.go index c85405a37..d22feadbe 100644 --- a/pkg/tls/zz_generated.deepcopy.go +++ b/pkg/tls/zz_generated.deepcopy.go @@ -36,7 +36,7 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertAndStores) DeepCopyInto(out *CertAndStores) { *out = *in - out.Certificate = in.Certificate + in.Certificate.DeepCopyInto(&out.Certificate) if in.Stores != nil { in, out := &in.Stores, &out.Stores *out = make([]string, len(*in)) @@ -135,7 +135,7 @@ func (in *Store) DeepCopyInto(out *Store) { if in.DefaultCertificate != nil { in, out := &in.DefaultCertificate, &out.DefaultCertificate *out = new(Certificate) - **out = **in + (*in).DeepCopyInto(*out) } if in.DefaultGeneratedCert != nil { in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert