ocsp stapling working
baalajimaestro: Forward ported to v2.9+ Signed-off-by: baalajimaestro <me@baalajimaestro.me>
This commit is contained in:
parent
194247caae
commit
b82f7f6a44
15 changed files with 172 additions and 24 deletions
2
go.mod
2
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue