...

Source file src/edge-infra.dev/pkg/edge/linkerd/certs/trustanchor/trustanchor.go

Documentation: edge-infra.dev/pkg/edge/linkerd/certs/trustanchor

     1  package trustanchor
     2  
     3  import (
     4  	"context"
     5  	"crypto/x509"
     6  	"fmt"
     7  	"slices"
     8  	"time"
     9  
    10  	"github.com/linkerd/linkerd2/pkg/tls"
    11  	corev1 "k8s.io/api/core/v1"
    12  	"k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/types"
    15  	"sigs.k8s.io/controller-runtime/pkg/client"
    16  
    17  	"edge-infra.dev/pkg/edge/linkerd"
    18  	l5dv1alpha1 "edge-infra.dev/pkg/edge/linkerd/k8s/apis/linkerd/v1alpha1"
    19  	"edge-infra.dev/pkg/edge/linkerd/k8s/controllers/metrics"
    20  	"edge-infra.dev/pkg/k8s/runtime/conditions"
    21  	"edge-infra.dev/pkg/lib/fog"
    22  )
    23  
    24  const (
    25  	CaBundleKey               = "ca-bundle.crt"
    26  	ManualTrustAnchorRotation = "linkerd.io/manual-anchor-rotation"
    27  	trustAnchorRotated        = "linkerd.io/trust-anchor-rotated"
    28  )
    29  
    30  // GenerateTrustAnchor creates the trust anchor cert and key
    31  // https://linkerd.io/2.11/tasks/automatically-rotating-control-plane-tls-credentials/#save-the-signing-key-pair-as-a-secret
    32  func GenerateTrustAnchor(ctx context.Context) ([]byte, []byte, error) {
    33  	log := fog.FromContext(ctx).WithName("generate-trust-anchor")
    34  
    35  	// generate private key for root ca
    36  	rootCAkey, err := tls.GenerateKey()
    37  	if err != nil {
    38  		return nil, nil, fmt.Errorf("failed to generate key for root ca: %w", err)
    39  	}
    40  
    41  	// creating root certificate authority with a duration of two years
    42  	certExpiryDate := time.Now().AddDate(linkerd.CertDurationYear, 0, 0)
    43  	metrics.RecordTrustAnchorExpiryTime(float64(certExpiryDate.Unix()))
    44  
    45  	tlsValidity := tls.Validity{
    46  		Lifetime: time.Until(certExpiryDate),
    47  	}
    48  	ca, err := tls.CreateRootCA("root.linkerd.cluster.local", rootCAkey, tlsValidity)
    49  	if err != nil {
    50  		return nil, nil, fmt.Errorf("failed to generate root ca: %w", err)
    51  	}
    52  
    53  	cert := ca.Cred.Crt.EncodePEM()
    54  	key, err := tls.EncodePrivateKeyPEM(rootCAkey)
    55  	if err != nil {
    56  		return nil, nil, fmt.Errorf("failed to encode private key: %w", err)
    57  	}
    58  	log.Info("generated a trust anchor certificate and key", "certificate expiry date", certExpiryDate)
    59  	return []byte(cert), key, nil
    60  }
    61  
    62  func buildSecret(l5d *l5dv1alpha1.Linkerd, cert, key []byte) *corev1.Secret {
    63  	return &corev1.Secret{
    64  		ObjectMeta: metav1.ObjectMeta{
    65  			Name:            linkerd.TrustAnchorName,
    66  			Namespace:       linkerd.Namespace,
    67  			OwnerReferences: linkerd.OwnerRef(l5d),
    68  		},
    69  		Type: corev1.SecretTypeTLS,
    70  		Data: map[string][]byte{
    71  			corev1.TLSCertKey:       cert,
    72  			corev1.TLSPrivateKeyKey: key,
    73  		},
    74  	}
    75  }
    76  
    77  // CreateIfNotExists checks if the trust anchor secret has already been
    78  // created and will create it if it isn't present.
    79  //
    80  // The content of the trust anchor secret that is required by the linkerd
    81  // installation manifest rendering process is returned regardless so that manifests
    82  // can be rendered correctly after controller restarts without re-generating the
    83  // secret each time.
    84  func CreateIfNotExists(ctx context.Context, c client.Client, l5d *l5dv1alpha1.Linkerd) (string, error) {
    85  	cert, err := SecretExists(ctx, c)
    86  	if err != nil {
    87  		conditions.MarkFalse(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.TrustAnchorSecretSetupFailedReason, "%v", err)
    88  		return "", err
    89  	}
    90  
    91  	// if cert exists then return early
    92  	if cert != "" {
    93  		return cert, nil
    94  	}
    95  
    96  	return Create(ctx, c, l5d)
    97  }
    98  
    99  // Create is responsible for creating a new trust anchor secret
   100  func Create(ctx context.Context, c client.Client, l5d *l5dv1alpha1.Linkerd) (string, error) {
   101  	cert, err := createSecret(ctx, c, l5d)
   102  	if err != nil {
   103  		conditions.MarkFalse(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.TrustAnchorSecretSetupFailedReason, "%v", err)
   104  		return "", err
   105  	}
   106  
   107  	conditions.MarkTrue(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.SucceededReason, "Created trust anchor secret")
   108  	return cert, nil
   109  }
   110  
   111  // Rotate generates a new trust anchor secret, annotates it, updates the ca bundle and patches the existing secret
   112  func Rotate(ctx context.Context, c client.Client, l5d *l5dv1alpha1.Linkerd) error {
   113  	log := fog.FromContext(ctx).WithName("rotate")
   114  
   115  	newTrustAnchorSecret, newSecret, err := generateSecret(ctx, l5d)
   116  	if err != nil {
   117  		log.Error(err, "failed to generate trust anchor secret")
   118  		return err
   119  	}
   120  	if newSecret.Annotations == nil {
   121  		newSecret.Annotations = make(map[string]string)
   122  	}
   123  	// annotate secret to prevent rotation on next reconcile
   124  	newSecret.Annotations[trustAnchorRotated] = "true"
   125  
   126  	oldSecret, err := getSecret(ctx, c)
   127  	if err != nil {
   128  		return err
   129  	} else if oldSecret == nil {
   130  		log.Info("trust anchor secret doesn't exist - creating secret")
   131  		if err := c.Create(ctx, newSecret, linkerd.CreateOpts()); err != nil {
   132  			return err
   133  		}
   134  	}
   135  	oldTrustAnchorCert, err := getCert(oldSecret)
   136  	if err != nil {
   137  		log.Error(err, "failed to get old trust anchor certificate from secret")
   138  		return err
   139  	}
   140  
   141  	if _, err := updateCaBundle(ctx, c, oldTrustAnchorCert, newTrustAnchorSecret); err != nil {
   142  		log.Error(err, "failed updating the ca bundle in the identity configmap")
   143  		return err
   144  	}
   145  	log.Info("successfully updated the identity configmap with the new ca bundle")
   146  
   147  	secret, err := getSecret(ctx, c)
   148  	if err != nil {
   149  		return err
   150  	} else if secret == nil {
   151  		log.Info("trust anchor secret doesn't exist - creating secret")
   152  		conditions.MarkTrue(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.SucceededReason, "Created trust anchor secret")
   153  		return c.Create(ctx, newSecret, linkerd.CreateOpts())
   154  	}
   155  
   156  	// if trust anchor secret did not exist prior to rotation, one will have been created
   157  	// so no need to patch
   158  	if oldSecret != nil {
   159  		conditions.MarkTrue(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.DualAnchor, "successfully merged old and new secret")
   160  		return c.Patch(ctx, newSecret, client.StrategicMergeFrom(oldSecret.DeepCopy()))
   161  	}
   162  
   163  	conditions.MarkTrue(l5d, l5dv1alpha1.TrustAnchor, l5dv1alpha1.SucceededReason, "Created trust anchor secret")
   164  	return nil
   165  }
   166  
   167  // IsRotated checks for the trust anchor rotated annotation on the trust
   168  // anchor secret and returns true if it is.
   169  func IsRotated(ctx context.Context, c client.Client) bool {
   170  	log := fog.FromContext(ctx).WithName("is-rotated")
   171  
   172  	secret, err := getSecret(ctx, c)
   173  	if err != nil {
   174  		log.Error(err, "failed to get secret")
   175  		return false
   176  	}
   177  	if secret == nil {
   178  		return false
   179  	}
   180  
   181  	_, annotationExists := secret.Annotations[trustAnchorRotated]
   182  	if !annotationExists {
   183  		log.Info("trust anchor rotation annotation does not exist - trust anchor has not been rotated")
   184  		return false
   185  	}
   186  
   187  	log.Info("the trust anchor has been rotated")
   188  	return true
   189  }
   190  
   191  func HasManualRotationAnnotation(l5d *l5dv1alpha1.Linkerd) bool {
   192  	_, exists := l5d.Annotations[ManualTrustAnchorRotation]
   193  	return exists
   194  }
   195  
   196  func RemoveRotationAnnotations(ctx context.Context, c client.Client, l5d *l5dv1alpha1.Linkerd) error {
   197  	secret, err := getSecret(ctx, c)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	secretPatch := []byte(fmt.Sprintf(
   202  		`{"metadata":{"annotations":{"%s":null}}}`,
   203  		trustAnchorRotated),
   204  	)
   205  	if err := c.Patch(ctx, secret, client.RawPatch(types.MergePatchType, secretPatch)); err != nil {
   206  		return err
   207  	}
   208  
   209  	l5dPatch := []byte(fmt.Sprintf(
   210  		`{"metadata":{"annotations":{"%s":null}}}`,
   211  		ManualTrustAnchorRotation),
   212  	)
   213  	return c.Patch(ctx, l5d.DeepCopy(), client.RawPatch(types.MergePatchType, l5dPatch))
   214  }
   215  
   216  // SecretExists attempts to get the trust anchor secret and return the cert string if it exists.
   217  // Returning an empty string and nil error indicates the secret does not exist.
   218  func SecretExists(ctx context.Context, c client.Client) (string, error) {
   219  	secret, err := getSecret(ctx, c)
   220  	if err != nil {
   221  		return "", err
   222  	} else if secret == nil {
   223  		return "", nil
   224  	}
   225  
   226  	return getCert(secret)
   227  }
   228  
   229  func getCert(secret *corev1.Secret) (string, error) {
   230  	if secret == nil {
   231  		return "", nil
   232  	}
   233  	data, ok := secret.Data[corev1.TLSCertKey]
   234  	if !ok || len(data) == 0 {
   235  		return "", fmt.Errorf("secret was present, but %s was not present or empty", corev1.TLSCertKey)
   236  	}
   237  	return string(secret.Data[corev1.TLSCertKey]), nil
   238  }
   239  
   240  // UpdateCaBundle checks if the ca bundle has the current trust anchor
   241  // certificate and updates the bundle if not.
   242  func UpdateCaBundle(ctx context.Context, c client.Client) (string, error) {
   243  	log := fog.FromContext(ctx).WithName("update-ca-bundle")
   244  	caBundle, err := GetCaBundle(ctx, c)
   245  	if err != nil {
   246  		return "", err
   247  	}
   248  
   249  	if caBundleOK, err := CheckCaBundle(ctx, c); err != nil || !caBundleOK {
   250  		if err != nil {
   251  			return "", err
   252  		}
   253  		log.Info("ca bundle is out-of-date - updating...")
   254  
   255  		lastCa, err := getLastValidCa(caBundle)
   256  		if err != nil {
   257  			return "", err
   258  		}
   259  
   260  		newCa, err := SecretExists(ctx, c)
   261  		if err != nil {
   262  			return "", err
   263  		}
   264  		caBundle, err = updateCaBundle(ctx, c, lastCa, newCa)
   265  		if err != nil {
   266  			return "", err
   267  		}
   268  	}
   269  
   270  	return caBundle, nil
   271  }
   272  
   273  // updateCaBundle patches the ca bundle in the identity configmap with the new trust anchor secret
   274  func updateCaBundle(ctx context.Context, c client.Client, oldTrustAnchorSecret, newTrustAnchorSecret string) (string, error) {
   275  	updatedCaBundle := fmt.Sprintf("%s%s", oldTrustAnchorSecret, newTrustAnchorSecret)
   276  
   277  	cm, err := getIdentityCM(ctx, c)
   278  	if err != nil {
   279  		return updatedCaBundle, err
   280  	}
   281  	if cm == nil {
   282  		return updatedCaBundle, fmt.Errorf("unable to update the ca bundle in the identity configmap")
   283  	}
   284  
   285  	cmPatch := cm.DeepCopy()
   286  	if cmPatch.Data == nil {
   287  		cmPatch.Data = make(map[string]string)
   288  	}
   289  	cmPatch.Data[CaBundleKey] = updatedCaBundle
   290  
   291  	return updatedCaBundle, c.Patch(ctx, cmPatch, client.StrategicMergeFrom(cm.DeepCopy()))
   292  }
   293  
   294  // GetCaBundle retrieves the ca bundle from the linkerd-identity-trust-roots configmap
   295  // or the trust anchor secret if the configmap does not exist
   296  func GetCaBundle(ctx context.Context, c client.Client) (string, error) {
   297  	log := fog.FromContext(ctx).WithName("get-ca-bundle")
   298  	const getCaFailMsg string = "unable to get the ca bundle"
   299  
   300  	cm, err := getIdentityCM(ctx, c)
   301  	if errors.IsNotFound(err) {
   302  		log.Info("identity configmap not found - get ca from the trust anchor secret")
   303  		caBundle, err := SecretExists(ctx, c)
   304  
   305  		if err == nil && caBundle == "" {
   306  			missingTrustErr := fmt.Errorf("trustanchor secret does not exist")
   307  			log.Error(missingTrustErr, getCaFailMsg)
   308  			return "", missingTrustErr
   309  		} else if err != nil {
   310  			log.Error(err, getCaFailMsg)
   311  			return "", err
   312  		}
   313  
   314  		return caBundle, nil
   315  	} else if err != nil {
   316  		log.Error(err, getCaFailMsg)
   317  		return "", err
   318  	}
   319  
   320  	bundle, exists := cm.Data[CaBundleKey]
   321  	if !exists {
   322  		missingCABundleErr := fmt.Errorf("ca bundle not found in configmap")
   323  		log.Error(missingCABundleErr, getCaFailMsg)
   324  		return "", missingCABundleErr
   325  	}
   326  	return bundle, nil
   327  }
   328  
   329  // CheckCaBundle checks that the ca bundle in the identity configmap contains the currently
   330  // deployed trust anchor secret.
   331  func CheckCaBundle(ctx context.Context, c client.Client) (bool, error) {
   332  	log := fog.FromContext(ctx).WithName("check-ca-bundle")
   333  	currentRootCa, err := SecretExists(ctx, c)
   334  	if err != nil {
   335  		return false, err
   336  	} else if currentRootCa == "" {
   337  		return false, fmt.Errorf("trust anchor secret does not exist")
   338  	}
   339  
   340  	caBundle, err := GetCaBundle(ctx, c)
   341  	if err != nil {
   342  		return false, err
   343  	}
   344  
   345  	bundleCerts, err := tls.DecodePEMCertificates(caBundle)
   346  	if err != nil {
   347  		return false, err
   348  	}
   349  	currentCert, err := tls.DecodePEMCertificates(currentRootCa)
   350  	if err != nil {
   351  		return false, err
   352  	}
   353  	if slices.ContainsFunc(currentCert, func(entry *x509.Certificate) bool {
   354  		for _, cert := range bundleCerts {
   355  			if cert.Equal(entry) {
   356  				return true
   357  			}
   358  		}
   359  		return false
   360  	}) {
   361  		return true, nil
   362  	}
   363  
   364  	log.Info("identity configmap is not up-to-date")
   365  	return false, nil
   366  }
   367  
   368  // getLastValidCa returns the ca in the bundle that was created last
   369  func getLastValidCa(caBundle string) (string, error) {
   370  	certs, err := tls.DecodePEMCertificates(caBundle)
   371  	if err != nil {
   372  		return "", err
   373  	}
   374  
   375  	if len(certs) == 0 {
   376  		return "", nil
   377  	} else if len(certs) > 1 {
   378  		// sort ca bundle certificates in ascending order from the earliest
   379  		// validity to the latest validity
   380  		slices.SortFunc(certs, func(certA, certB *x509.Certificate) int {
   381  			if certA.NotAfter.Before(certB.NotAfter) {
   382  				return -1
   383  			}
   384  			return 1
   385  		})
   386  	}
   387  
   388  	return tls.EncodeCertificatesPEM(certs[len(certs)-1]), nil
   389  }
   390  
   391  func getIdentityCM(ctx context.Context, c client.Client) (*corev1.ConfigMap, error) {
   392  	cm := &corev1.ConfigMap{}
   393  	err := c.Get(ctx, types.NamespacedName{Name: linkerd.LinkerdIdentityConfigMap, Namespace: linkerd.Namespace}, cm)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	return cm, nil
   398  }
   399  
   400  func getSecret(ctx context.Context, c client.Client) (*corev1.Secret, error) {
   401  	log := fog.FromContext(ctx).WithName("get-secret")
   402  	secret := &corev1.Secret{}
   403  	err := c.Get(ctx, linkerd.TrustAnchorKey(), secret)
   404  	if errors.IsNotFound(err) {
   405  		log.Info("the trust anchor secret was not found")
   406  		return nil, nil
   407  	} else if err != nil {
   408  		log.Error(err, "failed to get trust anchor secret")
   409  		return nil, fmt.Errorf("failed to check if trust anchor secret exists: %w", err)
   410  	}
   411  	return secret, err
   412  }
   413  
   414  // createSecret generates the Linkerd trust anchor signing key pair
   415  // and creates the K8s secret Linkerd is expecting if it does not already exist.
   416  // https://linkerd.io/2.11/tasks/automatically-rotating-control-plane-tls-credentials/#save-the-signing-key-pair-as-a-secret
   417  func createSecret(ctx context.Context, c client.Client, l5d *l5dv1alpha1.Linkerd) (string, error) {
   418  	log := fog.FromContext(ctx).WithName("create-secret")
   419  	cert, secret, err := generateSecret(ctx, l5d)
   420  	if err != nil {
   421  		log.Error(err, "failed to generate trust anchor secret")
   422  		return "", err
   423  	}
   424  
   425  	if err := c.Create(ctx, secret, linkerd.CreateOpts()); client.IgnoreAlreadyExists(err) != nil {
   426  		log.Error(err, "failed to create trust anchor secret")
   427  		return "", fmt.Errorf("failed to create secret")
   428  	}
   429  	log.Info("the trust anchor secret was created or already exists")
   430  	return cert, nil
   431  }
   432  
   433  func generateSecret(ctx context.Context, l5d *l5dv1alpha1.Linkerd) (string, *corev1.Secret, error) {
   434  	cert, key, err := GenerateTrustAnchor(ctx)
   435  	if err != nil {
   436  		return "", nil, err
   437  	}
   438  	return string(cert), buildSecret(l5d, cert, key), nil
   439  }
   440  

View as plain text