Support certificates configuration in TLSStore CRD

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
Kevin Pollet 2022-05-19 16:42:09 +02:00 committed by GitHub
parent ae6e844143
commit d5ff301d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 75 deletions

View file

@ -364,8 +364,9 @@ spec:
### Strict SNI Checking ### Strict SNI Checking
With strict SNI checking enabled, Traefik won't allow connections from clients With strict SNI checking enabled, Traefik won't allow connections from clients that do not specify a server_name extension
that do not specify a server_name extension or don't match any certificate configured on the tlsOption. or don't match any of the configured certificates.
The default certificate is irrelevant on that matter.
```yaml tab="File (YAML)" ```yaml tab="File (YAML)"
# Dynamic configuration # Dynamic configuration

View file

@ -36,9 +36,23 @@ spec:
spec: spec:
description: TLSStoreSpec configures a TLSStore resource. description: TLSStoreSpec configures a TLSStore resource.
properties: properties:
certificates:
description: Certificates is a list of secret names, each secret holding
a key/certificate pair to add to the store.
items:
description: Certificate holds a secret name for the TLSStore resource.
properties:
secretName:
description: SecretName is the name of the referenced Kubernetes
Secret to specify the certificate details.
type: string
required:
- secretName
type: object
type: array
defaultCertificate: defaultCertificate:
description: DefaultCertificate holds a secret name for the TLSOption description: DefaultCertificate is the name of the secret holding
resource. the default key/certificate pair for the store.
properties: properties:
secretName: secretName:
description: SecretName is the name of the referenced Kubernetes description: SecretName is the name of the referenced Kubernetes
@ -47,8 +61,6 @@ spec:
required: required:
- secretName - secretName
type: object type: object
required:
- defaultCertificate
type: object type: object
required: required:
- metadata - metadata

View file

@ -1618,25 +1618,27 @@ or referencing TLS stores in the [`IngressRoute`](#kind-ingressroute) / [`Ingres
Traefik currently only uses the [TLS Store named "default"](../../https/tls.md#certificates-stores). Traefik currently only uses the [TLS Store named "default"](../../https/tls.md#certificates-stores).
This means that if you have two stores that are named default in different kubernetes namespaces, This means that if you have two stores that are named default in different kubernetes namespaces,
they may be randomly chosen. they may be randomly chosen.
For the time being, please only configure one TLSSTore named default. For the time being, please only configure one TLSStore named default.
!!! info "TLSStore Attributes" !!! info "TLSStore Attributes"
```yaml tab="TLSStore" ```yaml tab="TLSStore"
apiVersion: traefik.containo.us/v1alpha1 apiVersion: traefik.containo.us/v1alpha1
kind: TLSStore kind: TLSStore
metadata: metadata:
name: default name: default
namespace: default namespace: default
spec: spec:
defaultCertificate: certificates: # [1]
secretName: my-secret # [1] - secretName: foo
- secretName: bar
defaultCertificate: # [2]
secretName: secret
``` ```
| Ref | Attribute | Purpose | | Ref | Attribute | Purpose |
|-----|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1] | `secretName` | The name of the referenced Kubernetes [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) that holds the default certificate for the store. | | [1] | `certificates` | List of Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), each of them holding a key/certificate pair to add to the store. |
| [2] | `defaultCertificate` | Name of a Kubernetes [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) that holds the default key/certificate pair for the store. |
??? example "Declaring and referencing a TLSStore" ??? example "Declaring and referencing a TLSStore"

View file

@ -1356,9 +1356,23 @@ spec:
spec: spec:
description: TLSStoreSpec configures a TLSStore resource. description: TLSStoreSpec configures a TLSStore resource.
properties: properties:
certificates:
description: Certificates is a list of secret names, each secret holding
a key/certificate pair to add to the store.
items:
description: Certificate holds a secret name for the TLSStore resource.
properties:
secretName:
description: SecretName is the name of the referenced Kubernetes
Secret to specify the certificate details.
type: string
required:
- secretName
type: object
type: array
defaultCertificate: defaultCertificate:
description: DefaultCertificate holds a secret name for the TLSOption description: DefaultCertificate is the name of the secret holding
resource. the default key/certificate pair for the store.
properties: properties:
secretName: secretName:
description: SecretName is the name of the referenced Kubernetes description: SecretName is the name of the referenced Kubernetes
@ -1367,8 +1381,6 @@ spec:
required: required:
- secretName - secretName
type: object type: object
required:
- defaultCertificate
type: object type: object
required: required:
- metadata - metadata

View file

@ -0,0 +1,43 @@
apiVersion: traefik.containo.us/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: default
spec:
certificates:
- secretName: supersecret
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: default
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami
port: 80
tls:
store:
name: default

View file

@ -179,18 +179,25 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
} }
func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration { func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration {
tlsConfigs := make(map[string]*tls.CertAndStores) stores, tlsConfigs := buildTLSStores(ctx, client)
if tlsConfigs == nil {
tlsConfigs = make(map[string]*tls.CertAndStores)
}
conf := &dynamic.Configuration{ conf := &dynamic.Configuration{
// TODO: choose between mutating and returning tlsConfigs
HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs), HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs),
TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs), TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs),
UDP: p.loadIngressRouteUDPConfiguration(ctx, client), UDP: p.loadIngressRouteUDPConfiguration(ctx, client),
TLS: &dynamic.TLSConfiguration{ TLS: &dynamic.TLSConfiguration{
Certificates: getTLSConfig(tlsConfigs), Options: buildTLSOptions(ctx, client),
Options: buildTLSOptions(ctx, client), Stores: stores,
Stores: buildTLSStores(ctx, client),
}, },
} }
// Done after because tlsConfigs is mutated by the others above.
conf.TLS.Certificates = getTLSConfig(tlsConfigs)
for _, middleware := range client.GetMiddlewares() { for _, middleware := range client.GetMiddlewares() {
id := provider.Normalize(makeID(middleware.Namespace, middleware.Name)) id := provider.Normalize(makeID(middleware.Namespace, middleware.Name))
ctxMid := log.With(ctx, log.Str(log.MiddlewareName, id)) ctxMid := log.With(ctx, log.Str(log.MiddlewareName, id))
@ -828,49 +835,60 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options
return tlsOptions return tlsOptions
} }
func buildTLSStores(ctx context.Context, client Client) map[string]tls.Store { func buildTLSStores(ctx context.Context, client Client) (map[string]tls.Store, map[string]*tls.CertAndStores) {
tlsStoreCRD := client.GetTLSStores() tlsStoreCRD := client.GetTLSStores()
var tlsStores map[string]tls.Store
if len(tlsStoreCRD) == 0 { if len(tlsStoreCRD) == 0 {
return tlsStores return nil, nil
} }
tlsStores = make(map[string]tls.Store)
var nsDefault []string var nsDefault []string
tlsStores := make(map[string]tls.Store)
tlsConfigs := make(map[string]*tls.CertAndStores)
for _, tlsStore := range tlsStoreCRD { for _, t := range tlsStoreCRD {
namespace := tlsStore.Namespace logger := log.FromContext(log.With(ctx, log.Str("TLSStore", t.Name), log.Str("namespace", t.Namespace)))
secretName := tlsStore.Spec.DefaultCertificate.SecretName
logger := log.FromContext(log.With(ctx, log.Str("tlsStore", tlsStore.Name), log.Str("namespace", namespace), log.Str("secretName", secretName)))
secret, exists, err := client.GetSecret(namespace, secretName) id := makeID(t.Namespace, t.Name)
if err != nil {
logger.Errorf("Failed to fetch secret %s/%s: %v", namespace, secretName, err)
continue
}
if !exists {
logger.Errorf("Secret %s/%s does not exist", namespace, secretName)
continue
}
cert, key, err := getCertificateBlocks(secret, namespace, secretName)
if err != nil {
logger.Errorf("Could not get certificate blocks: %v", err)
continue
}
id := makeID(tlsStore.Namespace, tlsStore.Name)
// If the name is default, we override the default config. // If the name is default, we override the default config.
if tlsStore.Name == tls.DefaultTLSStoreName { if t.Name == tls.DefaultTLSStoreName {
id = tlsStore.Name id = t.Name
nsDefault = append(nsDefault, tlsStore.Namespace) nsDefault = append(nsDefault, t.Namespace)
} }
tlsStores[id] = tls.Store{
DefaultCertificate: &tls.Certificate{ var tlsStore tls.Store
if t.Spec.DefaultCertificate != nil {
secretName := t.Spec.DefaultCertificate.SecretName
secret, exists, err := client.GetSecret(t.Namespace, secretName)
if err != nil {
logger.Errorf("Failed to fetch secret %s/%s: %v", t.Namespace, secretName, err)
continue
}
if !exists {
logger.Errorf("Secret %s/%s does not exist", t.Namespace, secretName)
continue
}
cert, key, err := getCertificateBlocks(secret, t.Namespace, secretName)
if err != nil {
logger.Errorf("Could not get certificate blocks: %v", err)
continue
}
tlsStore.DefaultCertificate = &tls.Certificate{
CertFile: tls.FileOrContent(cert), CertFile: tls.FileOrContent(cert),
KeyFile: tls.FileOrContent(key), KeyFile: tls.FileOrContent(key),
}, }
} }
if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil {
logger.Errorf("Failed to load certificates: %v", err)
continue
}
tlsStores[id] = tlsStore
} }
if len(nsDefault) > 1 { if len(nsDefault) > 1 {
@ -878,7 +896,25 @@ func buildTLSStores(ctx context.Context, client Client) map[string]tls.Store {
log.FromContext(ctx).Errorf("Default TLS Stores defined in multiple namespaces: %v", nsDefault) log.FromContext(ctx).Errorf("Default TLS Stores defined in multiple namespaces: %v", nsDefault)
} }
return tlsStores return tlsStores, tlsConfigs
}
// buildCertificates loads TLSStore certificates from secrets and sets them into tlsConfigs.
func buildCertificates(client Client, tlsStore, namespace string, certificates []v1alpha1.Certificate, tlsConfigs map[string]*tls.CertAndStores) error {
for _, c := range certificates {
configKey := namespace + "/" + c.SecretName
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
certAndStores, err := getTLS(client, c.SecretName, namespace)
if err != nil {
return fmt.Errorf("unable to read secret %s: %w", configKey, err)
}
certAndStores.Stores = []string{tlsStore}
tlsConfigs[configKey] = certAndStores
}
}
return nil
} }
func makeServiceKey(rule, ingressName string) (string, error) { func makeServiceKey(rule, ingressName string) (string, error) {

View file

@ -495,6 +495,7 @@ func namespaceOrFallback(lb v1alpha1.LoadBalancerSpec, fallback string) string {
return fallback return fallback
} }
// getTLSHTTP mutates tlsConfigs.
func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
if ingressRoute.Spec.TLS == nil { if ingressRoute.Spec.TLS == nil {
return nil return nil

View file

@ -269,6 +269,7 @@ func (p *Provider) loadTCPServers(client Client, namespace string, svc v1alpha1.
return servers, nil return servers, nil
} }
// getTLSTCP mutates tlsConfigs.
func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
if ingressRoute.Spec.TLS == nil { if ingressRoute.Spec.TLS == nil {
return nil return nil

View file

@ -3480,6 +3480,63 @@ func TestLoadIngressRoutes(t *testing.T) {
}, },
}, },
}, },
{
desc: "TLS with tls store containing certificates",
paths: []string{"services.yml", "with_tls_store_certificates.yml"},
expected: &dynamic.Configuration{
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: tls.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: tls.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
Stores: []string{"default"},
},
},
Stores: map[string]tls.Store{
"default": {},
},
},
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-test-route-6b204d94623b3df4370c": {
EntryPoints: []string{"web"},
Service: "default-test-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
TLS: &dynamic.RouterTLSConfig{},
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-test-route-6b204d94623b3df4370c": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
},
},
{ {
desc: "TLS with tls store default two times", desc: "TLS with tls store default two times",
paths: []string{"services.yml", "with_tls_store.yml", "with_default_tls_store.yml"}, paths: []string{"services.yml", "with_tls_store.yml", "with_default_tls_store.yml"},

View file

@ -20,13 +20,16 @@ type TLSStore struct {
// TLSStoreSpec configures a TLSStore resource. // TLSStoreSpec configures a TLSStore resource.
type TLSStoreSpec struct { type TLSStoreSpec struct {
DefaultCertificate DefaultCertificate `json:"defaultCertificate"` // DefaultCertificate is the name of the secret holding the default key/certificate pair for the store.
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty"`
// Certificates is a list of secret names, each secret holding a key/certificate pair to add to the store.
Certificates []Certificate `json:"certificates,omitempty"`
} }
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// DefaultCertificate holds a secret name for the TLSOption resource. // Certificate holds a secret name for the TLSStore resource.
type DefaultCertificate struct { type Certificate struct {
// SecretName is the name of the referenced Kubernetes Secret to specify the certificate details. // SecretName is the name of the referenced Kubernetes Secret to specify the certificate details.
SecretName string `json:"secretName"` SecretName string `json:"secretName"`
} }

View file

@ -53,6 +53,22 @@ func (in *BasicAuth) DeepCopy() *BasicAuth {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Certificate) DeepCopyInto(out *Certificate) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate.
func (in *Certificate) DeepCopy() *Certificate {
if in == nil {
return nil
}
out := new(Certificate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Chain) DeepCopyInto(out *Chain) { func (in *Chain) DeepCopyInto(out *Chain) {
*out = *in *out = *in
@ -142,22 +158,6 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DefaultCertificate) DeepCopyInto(out *DefaultCertificate) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultCertificate.
func (in *DefaultCertificate) DeepCopy() *DefaultCertificate {
if in == nil {
return nil
}
out := new(DefaultCertificate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DigestAuth) DeepCopyInto(out *DigestAuth) { func (in *DigestAuth) DeepCopyInto(out *DigestAuth) {
*out = *in *out = *in
@ -1413,7 +1413,7 @@ func (in *TLSStore) DeepCopyInto(out *TLSStore) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec in.Spec.DeepCopyInto(&out.Spec)
return return
} }
@ -1487,7 +1487,16 @@ func (in *TLSStoreRef) DeepCopy() *TLSStoreRef {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLSStoreSpec) DeepCopyInto(out *TLSStoreSpec) { func (in *TLSStoreSpec) DeepCopyInto(out *TLSStoreSpec) {
*out = *in *out = *in
out.DefaultCertificate = in.DefaultCertificate if in.DefaultCertificate != nil {
in, out := &in.DefaultCertificate, &out.DefaultCertificate
*out = new(Certificate)
**out = **in
}
if in.Certificates != nil {
in, out := &in.Certificates, &out.Certificates
*out = make([]Certificate, len(*in))
copy(*out, *in)
}
return return
} }