pkg/controller/elasticsearch/certificates/transport/pod_secret.go (181 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package transport
import (
"context"
"crypto"
cryptorand "crypto/rand"
"crypto/x509"
"errors"
"fmt"
"reflect"
"time"
corev1 "k8s.io/api/core/v1"
esv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/elasticsearch/v1"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/certificates"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)
// PodKeyFileName returns the name of the private key entry for a specific pod in a transport certificates secret.
func PodKeyFileName(podName string) string {
return fmt.Sprintf("%s.%s", podName, certificates.KeyFileName)
}
// PodCertFileName returns the name of the certificates entry for a specific pod in a transport certificates secret.
func PodCertFileName(podName string) string {
return fmt.Sprintf("%s.%s", podName, certificates.CertFileName)
}
// ensureTransportCertificatesSecretContentsForPod ensures that the transport certificates secret has the correct
// content for a specific pod
func ensureTransportCertificatesSecretContentsForPod(
ctx context.Context,
es esv1.Elasticsearch,
secret *corev1.Secret,
pod corev1.Pod,
ca *certificates.CA,
rotationParams certificates.RotationParams,
) error {
log := ulog.FromContext(ctx)
// verify that the secret contains a parsable and compatible private key
privateKey := certificates.GetCompatiblePrivateKey(ctx, ca.PrivateKey, secret, PodKeyFileName(pod.Name))
// if we need a new private key, generate it
if privateKey == nil {
generatedPrivateKey, err := certificates.NewPrivateKey(ca.PrivateKey)
if err != nil {
return err
}
privateKey = generatedPrivateKey
pemPrivateKey, err := certificates.EncodePEMPrivateKey(privateKey)
if err != nil {
return err
}
secret.Data[PodKeyFileName(pod.Name)] = pemPrivateKey
}
if shouldIssueNewCertificate(ctx, es, *secret, pod, privateKey, ca, rotationParams.RotateBefore) {
log.Info(
"Issuing new certificate",
"pod_name", pod.Name,
)
csr, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{}, privateKey)
if err != nil {
return err
}
// create a cert from the csr
parsedCSR, err := x509.ParseCertificateRequest(csr)
if err != nil {
return err
}
validatedCertificateTemplate, err := createValidatedCertificateTemplate(
pod, es, parsedCSR, rotationParams.Validity,
)
if err != nil {
return err
}
// sign the certificate
certData, err := ca.CreateCertificate(*validatedCertificateTemplate)
if err != nil {
return err
}
// store the issued certificate in a secret mounted into the pod
secret.Data[PodCertFileName(pod.Name)] = certificates.EncodePEMCert(certData, ca.Cert.Raw)
}
return nil
}
// shouldIssueNewCertificate returns true if we should issue a new certificate.
//
// Reasons for reissuing a certificate:
// - no certificate yet
// - certificate has the wrong format
// - certificate is invalid or expired
// - certificate has no SAN extra extension
// - certificate SAN and IP does not match pod SAN and IP
func shouldIssueNewCertificate(
ctx context.Context,
es esv1.Elasticsearch,
secret corev1.Secret,
pod corev1.Pod,
privateKey crypto.Signer,
ca *certificates.CA,
certReconcileBefore time.Duration,
) bool {
log := ulog.FromContext(ctx)
certCommonName := buildCertificateCommonName(pod, es)
generalNames, err := buildGeneralNames(es, pod)
if err != nil {
log.Error(err, "Cannot create GeneralNames for the TLS certificate",
"namespace", pod.Namespace, "pod_name", pod.Name)
return true
}
cert := extractTransportCert(ctx, secret, pod, certCommonName)
if cert == nil {
return true
}
if !certificates.PrivateMatchesPublicKey(ctx, cert.PublicKey, privateKey) {
log.Info(
"Certificate belongs do a different public key, should issue new",
"namespace", pod.Namespace,
"subject", cert.Subject,
"issuer", cert.Issuer,
"current_ca_subject", ca.Cert.Subject,
"pod_name", pod.Name,
)
return true
}
pool := x509.NewCertPool()
pool.AddCert(ca.Cert)
verifyOpts := x509.VerifyOptions{
DNSName: certCommonName,
Roots: pool,
Intermediates: pool,
}
if _, err := cert.Verify(verifyOpts); err != nil {
log.Info(
fmt.Sprintf("Certificate was not valid, should issue new: %s", err),
"namespace", pod.Namespace,
"subject", cert.Subject,
"issuer", cert.Issuer,
"current_ca_subject", ca.Cert.Subject,
"pod", pod.Name,
)
return true
}
if time.Now().After(cert.NotAfter.Add(-certReconcileBefore)) {
log.Info("Certificate soon to expire, should issue new",
"namespace", pod.Namespace, "pod", pod.Name)
return true
}
// compare actual vs. expected SANs
expected, err := certificates.MarshalToSubjectAlternativeNamesData(generalNames)
if err != nil {
log.Error(err, "Cannot marshal subject alternative names, will issue new certificate",
"namespace", pod.Namespace, "pod_name", pod.Name)
return true
}
extraExtensionFound := false
for _, ext := range cert.Extensions {
if !ext.Id.Equal(certificates.SubjectAlternativeNamesObjectIdentifier) {
continue
}
extraExtensionFound = true
if !reflect.DeepEqual(ext.Value, expected) {
log.Info("Certificate SANs do not match expected one, should issue new",
"namespace", pod.Namespace, "pod_name", pod.Name)
return true
}
}
if !extraExtensionFound {
log.Error(errors.New("no SAN extra extension"),
"SAN extra extension not found, should issue new certificate",
"namespace", pod.Namespace, "pod_name", pod.Name)
return true
}
return false
}
// extractTransportCert extracts the transport certificate for the pod with the commonName from the Secret
func extractTransportCert(ctx context.Context, secret corev1.Secret, pod corev1.Pod, commonName string) *x509.Certificate {
log := ulog.FromContext(ctx)
certData, ok := secret.Data[PodCertFileName(pod.Name)]
if !ok {
log.Info("No tls certificate found in secret",
"namespace", pod.Namespace, "pod_name", pod.Name)
return nil
}
certs, err := certificates.ParsePEMCerts(certData)
if err != nil {
log.Error(err, "Invalid certificate data found",
"namespace", pod.Namespace, "pod_name", pod.Name)
return nil
}
// look for the certificate based on the CommonName
names := make([]string, 0, len(certs))
for _, c := range certs {
if c.Subject.CommonName == commonName {
return c
}
names = append(names, c.Subject.CommonName)
}
log.Info(
"Did not find a certificate with the expected common name",
"namespace", pod.Namespace,
"pod_name", pod.Name,
"expected_name", commonName,
"actual_name", names,
)
return nil
}