cluster-api-provider-rke2/pkg/secret/certificates.go

546 lines
16 KiB
Go

/*
Copyright 2022 SUSE.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package secret
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"path/filepath"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util/certs"
bootstrapv1 "github.com/rancher/cluster-api-provider-rke2/bootstrap/api/v1beta1"
"github.com/rancher/cluster-api-provider-rke2/pkg/consts"
)
const (
// DefaultCertificatesDir is the default location (file path) where the provider will put the certificates, this location will then
// be automatically used by RKE2 to use the pre-defined certificates instead of generating them.
DefaultCertificatesDir = "/var/lib/rancher/rke2/server/tls"
// DefaultETCDCertificatesDir is the default location (file path) where the provider will put the etcd certificates, this location will then
// be automatically used by RKE2 to use the pre-defined certificates instead of generating them.
DefaultETCDCertificatesDir = DefaultCertificatesDir + "/etcd"
// Kubeconfig is the secret name suffix storing the Cluster Kubeconfig.
Kubeconfig = Purpose("kubeconfig")
// KubeconfigDataName is the data entry name for the Kubeconfig file content.
KubeconfigDataName string = "value"
// EtcdCA is the secret name suffix for the Etcd CA.
EtcdCA Purpose = "peer-etcd"
// EtcdServerCA is the secret name suffix for the Etcd CA.
EtcdServerCA Purpose = "etcd"
// ClusterCA is the secret name suffix for APIServer CA.
ClusterCA = Purpose("ca")
// ClientClusterCA is the secret name suffix for APIServer CA.
ClientClusterCA = Purpose("cca")
// TLSKeyDataName is the key used to store a TLS private key in the secret's data field.
TLSKeyDataName = "tls.key"
// TLSCrtDataName is the key used to store a TLS certificate in the secret's data field.
TLSCrtDataName = "tls.crt"
// APIServerEtcdClient is the secret name of user-supplied secret containing the apiserver-etcd-client key/cert.
APIServerEtcdClient Purpose = "apiserver-etcd-client"
// ServiceAccount is the secret name suffix for the Service Account keys.
ServiceAccount Purpose = "sa"
// TenYears is the duration of one year.
TenYears = time.Hour * 24 * 365 * 10
)
// Purpose is the name to append to the secret generated for a cluster.
type Purpose string
// CertificatesGenerator is an interface for certificate content generation and storage.
type CertificatesGenerator interface {
Lookup(ctx context.Context, ctrlclient client.Reader, clusterName client.ObjectKey) error
Generate() error
SaveGenerated(ctx context.Context, ctrlclient client.Client, clusterName client.ObjectKey, owner metav1.OwnerReference) error
LookupOrGenerate(ctx context.Context, ctrlclient client.Client, clusterName client.ObjectKey, owner metav1.OwnerReference) error
}
// Certificate is representing common operations on certificate rereival from the cluster.
type Certificate interface {
GetPurpose() Purpose
GetKeyPair() *certs.KeyPair
SetKeyPair(keyPair *certs.KeyPair)
Lookup(ctx context.Context, cl client.Reader, key client.ObjectKey) (*corev1.Secret, error)
Generate() error
IsGenerated() bool
IsExternal() bool
SaveGenerated(ctx context.Context, cl client.Client, key client.ObjectKey, owner metav1.OwnerReference) error
AsSecret(clusterName client.ObjectKey, owner metav1.OwnerReference) *corev1.Secret
AsFiles() []bootstrapv1.File
}
// ManagedCertificate represents a single certificate CA.
type ManagedCertificate struct {
External bool
Generated bool
Purpose Purpose
KeyPair *certs.KeyPair
CertFile, KeyFile string
}
// SaveGenerated implements Certificate.
func (c *ManagedCertificate) SaveGenerated(ctx context.Context, cl client.Client, key types.NamespacedName, owner metav1.OwnerReference) error {
s := c.AsSecret(key, owner)
if err := cl.Get(ctx, client.ObjectKeyFromObject(s), &corev1.Secret{}); apierrors.IsNotFound(err) {
if err := cl.Create(ctx, s); client.IgnoreAlreadyExists(err) != nil {
return errors.WithStack(err)
}
} else if err != nil {
return errors.WithStack(err)
}
return nil
}
// Lookup implements certificate lookup.
func (c *ManagedCertificate) Lookup(ctx context.Context, ctrlclient client.Reader, clusterName client.ObjectKey) (*corev1.Secret, error) {
s := &corev1.Secret{}
key := client.ObjectKey{
Name: Name(clusterName.Name, c.GetPurpose()),
Namespace: clusterName.Namespace,
}
if err := ctrlclient.Get(ctx, key, s); err != nil {
if apierrors.IsNotFound(err) {
if c.IsExternal() {
return nil, errors.WithMessage(err, "external certificate not found")
}
return nil, nil //nolint:nilnil
}
return nil, errors.WithStack(err)
}
return s, nil
}
var (
_ CertificatesGenerator = &Certificates{}
_ Certificate = &ManagedCertificate{}
)
// Certificates are the certificates necessary to bootstrap a cluster.
type Certificates []Certificate
// NewCertificatesForInitialControlPlane returns a list of certificates configured for a control plane node.
func NewCertificatesForInitialControlPlane() Certificates {
certificatesDir := DefaultCertificatesDir
certificates := Certificates{
&ManagedCertificate{
Purpose: ClusterCA,
CertFile: filepath.Join(certificatesDir, "server-ca.crt"),
KeyFile: filepath.Join(certificatesDir, "server-ca.key"),
},
&ManagedCertificate{
Purpose: ClientClusterCA,
CertFile: filepath.Join(certificatesDir, "client-ca.crt"),
KeyFile: filepath.Join(certificatesDir, "client-ca.key"),
},
&ManagedCertificate{
Purpose: EtcdCA,
CertFile: filepath.Join(DefaultETCDCertificatesDir, "peer-ca.crt"),
KeyFile: filepath.Join(DefaultETCDCertificatesDir, "peer-ca.key"),
},
&ManagedCertificate{
Purpose: EtcdServerCA,
CertFile: filepath.Join(DefaultETCDCertificatesDir, "server-ca.crt"),
KeyFile: filepath.Join(DefaultETCDCertificatesDir, "server-ca.key"),
},
}
return certificates
}
// NewCertificatesForLegacyControlPlane returns a list of certificates configured for a control plane node, excluding etcd certificates set.
func NewCertificatesForLegacyControlPlane() Certificates {
certificatesDir := DefaultCertificatesDir
certificates := Certificates{
&ManagedCertificate{
Purpose: ClusterCA,
CertFile: filepath.Join(certificatesDir, "server-ca.crt"),
KeyFile: filepath.Join(certificatesDir, "server-ca.key"),
},
&ManagedCertificate{
Purpose: ClientClusterCA,
CertFile: filepath.Join(certificatesDir, "client-ca.crt"),
KeyFile: filepath.Join(certificatesDir, "client-ca.key"),
},
}
return certificates
}
// GetByPurpose returns a certificate by the given name.
// This could be removed if we use a map instead of a slice to hold certificates, however other code becomes more complex.
func (c Certificates) GetByPurpose(purpose Purpose) Certificate {
for _, certificate := range c {
if certificate.GetPurpose() == purpose {
return certificate
}
}
return nil
}
// Lookup looks up each certificate from secrets and populates the certificate with the secret data.
func (c Certificates) Lookup(ctx context.Context, ctrlclient client.Reader, clusterName client.ObjectKey) error {
// Look up each certificate as a secret and populate the certificate/key
for _, certificate := range c {
s, err := certificate.Lookup(ctx, ctrlclient, clusterName)
if err != nil || s == nil {
return err
}
// If a user has a badly formatted secret it will prevent the cluster from working.
kp, err := secretToKeyPair(s)
if err != nil {
return err
}
certificate.SetKeyPair(kp)
}
return nil
}
// Generate will generate any certificates that do not have KeyPair data.
func (c *ManagedCertificate) Generate() error {
// Do not generate the APIServerEtcdClient key pair. It is user supplied
if c.Purpose == APIServerEtcdClient {
return nil
}
generator := generateCACert
if c.Purpose == ServiceAccount {
generator = generateServiceAccountKeys
}
kp, err := generator()
if err != nil {
return err
}
c.SetKeyPair(kp)
c.Generated = true
return nil
}
// GetPurpose returns the assigned purpose for the certificate.
func (c *ManagedCertificate) GetPurpose() Purpose {
return c.Purpose
}
// GetKeyPair gets the certificate key pair.
func (c *ManagedCertificate) GetKeyPair() *certs.KeyPair {
return c.KeyPair
}
// SetKeyPair sets the certificate key pair.
func (c *ManagedCertificate) SetKeyPair(keyPair *certs.KeyPair) {
c.KeyPair = keyPair
}
// IsGenerated returns if this time the certificate was newly generated, opposed to being fetched from cache.
func (c *ManagedCertificate) IsGenerated() bool {
return c.Generated
}
// IsExternal returns true for extenally managed cerificates.
func (c *ManagedCertificate) IsExternal() bool {
return c.External
}
// Generate will generate any certificates that do not have KeyPair data.
func (c Certificates) Generate() error {
for _, certificate := range c {
if certificate.GetKeyPair() == nil {
err := certificate.Generate()
if err != nil {
return err
}
}
}
return nil
}
// SaveGenerated will save any certificates that have been generated as Kubernetes secrets.
func (c Certificates) SaveGenerated(ctx context.Context, ctrlclient client.Client, clusterName client.ObjectKey, owner metav1.OwnerReference) error {
for _, certificate := range c {
if err := certificate.SaveGenerated(ctx, ctrlclient, clusterName, owner); err != nil {
return errors.WithStack(err)
}
}
return nil
}
// LookupOrGenerate is a convenience function that wraps cluster bootstrap certificate behavior.
func (c Certificates) LookupOrGenerate(
ctx context.Context,
ctrlclient client.Client,
clusterName client.ObjectKey,
owner metav1.OwnerReference,
) error {
// Find the certificates that exist
if err := c.Lookup(ctx, ctrlclient, clusterName); err != nil {
return err
}
// Generate the certificates that don't exist
if err := c.Generate(); err != nil {
return err
}
// Save any certificates that have been generated
return c.SaveGenerated(ctx, ctrlclient, clusterName, owner)
}
// AsSecret converts a single certificate into a Kubernetes secret.
func (c *ManagedCertificate) AsSecret(clusterName client.ObjectKey, owner metav1.OwnerReference) *corev1.Secret {
s := asSecret(map[string][]byte{
TLSKeyDataName: c.KeyPair.Key,
TLSCrtDataName: c.KeyPair.Cert,
}, c.GetPurpose(), clusterName, owner)
if c.Generated {
s.OwnerReferences = []metav1.OwnerReference{owner}
}
return s
}
// AsFiles converts the certificate to a slice of Files that may have 0, 1 or 2 Files.
func (c *ManagedCertificate) AsFiles() []bootstrapv1.File {
out := make([]bootstrapv1.File, 0)
if len(c.KeyPair.Cert) > 0 {
out = append(out, bootstrapv1.File{
Path: c.CertFile,
Owner: consts.DefaultFileOwner,
Permissions: "0640",
Content: string(c.KeyPair.Cert),
})
}
if len(c.KeyPair.Key) > 0 {
out = append(out, bootstrapv1.File{
Path: c.KeyFile,
Owner: consts.DefaultFileOwner,
Permissions: "0600",
Content: string(c.KeyPair.Key),
})
}
return out
}
// Name returns the name of the secret for a cluster.
func Name(cluster string, suffix Purpose) string {
return fmt.Sprintf("%s-%s", cluster, suffix)
}
// AsFiles converts a slice of certificates into bootstrap files.
func (c Certificates) AsFiles() []bootstrapv1.File {
clusterCA := c.GetByPurpose(ClusterCA)
clientClusterCA := c.GetByPurpose(ClientClusterCA)
etcdCA := c.GetByPurpose(EtcdCA)
etcdServerCA := c.GetByPurpose(EtcdServerCA)
certFiles := make([]bootstrapv1.File, 0)
if clusterCA != nil {
certFiles = append(certFiles, clusterCA.AsFiles()...)
}
if clientClusterCA != nil {
certFiles = append(certFiles, clientClusterCA.AsFiles()...)
}
if etcdCA != nil {
certFiles = append(certFiles, etcdCA.AsFiles()...)
}
if etcdServerCA != nil {
certFiles = append(certFiles, etcdServerCA.AsFiles()...)
}
// these will only exist if external etcd was defined and supplied by the user
apiserverEtcdClientCert := c.GetByPurpose(APIServerEtcdClient)
if apiserverEtcdClientCert != nil {
certFiles = append(certFiles, apiserverEtcdClientCert.AsFiles()...)
}
return certFiles
}
// secretToKeyPair gets a Certificate Keypair from a data entry in a secret.
func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) {
c, exists := s.Data[TLSCrtDataName]
if !exists {
return nil, errors.Errorf("missing data for key %s", TLSCrtDataName)
}
// In some cases (external etcd) it's ok if the etcd.key does not exist.
key, exists := s.Data[TLSKeyDataName]
if !exists {
key = []byte("")
}
return &certs.KeyPair{
Cert: c,
Key: key,
}, nil
}
func generateCACert() (*certs.KeyPair, error) {
x509Cert, privKey, err := newCertificateAuthority()
if err != nil {
return nil, err
}
return &certs.KeyPair{
Cert: certs.EncodeCertPEM(x509Cert),
Key: certs.EncodePrivateKeyPEM(privKey),
}, nil
}
// newCertificateAuthority creates new certificate and private key for the certificate authority.
func newCertificateAuthority() (*x509.Certificate, *rsa.PrivateKey, error) {
key, err := certs.NewPrivateKey()
if err != nil {
return nil, nil, err
}
c, err := newSelfSignedCACert(key)
if err != nil {
return nil, nil, err
}
return c, key, nil
}
// newSelfSignedCACert creates a CA certificate.
func newSelfSignedCACert(key *rsa.PrivateKey) (*x509.Certificate, error) {
cfg := certs.Config{
CommonName: "kubernetes",
}
now := time.Now().UTC()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: cfg.CommonName,
Organization: cfg.Organization,
},
NotBefore: now.Add(time.Minute * -5),
NotAfter: now.Add(TenYears), // 10 years
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
MaxPathLenZero: true,
BasicConstraintsValid: true,
MaxPathLen: 0,
IsCA: true,
}
b, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key)
if err != nil {
return nil, errors.Wrapf(err, "failed to create self signed CA certificate: %+v", tmpl)
}
c, err := x509.ParseCertificate(b)
return c, errors.WithStack(err)
}
func generateServiceAccountKeys() (*certs.KeyPair, error) {
saCreds, err := certs.NewPrivateKey()
if err != nil {
return nil, err
}
saPub, err := certs.EncodePublicKeyPEM(&saCreds.PublicKey)
if err != nil {
return nil, err
}
return &certs.KeyPair{
Cert: saPub,
Key: certs.EncodePrivateKeyPEM(saCreds),
}, nil
}
func asSecret(data map[string][]byte, purpose Purpose, clusterName types.NamespacedName, _ metav1.OwnerReference) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: clusterName.Namespace,
Name: Name(clusterName.Name, purpose),
Labels: map[string]string{
clusterv1.ClusterNameLabel: clusterName.Name,
},
},
Data: data,
Type: clusterv1.ClusterSecretType,
}
}
// GetFromNamespacedName retrieves the specified Secret (if any) from the given
// cluster name and namespace.
func GetFromNamespacedName(ctx context.Context, c client.Reader, clusterName client.ObjectKey, purpose Purpose) (*corev1.Secret, error) {
secret := &corev1.Secret{}
secretKey := client.ObjectKey{
Namespace: clusterName.Namespace,
Name: Name(clusterName.Name, purpose),
}
if err := c.Get(ctx, secretKey, secret); err != nil {
return nil, err
}
return secret, nil
}