From ae4a00b4bcc2c9131de61265a9739e2003d36746 Mon Sep 17 00:00:00 2001 From: Nelson Isioma Date: Tue, 11 Mar 2025 15:38:05 +0100 Subject: [PATCH] Allow root CA to be added through config maps --- docs/content/migration/v3.md | 36 +++++ .../kubernetes-crd-definition-v1.yml | 56 +++++++- .../traefik.io_serverstransports.yaml | 28 +++- .../traefik.io_serverstransporttcps.yaml | 28 +++- .../routing/providers/kubernetes-crd.md | 46 +++---- integration/fixtures/k8s/01-traefik-crd.yml | 56 +++++++- pkg/config/dynamic/http_config.go | 4 +- pkg/config/dynamic/tcp_config.go | 4 +- pkg/provider/kubernetes/crd/client.go | 16 +++ .../fixtures/tcp/with_servers_transport.yml | 46 +++++++ .../crd/fixtures/with_servers_transport.yml | 52 ++++++++ pkg/provider/kubernetes/crd/kubernetes.go | 126 +++++++++++++++++- .../kubernetes/crd/kubernetes_test.go | 4 +- .../traefikio/v1alpha1/serverstransport.go | 17 +++ .../traefikio/v1alpha1/serverstransporttcp.go | 5 +- .../v1alpha1/zz_generated.deepcopy.go | 40 ++++++ 16 files changed, 516 insertions(+), 48 deletions(-) diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index f38081fa7..19a2370d5 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -192,6 +192,8 @@ To be consistent with the naming and other metrics providers, the metric now rep ### Kubernetes CRD Provider +#### Load-Balancing + In `v3.4`, the HTTP service definition has been updated. The strategy field now supports two new values: `wrr` and `p2c` (please refer to the [HTTP Services Load Balancing documentation](../../routing/services/#load-balancing-strategy) for more details). @@ -202,3 +204,37 @@ kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.4/docs/con ``` Please note that the `RoundRobin` strategy value is now deprecated, but still supported and equivalent to `wrr`, and will be removed in the next major release. + +#### ServersTransport CA Certificate + +In `v3.4`, a new `rootCAs` option has been added to the `ServersTransport` and `ServersTransportTCP` CRDs. +It allows the configuration of CA certificates from both `ConfigMaps` and `Secrets`, +and replaces the `rootCAsSecrets` option, as shown below: + +```yaml +--- +apiVersion: traefik.io/v1alpha1 +kind: ServersTransport +metadata: + name: foo + namespace: bar +spec: + rootCAs: + - configMap: ca-config-map + - secret: ca-secret + +--- +apiVersion: traefik.io/v1alpha1 +kind: ServersTransportTCP +metadata: + name: foo + namespace: bar +spec: + rootCAs: + - configMap: ca-config-map + - secret: ca-secret +``` + +The `rootCAsSecrets` option, which allows only `Secrets` references, +is still supported, but is now deprecated, +and will be removed in the next major release. diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 76f596493..1ae793877 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -2252,9 +2252,33 @@ spec: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets or ConfigMaps + used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to validate - self-signed certificate. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array @@ -2373,9 +2397,33 @@ spec: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets + or ConfigMaps used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to - validate self-signed certificates. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml index 3226cc9c7..50b15ea0c 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml @@ -113,9 +113,33 @@ spec: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets or ConfigMaps + used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to validate - self-signed certificate. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml index 21df38773..8cd603050 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransporttcps.yaml @@ -89,9 +89,33 @@ spec: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets + or ConfigMaps used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to - validate self-signed certificates. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index be1e038d2..a026cd552 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1851,9 +1851,9 @@ Register the `TLSStore` kind in the Kubernetes cluster before creating `TLSStore spec: serverName: foobar # [1] insecureSkipVerify: true # [2] - rootCAsSecrets: # [3] - - foobar - - foobar + rootCAs: # [3] + - configMap: foobar + - secret: foobar certificatesSecrets: # [4] - foobar - foobar @@ -1871,22 +1871,22 @@ Register the `TLSStore` kind in the Kubernetes cluster before creating `TLSStore trustDomain: "spiffe://trust-domain" # [14] ``` -| Ref | Attribute | Purpose | -|------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `serverName` | ServerName used to contact the server. | -| [2] | `insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | -| [3] | `rootCAsSecrets` | Defines the set of root certificate authorities to use when verifying server certificates. The secret must contain a certificate under either a tls.ca or a ca.crt key. | -| [4] | `certificatesSecrets` | Certificates to present to the server for mTLS. | -| [5] | `maxIdleConnsPerHost` | Controls the maximum idle (keep-alive) connections to keep per-host. If zero, `defaultMaxIdleConnsPerHost` is used. | -| [6] | `forwardingTimeouts` | Timeouts for requests forwarded to the servers. | -| [7] | `dialTimeout` | The amount of time to wait until a connection to a server can be established. If zero, no timeout exists. | -| [8] | `responseHeaderTimeout` | The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists. | -| [9] | `idleConnTimeout` | The maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout exists. | -| [10] | `peerCertURI` | URI used to match against SAN URIs during the server's certificate verification. | -| [11] | `disableHTTP2` | Disables HTTP/2 for connections with servers. | -| [12] | `spiffe` | The spiffe configuration. | -| [13] | `ids` | Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). | -| [14] | `trustDomain` | Defines the allowed SPIFFE trust domain. | +| Ref | Attribute | Purpose | +|------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `serverName` | ServerName used to contact the server. | +| [2] | `insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | +| [3] | `rootCAs` | Defines the set of root certificate authorities to use when verifying server certificates. The referenced Secret or ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. | +| [4] | `certificatesSecrets` | Certificates to present to the server for mTLS. | +| [5] | `maxIdleConnsPerHost` | Controls the maximum idle (keep-alive) connections to keep per-host. If zero, `defaultMaxIdleConnsPerHost` is used. | +| [6] | `forwardingTimeouts` | Timeouts for requests forwarded to the servers. | +| [7] | `dialTimeout` | The amount of time to wait until a connection to a server can be established. If zero, no timeout exists. | +| [8] | `responseHeaderTimeout` | The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists. | +| [9] | `idleConnTimeout` | The maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. If zero, no timeout exists. | +| [10] | `peerCertURI` | URI used to match against SAN URIs during the server's certificate verification. | +| [11] | `disableHTTP2` | Disables HTTP/2 for connections with servers. | +| [12] | `spiffe` | The spiffe configuration. | +| [13] | `ids` | Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). | +| [14] | `trustDomain` | Defines the allowed SPIFFE trust domain. | !!! info "CA Secret" @@ -1960,9 +1960,9 @@ The `default@internal` serversTransportTCP is created from the [static configura serverName: foobar # [5] insecureSkipVerify: true # [6] peerCertURI: foobar # [7] - rootCAsSecrets: # [8] - - foobar - - foobar + rootCAs: # [8] + - secret: foobar + - configMap: foobar certificatesSecrets: # [9] - foobar - foobar @@ -1982,7 +1982,7 @@ The `default@internal` serversTransportTCP is created from the [static configura | [5] | `serverName` | ServerName used to contact the server. | | [6] | `insecureSkipVerify` | Controls whether the server's certificate chain and host name is verified. | | [7] | `peerCertURI` | URI used to match against SAN URIs during the server's certificate verification. | -| [8] | `rootCAsSecrets` | Defines the set of root certificate authorities to use when verifying server certificates. The secret must contain a certificate under either a tls.ca or a ca.crt key. | +| [8] | `rootCAs` | Defines the set of root certificate authorities to use when verifying server certificates. The referenced Secret or ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. | | [9] | `certificatesSecrets` | Certificates to present to the server for mTLS. | | [10] | `spiffe` | The SPIFFE configuration. | | [11] | `ids` | Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). | diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 76f596493..1ae793877 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -2252,9 +2252,33 @@ spec: description: PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets or ConfigMaps + used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to validate - self-signed certificate. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array @@ -2373,9 +2397,33 @@ spec: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification. type: string + rootCAs: + description: RootCAs defines a list of CA certificate Secrets + or ConfigMaps used to validate server certificates. + items: + description: |- + RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. + If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. + properties: + configMap: + description: |- + ConfigMap defines the name of a ConfigMap that holds a CA certificate. + The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + type: string + secret: + description: |- + Secret defines the name of a Secret that holds a CA certificate. + The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + type: string + type: object + x-kubernetes-validations: + - message: RootCA cannot have both Secret and ConfigMap defined. + rule: has(self.secret) && has(self.configMap) + type: array rootCAsSecrets: - description: RootCAsSecrets defines a list of CA secret used to - validate self-signed certificates. + description: |- + RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. items: type: string type: array diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index deb2abeda..a7c927058 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -331,8 +331,8 @@ type HealthCheck struct{} type ServersTransport struct { ServerName string `description:"Defines the serverName used to contact the server." json:"serverName,omitempty" toml:"serverName,omitempty" yaml:"serverName,omitempty"` InsecureSkipVerify bool `description:"Disables SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` - RootCAs []types.FileOrContent `description:"Defines a list of CA secret used to validate self-signed certificate" json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` - Certificates traefiktls.Certificates `description:"Defines a list of secret storing client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` + RootCAs []types.FileOrContent `description:"Defines a list of CA certificates used to validate server certificates." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` + Certificates traefiktls.Certificates `description:"Defines a list of client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` ForwardingTimeouts *ForwardingTimeouts `description:"Defines the timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"` DisableHTTP2 bool `description:"Disables HTTP/2 for connections with backend servers." json:"disableHTTP2,omitempty" toml:"disableHTTP2,omitempty" yaml:"disableHTTP2,omitempty" export:"true"` diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go index e0f607d84..de7515f5b 100644 --- a/pkg/config/dynamic/tcp_config.go +++ b/pkg/config/dynamic/tcp_config.go @@ -159,8 +159,8 @@ type TCPServersTransport struct { type TLSClientConfig struct { ServerName string `description:"Defines the serverName used to contact the server." json:"serverName,omitempty" toml:"serverName,omitempty" yaml:"serverName,omitempty"` InsecureSkipVerify bool `description:"Disables SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` - RootCAs []types.FileOrContent `description:"Defines a list of CA secret used to validate self-signed certificate" json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` - Certificates traefiktls.Certificates `description:"Defines a list of secret storing client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` + RootCAs []types.FileOrContent `description:"Defines a list of CA certificates used to validate server certificates." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` + Certificates traefiktls.Certificates `description:"Defines a list of client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"` PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty" export:"true"` Spiffe *Spiffe `description:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` } diff --git a/pkg/provider/kubernetes/crd/client.go b/pkg/provider/kubernetes/crd/client.go index f82852e9f..dfaa7584a 100644 --- a/pkg/provider/kubernetes/crd/client.go +++ b/pkg/provider/kubernetes/crd/client.go @@ -50,6 +50,7 @@ type Client interface { GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpointSlicesForService(namespace, serviceName string) ([]*discoveryv1.EndpointSlice, error) GetNodes() ([]*corev1.Node, bool, error) + GetConfigMap(namespace, name string) (*corev1.ConfigMap, bool, error) } // TODO: add tests for the clientWrapper (and its methods) itself. @@ -227,6 +228,10 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< if err != nil { return nil, err } + _, err = factoryKube.Core().V1().ConfigMaps().Informer().AddEventHandler(eventHandler) + if err != nil { + return nil, err + } factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm)) _, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler) @@ -478,6 +483,17 @@ func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, bool, return secret, exist, err } +// GetConfigMap returns the named config map from the given namespace. +func (c *clientWrapper) GetConfigMap(namespace, name string) (*corev1.ConfigMap, bool, error) { + if !c.isWatchedNamespace(namespace) { + return nil, false, fmt.Errorf("failed to get config map %s/%s: namespace is not within watched namespaces", namespace, name) + } + + configMap, err := c.factoriesKube[c.lookupNamespace(namespace)].Core().V1().ConfigMaps().Lister().ConfigMaps(namespace).Get(name) + exist, err := translateNotFoundError(err) + return configMap, exist, err +} + func (c *clientWrapper) GetNodes() ([]*corev1.Node, bool, error) { nodes, err := c.clusterScopeFactory.Core().V1().Nodes().Lister().List(labels.Everything()) exist, err := translateNotFoundError(err) diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport.yml index f0fb21f5d..fa6e2a3bd 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_servers_transport.yml @@ -48,6 +48,26 @@ data: ca.crt: VEVTVFJPT1RDQVM0 tls.ca: VEVTVFJPT1RDQVM1 # <-- This should be the preferred one. +--- +apiVersion: v1 +kind: Secret +metadata: + name: root-ca5 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVM2 + +--- +apiVersion: v1 +kind: Secret +metadata: + name: root-ca6 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVM3 + --- apiVersion: v1 kind: Secret @@ -82,6 +102,26 @@ data: tls.crt: VEVTVENFUlQz tls.key: VEVTVEtFWTM= +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: root-ca-as-config-map + namespace: foo + +data: + ca.crt: "TESTROOTCASFROMCONFIGMAP" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: root-ca-as-config-map-2 + namespace: foo + +data: + ca.crt: "TESTROOTCASFROMCONFIGMAP2" + --- apiVersion: traefik.io/v1alpha1 kind: ServersTransportTCP @@ -101,6 +141,12 @@ spec: - root-ca3 - root-ca4 - allcerts + rootCAs: + - configMap: root-ca-as-config-map + - secret: root-ca5 + # referencing both a ConfigMap and a Secret should fail. + - configMap: root-ca-as-config-map-2 + secret: root-ca6 certificatesSecrets: - mtls1 - mtls2 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml index 8515039f4..496c5af30 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml @@ -1,4 +1,10 @@ apiVersion: v1 +kind: Namespace +metadata: + name: foo + +--- +apiVersion: v1 kind: Secret metadata: name: root-ca0 @@ -48,6 +54,26 @@ data: ca.crt: VEVTVFJPT1RDQVM0 tls.ca: VEVTVFJPT1RDQVM1 # <-- This should be the preferred one. +--- +apiVersion: v1 +kind: Secret +metadata: + name: root-ca5 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVM2 + +--- +apiVersion: v1 +kind: Secret +metadata: + name: root-ca6 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVM3 + --- apiVersion: v1 kind: Secret @@ -82,6 +108,26 @@ data: tls.crt: VEVTVENFUlQz tls.key: VEVTVEtFWTM= +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: root-ca-as-config-map + namespace: foo + +data: + ca.crt: "TESTROOTCASFROMCONFIGMAP" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: root-ca-as-config-map-2 + namespace: foo + +data: + ca.crt: "TESTROOTCASFROMCONFIGMAP2" + --- apiVersion: traefik.io/v1alpha1 kind: ServersTransport @@ -102,6 +148,12 @@ spec: - root-ca3 - root-ca4 - allcerts + rootCAs: + - configMap: root-ca-as-config-map + - secret: root-ca5 + # referencing both a ConfigMap and a Secret should fail. + - configMap: root-ca-as-config-map-2 + secret: root-ca6 certificatesSecrets: - mtls1 - mtls2 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index f9a8809c1..566a2bc39 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -340,19 +340,61 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) } for _, serversTransport := range client.GetServersTransports() { - logger := log.Ctx(ctx).With().Str(logs.ServersTransportName, serversTransport.Name).Logger() + logger := log.Ctx(ctx).With(). + Str(logs.ServersTransportName, serversTransport.Name). + Str("namespace", serversTransport.Namespace). + Logger() + + if len(serversTransport.Spec.RootCAsSecrets) > 0 { + logger.Warn().Msg("RootCAsSecrets option is deprecated, please use the RootCA option instead.") + } var rootCAs []types.FileOrContent for _, secret := range serversTransport.Spec.RootCAsSecrets { caSecret, err := loadCASecret(serversTransport.Namespace, secret, client) if err != nil { - logger.Error().Err(err).Msgf("Error while loading rootCAs %s", secret) + logger.Error(). + Err(err). + Str("secret", secret). + Msg("Error while loading CA Secret") continue } rootCAs = append(rootCAs, types.FileOrContent(caSecret)) } + for _, rootCA := range serversTransport.Spec.RootCAs { + if rootCA.Secret != nil && rootCA.ConfigMap != nil { + logger.Error().Msg("Error while loading CA: both Secret and ConfigMap are defined") + continue + } + + if rootCA.Secret != nil { + ca, err := loadCASecret(serversTransport.Namespace, *rootCA.Secret, client) + if err != nil { + logger.Error(). + Err(err). + Str("secret", *rootCA.Secret). + Msg("Error while loading CA Secret") + continue + } + + rootCAs = append(rootCAs, types.FileOrContent(ca)) + continue + } + + ca, err := loadCAConfigMap(serversTransport.Namespace, *rootCA.ConfigMap, client) + if err != nil { + logger.Error(). + Err(err). + Str("configMap", *rootCA.ConfigMap). + Msg("Error while loading CA ConfigMap") + continue + } + + rootCAs = append(rootCAs, types.FileOrContent(ca)) + } + var certs tls.Certificates for _, secret := range serversTransport.Spec.CertificatesSecrets { tlsSecret, tlsKey, err := loadAuthTLSSecret(serversTransport.Namespace, secret, client) @@ -449,20 +491,56 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) } if serversTransportTCP.Spec.TLS != nil { + if len(serversTransportTCP.Spec.TLS.RootCAsSecrets) > 0 { + logger.Warn().Msg("RootCAsSecrets option is deprecated, please use the RootCA option instead.") + } + var rootCAs []types.FileOrContent for _, secret := range serversTransportTCP.Spec.TLS.RootCAsSecrets { caSecret, err := loadCASecret(serversTransportTCP.Namespace, secret, client) if err != nil { logger.Error(). Err(err). - Str("rootCAs", secret). - Msg("Error while loading rootCAs") + Str("secret", secret). + Msg("Error while loading CA Secret") continue } rootCAs = append(rootCAs, types.FileOrContent(caSecret)) } + for _, rootCA := range serversTransportTCP.Spec.TLS.RootCAs { + if rootCA.Secret != nil && rootCA.ConfigMap != nil { + logger.Error().Msg("Error while loading CA: both Secret and ConfigMap are defined") + continue + } + + if rootCA.Secret != nil { + ca, err := loadCASecret(serversTransportTCP.Namespace, *rootCA.Secret, client) + if err != nil { + logger.Error(). + Err(err). + Str("secret", *rootCA.Secret). + Msg("Error while loading CA Secret") + continue + } + + rootCAs = append(rootCAs, types.FileOrContent(ca)) + continue + } + + ca, err := loadCAConfigMap(serversTransportTCP.Namespace, *rootCA.ConfigMap, client) + if err != nil { + logger.Error(). + Err(err). + Str("configMap", *rootCA.ConfigMap). + Msg("Error while loading CA ConfigMap") + continue + } + + rootCAs = append(rootCAs, types.FileOrContent(ca)) + } + var certs tls.Certificates for _, secret := range serversTransportTCP.Spec.TLS.CertificatesSecrets { tlsCert, tlsKey, err := loadAuthTLSSecret(serversTransportTCP.Namespace, secret, client) @@ -936,7 +1014,7 @@ func loadCASecret(namespace, secretName string, k8sClient Client) (string, error return tlsCAData, nil } - // TODO: remove this behavior in the next major version (v3) + // TODO: remove this behavior in the next major version (v4) if len(secret.Data) == 1 { // For backwards compatibility, use the only available secret data as CA if both 'ca.crt' and 'tls.ca' are missing. for _, v := range secret.Data { @@ -944,7 +1022,29 @@ func loadCASecret(namespace, secretName string, k8sClient Client) (string, error } } - return "", fmt.Errorf("could not find CA block: %w", err) + return "", fmt.Errorf("secret '%s/%s' has no CA block: %w", namespace, secretName, err) +} + +func loadCAConfigMap(namespace, name string, k8sClient Client) (string, error) { + configMap, ok, err := k8sClient.GetConfigMap(namespace, name) + if err != nil { + return "", fmt.Errorf("failed to fetch configMap '%s/%s': %w", namespace, name, err) + } + + if !ok { + return "", fmt.Errorf("configMap '%s/%s' not found", namespace, name) + } + + if configMap == nil { + return "", fmt.Errorf("data for configMap '%s/%s' must not be nil", namespace, name) + } + + tlsCAData, err := getCABlocksFromConfigMap(configMap, namespace, name) + if err == nil { + return tlsCAData, nil + } + + return "", fmt.Errorf("configMap '%s/%s' has no CA block: %w", namespace, name, err) } func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) { @@ -1384,6 +1484,20 @@ func getCABlocks(secret *corev1.Secret, namespace, secretName string) (string, e return "", fmt.Errorf("secret %s/%s contains neither tls.ca nor ca.crt", namespace, secretName) } +func getCABlocksFromConfigMap(configMap *corev1.ConfigMap, namespace, name string) (string, error) { + tlsCrtData, tlsCrtExists := configMap.Data["tls.ca"] + if tlsCrtExists { + return tlsCrtData, nil + } + + tlsCrtData, tlsCrtExists = configMap.Data["ca.crt"] + if tlsCrtExists { + return tlsCrtData, nil + } + + return "", fmt.Errorf("config map %s/%s contains neither tls.ca nor ca.crt", namespace, name) +} + func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { if throttleDuration == 0 { return nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 0f7e86d03..e14eef1ef 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1491,7 +1491,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { TLS: &dynamic.TLSClientConfig{ ServerName: "test", InsecureSkipVerify: true, - RootCAs: []types.FileOrContent{"TESTROOTCAS0", "TESTROOTCAS1", "TESTROOTCAS2", "TESTROOTCAS3", "TESTROOTCAS5", "TESTALLCERTS"}, + RootCAs: []types.FileOrContent{"TESTROOTCAS0", "TESTROOTCAS1", "TESTROOTCAS2", "TESTROOTCAS3", "TESTROOTCAS5", "TESTALLCERTS", "TESTROOTCASFROMCONFIGMAP", "TESTROOTCAS6"}, Certificates: tls.Certificates{ {CertFile: "TESTCERT1", KeyFile: "TESTKEY1"}, {CertFile: "TESTCERT2", KeyFile: "TESTKEY2"}, @@ -4734,7 +4734,7 @@ func TestLoadIngressRoutes(t *testing.T) { "foo-test": { ServerName: "test", InsecureSkipVerify: true, - RootCAs: []types.FileOrContent{"TESTROOTCAS0", "TESTROOTCAS1", "TESTROOTCAS2", "TESTROOTCAS3", "TESTROOTCAS5", "TESTALLCERTS"}, + RootCAs: []types.FileOrContent{"TESTROOTCAS0", "TESTROOTCAS1", "TESTROOTCAS2", "TESTROOTCAS3", "TESTROOTCAS5", "TESTALLCERTS", "TESTROOTCASFROMCONFIGMAP", "TESTROOTCAS6"}, Certificates: tls.Certificates{ {CertFile: "TESTCERT1", KeyFile: "TESTKEY1"}, {CertFile: "TESTCERT2", KeyFile: "TESTKEY2"}, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go index 3b13986fd..a205ae930 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go @@ -31,7 +31,10 @@ type ServersTransportSpec struct { ServerName string `json:"serverName,omitempty"` // InsecureSkipVerify disables SSL certificate verification. InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + // RootCAs defines a list of CA certificate Secrets or ConfigMaps used to validate server certificates. + RootCAs []RootCA `json:"rootCAs,omitempty"` // RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + // Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"` // CertificatesSecrets defines a list of secret storing client certificates for mTLS. CertificatesSecrets []string `json:"certificatesSecrets,omitempty"` @@ -74,6 +77,20 @@ type ForwardingTimeouts struct { PingTimeout *intstr.IntOrString `json:"pingTimeout,omitempty"` } +// +k8s:deepcopy-gen=true + +// RootCA defines a reference to a Secret or a ConfigMap that holds a CA certificate. +// If both a Secret and a ConfigMap reference are defined, the Secret reference takes precedence. +// +kubebuilder:validation:XValidation:rule="has(self.secret) && has(self.configMap)",message="RootCA cannot have both Secret and ConfigMap defined." +type RootCA struct { + // Secret defines the name of a Secret that holds a CA certificate. + // The referenced Secret must contain a certificate under either a tls.ca or a ca.crt key. + Secret *string `json:"secret,omitempty"` + // ConfigMap defines the name of a ConfigMap that holds a CA certificate. + // The referenced ConfigMap must contain a certificate under either a tls.ca or a ca.crt key. + ConfigMap *string `json:"configMap,omitempty"` +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ServersTransportList is a collection of ServersTransport resources. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go index 46ba8694e..bd6fe637e 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransporttcp.go @@ -49,7 +49,10 @@ type TLSClientConfig struct { ServerName string `json:"serverName,omitempty"` // InsecureSkipVerify disables TLS certificate verification. InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` - // RootCAsSecrets defines a list of CA secret used to validate self-signed certificates. + // RootCAs defines a list of CA certificate Secrets or ConfigMaps used to validate server certificates. + RootCAs []RootCA `json:"rootCAs,omitempty"` + // RootCAsSecrets defines a list of CA secret used to validate self-signed certificate. + // Deprecated: RootCAsSecrets is deprecated, please use the RootCAs option instead. RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"` // CertificatesSecrets defines a list of secret storing client certificates for mTLS. CertificatesSecrets []string `json:"certificatesSecrets,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index e9dd2386b..f975a8bfb 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -1162,6 +1162,32 @@ func (in *Retry) DeepCopy() *Retry { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RootCA) DeepCopyInto(out *RootCA) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(string) + **out = **in + } + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootCA. +func (in *RootCA) DeepCopy() *RootCA { + if in == nil { + return nil + } + out := new(RootCA) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Route) DeepCopyInto(out *Route) { *out = *in @@ -1347,6 +1373,13 @@ func (in *ServersTransportList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServersTransportSpec) DeepCopyInto(out *ServersTransportSpec) { *out = *in + if in.RootCAs != nil { + in, out := &in.RootCAs, &out.RootCAs + *out = make([]RootCA, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.RootCAsSecrets != nil { in, out := &in.RootCAsSecrets, &out.RootCAsSecrets *out = make([]string, len(*in)) @@ -1593,6 +1626,13 @@ func (in *TLS) DeepCopy() *TLS { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) { *out = *in + if in.RootCAs != nil { + in, out := &in.RootCAs, &out.RootCAs + *out = make([]RootCA, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.RootCAsSecrets != nil { in, out := &in.RootCAsSecrets, &out.RootCAsSecrets *out = make([]string, len(*in))