From 0a3e40332a86cc061bf16d8895b10defeb999171 Mon Sep 17 00:00:00 2001 From: Rio Kierkels Date: Mon, 14 Jun 2021 18:06:10 +0200 Subject: [PATCH] Improve CA certificate loading from kubernetes secret --- docs/content/https/tls.md | 2 +- docs/content/middlewares/http/forwardauth.md | 3 +- .../routing/providers/kubernetes-crd.md | 32 ++++++++---- .../crd/fixtures/with_servers_transport.yml | 50 ++++++++++++++++++- .../crd/fixtures/with_tls_options.yml | 2 +- pkg/provider/kubernetes/crd/kubernetes.go | 36 +++++++------ .../kubernetes/crd/kubernetes_test.go | 3 +- 7 files changed, 98 insertions(+), 30 deletions(-) diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index e8513a746..8740a5719 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -447,7 +447,7 @@ metadata: spec: clientAuth: - # the CA certificate is extracted from key `tls.ca` of the given secrets. + # the CA certificate is extracted from key `tls.ca` or `ca.crt` of the given secrets. secretNames: - secretCA clientAuthType: RequireAndVerifyClientCert diff --git a/docs/content/middlewares/http/forwardauth.md b/docs/content/middlewares/http/forwardauth.md index 8947cd68c..fc8d7ea37 100644 --- a/docs/content/middlewares/http/forwardauth.md +++ b/docs/content/middlewares/http/forwardauth.md @@ -373,7 +373,8 @@ metadata: namespace: default data: - ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + # Must contain a certificate under either a `tls.ca` or a `ca.crt` key. + tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= ``` ```yaml tab="Consul Catalog" diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 808884ecb..053436594 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1482,16 +1482,20 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre sniStrict: true # [8] ``` -| Ref | Attribute | Purpose | -|-----|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `minVersion` | Defines the [minimum TLS version](../../https/tls.md#minimum-tls-version) that is acceptable | -| [2] | `maxVersion` | Defines the [maximum TLS version](../../https/tls.md#maximum-tls-version) that is acceptable | -| [3] | `cipherSuites` | list of supported [cipher suites](../../https/tls.md#cipher-suites) for TLS versions up to TLS 1.2 | -| [4] | `curvePreferences` | List of the [elliptic curves references](../../https/tls.md#curve-preferences) that will be used in an ECDHE handshake, in preference order | -| [5] | `clientAuth` | determines the server's policy for TLS [Client Authentication](../../https/tls.md#client-authentication-mtls) | -| [6] | `clientAuth.secretNames` | list of names of the referenced Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (in TLSOption namespace) | -| [7] | `clientAuth.clientAuthType` | defines the client authentication type to apply. The available values are: `NoClientCert`, `RequestClientCert`, `VerifyClientCertIfGiven` and `RequireAndVerifyClientCert` | -| [8] | `sniStrict` | if `true`, Traefik won't allow connections from clients connections that do not specify a server_name extension | +| Ref | Attribute | Purpose | +|-----|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `minVersion` | Defines the [minimum TLS version](../../https/tls.md#minimum-tls-version) that is acceptable | +| [2] | `maxVersion` | Defines the [maximum TLS version](../../https/tls.md#maximum-tls-version) that is acceptable | +| [3] | `cipherSuites` | list of supported [cipher suites](../../https/tls.md#cipher-suites) for TLS versions up to TLS 1.2 | +| [4] | `curvePreferences` | List of the [elliptic curves references](../../https/tls.md#curve-preferences) that will be used in an ECDHE handshake, in preference order | +| [5] | `clientAuth` | determines the server's policy for TLS [Client Authentication](../../https/tls.md#client-authentication-mtls) | +| [6] | `clientAuth.secretNames` | list of names of the referenced Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (in TLSOption namespace). The secret must contain a certificate under either a `tls.ca` or a `ca.crt` key. | +| [7] | `clientAuth.clientAuthType` | defines the client authentication type to apply. The available values are: `NoClientCert`, `RequestClientCert`, `VerifyClientCertIfGiven` and `RequireAndVerifyClientCert` | +| [8] | `sniStrict` | if `true`, Traefik won't allow connections from clients connections that do not specify a server_name extension | + +!!! info "CA Secret" + + The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key. ??? example "Declaring and referencing a TLSOption" @@ -1544,6 +1548,7 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre namespace: default data: + # Must contain a certificate under either a `tls.ca` or a `ca.crt` key. tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= --- @@ -1554,6 +1559,7 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre namespace: default data: + # Must contain a certificate under either a `tls.ca` or a `ca.crt` key. tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= ``` @@ -1679,7 +1685,7 @@ or referencing TLS stores in the [`IngressRoute`](#kind-ingressroute) / [`Ingres |-----|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | [1] | `serverName` | ServerName used to contact the server. | | [2] | `insecureSkipVerify` | Disable SSL certificate verification. | -| [3] | `rootCAsSecrets` | Add cert file for self-signed certificate. | +| [3] | `rootCAsSecrets` | Add cert file for self-signed certificate. The secret must contain a certificate under either a tls.ca or a ca.crt key. | | [4] | `certificatesSecrets` | Certificates for mTLS. | | [5] | `maxIdleConnsPerHost` | If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, `defaultMaxIdleConnsPerHost` is used. | | [6] | `forwardingTimeouts` | Timeouts for requests forwarded to the backend servers. | @@ -1687,6 +1693,10 @@ or referencing TLS stores in the [`IngressRoute`](#kind-ingressroute) / [`Ingres | [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 period for which an idle HTTP keep-alive connection will remain open before closing itself. | +!!! info "CA Secret" + + The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key. + ??? example "Declaring and referencing a ServersTransport" ```yaml tab="ServersTransport" diff --git a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml index 5bc2af3ee..7c5a15012 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_servers_transport.yml @@ -1,11 +1,21 @@ apiVersion: v1 kind: Secret +metadata: + name: rootCas0 + namespace: foo + +data: + foobar: VEVTVFJPT1RDQVMw + +--- +apiVersion: v1 +kind: Secret metadata: name: rootCas1 namespace: foo data: - tls.ca: VEVTVFJPT1RDQVM= + tls.ca: VEVTVFJPT1RDQVMx --- apiVersion: v1 @@ -17,6 +27,27 @@ metadata: data: tls.ca: VEVTVFJPT1RDQVMy +--- +apiVersion: v1 +kind: Secret +metadata: + name: rootCas3 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVMz + +--- +apiVersion: v1 +kind: Secret +metadata: + name: rootCas4 + namespace: foo + +data: + ca.crt: VEVTVFJPT1RDQVM0 + tls.ca: VEVTVFJPT1RDQVM1 # <-- This should be the prefered one. + --- apiVersion: v1 kind: Secret @@ -39,6 +70,18 @@ data: tls.crt: VEVTVENFUlQy tls.key: VEVTVEtFWTI= +--- +apiVersion: v1 +kind: Secret +metadata: + name: allcerts + namespace: foo + +data: + ca.crt: VEVTVEFMTENFUlRT + tls.crt: VEVTVENFUlQz + tls.key: VEVTVEtFWTM= + --- apiVersion: traefik.containo.us/v1alpha1 kind: ServersTransport @@ -51,11 +94,16 @@ spec: insecureSkipVerify: true maxIdleConnsPerHost: 42 rootCAsSecrets: + - rootCas0 - rootCas1 - rootCas2 + - rootCas3 + - rootCas4 + - allcerts certificatesSecrets: - mtls1 - mtls2 + - allcerts forwardingTimeouts: dialTimeout: 42 responseHeaderTimeout: 42s diff --git a/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml b/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml index a5dd706b8..e6bae6250 100644 --- a/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml +++ b/pkg/provider/kubernetes/crd/fixtures/with_tls_options.yml @@ -15,7 +15,7 @@ metadata: namespace: default data: - tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= --- apiVersion: traefik.containo.us/v1alpha1 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 108f7d87b..cd55a18a7 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -508,20 +508,29 @@ func loadCASecret(namespace, secretName string, k8sClient Client) (string, error if err != nil { return "", fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err) } + if !ok { return "", fmt.Errorf("secret '%s/%s' not found", namespace, secretName) } + if secret == nil { return "", fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName) } - if len(secret.Data) != 1 { - return "", fmt.Errorf("found %d elements for secret '%s/%s', must be single element exactly", len(secret.Data), namespace, secretName) + + tlsCAData, err := getCABlocks(secret, namespace, secretName) + if err == nil { + return tlsCAData, nil } - for _, v := range secret.Data { - return string(v), nil + // TODO: remove this behavior in the next major version (v3) + 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 { + return string(v), nil + } } - return "", nil + + return "", fmt.Errorf("could not find CA block: %w", err) } func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) { @@ -529,15 +538,14 @@ func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, if err != nil { return "", "", fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err) } + if !exists { return "", "", fmt.Errorf("secret '%s/%s' does not exist", namespace, secretName) } + if secret == nil { return "", "", fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName) } - if len(secret.Data) != 2 { - return "", "", fmt.Errorf("found %d elements for secret '%s/%s', must be two elements exactly", len(secret.Data), namespace, secretName) - } return getCertificateBlocks(secret, namespace, secretName) } @@ -869,16 +877,16 @@ func getCertificateBlocks(secret *corev1.Secret, namespace, secretName string) ( func getCABlocks(secret *corev1.Secret, namespace, secretName string) (string, error) { tlsCrtData, tlsCrtExists := secret.Data["tls.ca"] - if !tlsCrtExists { - return "", fmt.Errorf("the tls.ca entry is missing from secret %s/%s", namespace, secretName) + if tlsCrtExists { + return string(tlsCrtData), nil } - cert := string(tlsCrtData) - if cert == "" { - return "", fmt.Errorf("the tls.ca entry in secret %s/%s is empty", namespace, secretName) + tlsCrtData, tlsCrtExists = secret.Data["ca.crt"] + if tlsCrtExists { + return string(tlsCrtData), nil } - return cert, nil + return "", fmt.Errorf("secret %s/%s contains neither tls.ca nor ca.crt", namespace, secretName) } func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *safe.Pool, eventsChan <-chan interface{}) chan interface{} { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index c0c3640b3..aea40d19c 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3444,10 +3444,11 @@ func TestLoadIngressRoutes(t *testing.T) { "test": { ServerName: "test", InsecureSkipVerify: true, - RootCAs: []tls.FileOrContent{"TESTROOTCAS", "TESTROOTCAS2"}, + RootCAs: []tls.FileOrContent{"TESTROOTCAS0", "TESTROOTCAS1", "TESTROOTCAS2", "TESTROOTCAS3", "TESTROOTCAS5", "TESTALLCERTS"}, Certificates: tls.Certificates{ {CertFile: "TESTCERT1", KeyFile: "TESTKEY1"}, {CertFile: "TESTCERT2", KeyFile: "TESTKEY2"}, + {CertFile: "TESTCERT3", KeyFile: "TESTKEY3"}, }, MaxIdleConnsPerHost: 42, ForwardingTimeouts: &dynamic.ForwardingTimeouts{