From 49d2d20934929c7c325a32323812f34511698ef0 Mon Sep 17 00:00:00 2001 From: Peter Brachwitz Date: Tue, 4 Jan 2022 12:09:10 +0100 Subject: [PATCH] Add support for transport TLS certificate other/common name suffix (#5189) Add a new option called otherNameSuffix to allow users to influence the name construction in the OtherName SAN extension of Elasticsearch node certificates for the transport layer. Co-authored-by: Sebastien Guilloux --- config/crds/v1/all-crds.yaml | 9 +++ ...search.k8s.elastic.co_elasticsearches.yaml | 9 +++ .../eck-operator-crds/templates/all-crds.yaml | 9 +++ docs/reference/api-docs.asciidoc | 1 + .../elasticsearch/v1/elasticsearch_types.go | 5 ++ .../certificates/transport/csr.go | 16 +++-- .../certificates/transport/csr_test.go | 66 +++++++++++++++++-- .../certificates/transport/pod_secret.go | 2 +- .../certificates/transport/reconcile.go | 2 +- 9 files changed, 106 insertions(+), 13 deletions(-) diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index 83cb42b691..f1da4e69f2 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -4399,6 +4399,15 @@ spec: description: SecretName is the name of the secret. type: string type: object + otherNameSuffix: + description: 'OtherNameSuffix when defined will be prefixed + with the Pod name and used as the common name, and the first + DNSName, as well as an OtherName required by Elasticsearch + in the Subject Alternative Name extension of each Elasticsearch + node''s transport TLS certificate. Example: if set to "node.cluster.local", + the generated certificate will have its otherName set to + ".node.cluster.local".' + type: string subjectAltNames: description: SubjectAlternativeNames is a list of SANs to include in the generated node transport TLS certificates. diff --git a/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml b/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml index e2d50bcc72..a2664c5be7 100644 --- a/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml +++ b/config/crds/v1/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml @@ -8808,6 +8808,15 @@ spec: description: SecretName is the name of the secret. type: string type: object + otherNameSuffix: + description: 'OtherNameSuffix when defined will be prefixed + with the Pod name and used as the common name, and the first + DNSName, as well as an OtherName required by Elasticsearch + in the Subject Alternative Name extension of each Elasticsearch + node''s transport TLS certificate. Example: if set to "node.cluster.local", + the generated certificate will have its otherName set to + ".node.cluster.local".' + type: string subjectAltNames: description: SubjectAlternativeNames is a list of SANs to include in the generated node transport TLS certificates. diff --git a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml index 676f4b4c29..d0ae622f62 100644 --- a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml +++ b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml @@ -4429,6 +4429,15 @@ spec: description: SecretName is the name of the secret. type: string type: object + otherNameSuffix: + description: 'OtherNameSuffix when defined will be prefixed + with the Pod name and used as the common name, and the first + DNSName, as well as an OtherName required by Elasticsearch + in the Subject Alternative Name extension of each Elasticsearch + node''s transport TLS certificate. Example: if set to "node.cluster.local", + the generated certificate will have its otherName set to + ".node.cluster.local".' + type: string subjectAltNames: description: SubjectAlternativeNames is a list of SANs to include in the generated node transport TLS certificates. diff --git a/docs/reference/api-docs.asciidoc b/docs/reference/api-docs.asciidoc index c8cc5e0a5e..03c62761d6 100644 --- a/docs/reference/api-docs.asciidoc +++ b/docs/reference/api-docs.asciidoc @@ -1092,6 +1092,7 @@ TransportConfig holds the transport layer settings for Elasticsearch. [cols="25a,75a", options="header"] |=== | Field | Description +| *`otherNameSuffix`* __string__ | OtherNameSuffix when defined will be prefixed with the Pod name and used as the common name, and the first DNSName, as well as an OtherName required by Elasticsearch in the Subject Alternative Name extension of each Elasticsearch node's transport TLS certificate. Example: if set to "node.cluster.local", the generated certificate will have its otherName set to ".node.cluster.local". | *`subjectAltNames`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-subjectalternativename[$$SubjectAlternativeName$$]__ | SubjectAlternativeNames is a list of SANs to include in the generated node transport TLS certificates. | *`certificate`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-secretref[$$SecretRef$$]__ | Certificate is a reference to a Kubernetes secret that contains the CA certificate and private key for generating node certificates. The referenced secret should contain the following: - `ca.crt`: The CA certificate in PEM format. - `ca.key`: The private key for the CA certificate in PEM format. diff --git a/pkg/apis/elasticsearch/v1/elasticsearch_types.go b/pkg/apis/elasticsearch/v1/elasticsearch_types.go index be112cf30b..3e5b69f7f5 100644 --- a/pkg/apis/elasticsearch/v1/elasticsearch_types.go +++ b/pkg/apis/elasticsearch/v1/elasticsearch_types.go @@ -148,6 +148,11 @@ type TransportConfig struct { } type TransportTLSOptions struct { + // OtherNameSuffix when defined will be prefixed with the Pod name and used as the common name, + // and the first DNSName, as well as an OtherName required by Elasticsearch in the Subject Alternative Name + // extension of each Elasticsearch node's transport TLS certificate. + // Example: if set to "node.cluster.local", the generated certificate will have its otherName set to ".node.cluster.local". + OtherNameSuffix string `json:"otherNameSuffix,omitempty"` // SubjectAlternativeNames is a list of SANs to include in the generated node transport TLS certificates. SubjectAlternativeNames []commonv1.SubjectAlternativeName `json:"subjectAltNames,omitempty"` // Certificate is a reference to a Kubernetes secret that contains the CA certificate diff --git a/pkg/controller/elasticsearch/certificates/transport/csr.go b/pkg/controller/elasticsearch/certificates/transport/csr.go index ea9e01a269..83fbc4184d 100644 --- a/pkg/controller/elasticsearch/certificates/transport/csr.go +++ b/pkg/controller/elasticsearch/certificates/transport/csr.go @@ -41,7 +41,7 @@ func createValidatedCertificateTemplate( // TODO: csr signature is not checked certificateTemplate := certificates.ValidatedCertificateTemplate(x509.Certificate{ Subject: pkix.Name{ - CommonName: buildCertificateCommonName(pod, cluster.Name, cluster.Namespace), + CommonName: buildCertificateCommonName(pod, cluster), OrganizationalUnit: []string{cluster.Name}, }, @@ -76,7 +76,7 @@ func buildGeneralNames( ssetName := pod.Labels[label.StatefulSetNameLabelName] svcName := nodespec.HeadlessServiceName(ssetName) - commonName := buildCertificateCommonName(pod, cluster.Name, cluster.Namespace) + commonName := buildCertificateCommonName(pod, cluster) commonNameUTF8OtherName := &certificates.UTF8StringValuedOtherName{ OID: certificates.CommonNameObjectIdentifier, @@ -112,7 +112,13 @@ func buildGeneralNames( return generalNames, nil } -// buildCertificateCommonName returns the CN (and ES othername) entry for a given pod within a stack -func buildCertificateCommonName(pod corev1.Pod, clusterName, namespace string) string { - return fmt.Sprintf("%s.node.%s.%s.es.local", pod.Name, clusterName, namespace) +// buildCertificateCommonName returns the CN (and ES otherName) entry for a given Elasticsearch Pod. +// If the user provided an otherName suffix in the spec, it prepends the pod name to it (..node..es.local. +func buildCertificateCommonName(pod corev1.Pod, es esv1.Elasticsearch) string { + userConfiguredSuffix := es.Spec.Transport.TLS.OtherNameSuffix + if userConfiguredSuffix == "" { + return fmt.Sprintf("%s.node.%s.%s.es.local", pod.Name, es.Name, es.Namespace) + } + return fmt.Sprintf("%s.%s", pod.Name, userConfiguredSuffix) } diff --git a/pkg/controller/elasticsearch/certificates/transport/csr_test.go b/pkg/controller/elasticsearch/certificates/transport/csr_test.go index 47da0447e8..023cec8677 100644 --- a/pkg/controller/elasticsearch/certificates/transport/csr_test.go +++ b/pkg/controller/elasticsearch/certificates/transport/csr_test.go @@ -67,18 +67,22 @@ func Test_createValidatedCertificateTemplate(t *testing.T) { func Test_buildGeneralNames(t *testing.T) { expectedCommonName := "test-pod-name.node.test-es-name.test-namespace.es.local" expectedTransportSvcName := "test-es-name-es-transport.test-namespace.svc" - otherName, err := (&certificates.UTF8StringValuedOtherName{ - OID: certificates.CommonNameObjectIdentifier, - Value: expectedCommonName, - }).ToOtherName() - require.NoError(t, err) + + mkOtherName := func(name string) certificates.OtherName { + otherName, err := (&certificates.UTF8StringValuedOtherName{ + OID: certificates.CommonNameObjectIdentifier, + Value: name, + }).ToOtherName() + require.NoError(t, err) + return *otherName + } type args struct { cluster esv1.Elasticsearch pod corev1.Pod } expectedGeneralNames := []certificates.GeneralName{ - {OtherName: *otherName}, + {OtherName: mkOtherName(expectedCommonName)}, {DNSName: expectedCommonName}, {DNSName: expectedTransportSvcName}, {DNSName: "test-pod-name.test-sset"}, @@ -176,6 +180,56 @@ func Test_buildGeneralNames(t *testing.T) { {DNSName: "my-custom-domain"}, }...), }, + { + name: "custom name suffix", + args: args{ + cluster: func() esv1.Elasticsearch { + es := testES + es.Spec.Transport.TLS.OtherNameSuffix = "user.provided.suffix" + return es + }(), + pod: testPod, + }, + want: func() []certificates.GeneralName { + expectedCommonName := "test-pod-name.user.provided.suffix" + return []certificates.GeneralName{ + {OtherName: mkOtherName(expectedCommonName)}, + {DNSName: expectedCommonName}, + {DNSName: expectedTransportSvcName}, + {DNSName: "test-pod-name.test-sset"}, + {IPAddress: net.ParseIP(testIP).To4()}, + {IPAddress: net.ParseIP("127.0.0.1").To4()}, + } + }(), + }, + { + name: "custom name suffix with additional SANs", + args: args{ + cluster: func() esv1.Elasticsearch { + es := testES + es.Spec.Transport.TLS.OtherNameSuffix = "user.provided.suffix" + es.Spec.Transport.TLS.SubjectAlternativeNames = []commonv1.SubjectAlternativeName{ + { + DNS: "my-custom-domain", + }, + } + return es + }(), + pod: testPod, + }, + want: func() []certificates.GeneralName { + expectedCommonName := "test-pod-name.user.provided.suffix" + return []certificates.GeneralName{ + {OtherName: mkOtherName(expectedCommonName)}, + {DNSName: expectedCommonName}, + {DNSName: expectedTransportSvcName}, + {DNSName: "test-pod-name.test-sset"}, + {IPAddress: net.ParseIP(testIP).To4()}, + {IPAddress: net.ParseIP("127.0.0.1").To4()}, + {DNSName: "my-custom-domain"}, + } + }(), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/controller/elasticsearch/certificates/transport/pod_secret.go b/pkg/controller/elasticsearch/certificates/transport/pod_secret.go index d5c0114d0c..55495794b2 100644 --- a/pkg/controller/elasticsearch/certificates/transport/pod_secret.go +++ b/pkg/controller/elasticsearch/certificates/transport/pod_secret.go @@ -108,7 +108,7 @@ func shouldIssueNewCertificate( ca *certificates.CA, certReconcileBefore time.Duration, ) bool { - certCommonName := buildCertificateCommonName(pod, es.Name, es.Namespace) + certCommonName := buildCertificateCommonName(pod, es) generalNames, err := buildGeneralNames(es, pod) if err != nil { diff --git a/pkg/controller/elasticsearch/certificates/transport/reconcile.go b/pkg/controller/elasticsearch/certificates/transport/reconcile.go index 4f7a0c809a..867b3d2a73 100644 --- a/pkg/controller/elasticsearch/certificates/transport/reconcile.go +++ b/pkg/controller/elasticsearch/certificates/transport/reconcile.go @@ -122,7 +122,7 @@ func reconcileNodeSetTransportCertificatesSecrets( ); err != nil { return err } - certCommonName := buildCertificateCommonName(pod, es.Name, es.Namespace) + certCommonName := buildCertificateCommonName(pod, es) cert := extractTransportCert(*secret, pod, certCommonName) if cert == nil { return errors.New("no certificate found for pod")