diff --git a/config/samples/certificateProvider/cert_manager_selfsigned_issuer.yaml b/config/samples/certificateProvider/cert_manager_selfsigned_issuer.yaml new file mode 100644 index 0000000..8120e82 --- /dev/null +++ b/config/samples/certificateProvider/cert_manager_selfsigned_issuer.yaml @@ -0,0 +1,6 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned +spec: + selfSigned: {} diff --git a/go.mod b/go.mod index 64e4183..99d3c14 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 toolchain go1.24.4 require ( + github.com/cert-manager/cert-manager v1.17.2 github.com/go-logr/logr v1.4.3 github.com/stretchr/testify v1.10.0 go.etcd.io/etcd/api/v3 v3.6.1 @@ -43,15 +44,15 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -66,7 +67,7 @@ require ( github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -97,9 +98,9 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect @@ -120,7 +121,7 @@ require ( k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/e2e-framework v0.6.0 - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 9125b03..9add5ba 100644 --- a/go.sum +++ b/go.sum @@ -19,7 +19,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -34,8 +33,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -45,12 +44,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -102,8 +99,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -266,8 +261,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -318,8 +313,10 @@ sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytI sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= +sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/pkg/certificate/cert-manager/doc.go b/pkg/certificate/cert_manager/doc.go similarity index 100% rename from pkg/certificate/cert-manager/doc.go rename to pkg/certificate/cert_manager/doc.go diff --git a/pkg/certificate/cert_manager/provider.go b/pkg/certificate/cert_manager/provider.go new file mode 100644 index 0000000..b622c5b --- /dev/null +++ b/pkg/certificate/cert_manager/provider.go @@ -0,0 +1,385 @@ +package cert_manager + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "log" + "net" + "strings" + "time" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + interfaces "go.etcd.io/etcd-operator/pkg/certificate/interfaces" +) + +const ( + IssuerNameKey = "issuerName" + IssuerKindKey = "issuerKind" +) + +type CertManagerProvider struct { + client.Client +} + +var _ interfaces.Provider = (*CertManagerProvider)(nil) + +func New(c client.Client) interfaces.Provider { + return &CertManagerProvider{ + c, + } +} + +func (cm *CertManagerProvider) EnsureCertificateSecret(ctx context.Context, secretName, namespace string, + cfg *interfaces.Config) error { + cmCertificate := &certmanagerv1.Certificate{} + err := cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, cmCertificate) + if err != nil { + if k8serrors.IsNotFound(err) { + valErr := cm.validateCertificateConfig(ctx, namespace, cfg) + if valErr != nil { + return valErr + } + err := cm.createCertificate(ctx, secretName, namespace, cfg) + if err != nil { + return err + } + } else { + return err + } + } + + log.Printf("Valid certificate: %s present in namespace: %s, checking certificate status...", secretName, namespace) + + err = cm.checkCertificateStatus(secretName, namespace, ctx) + if err != nil && !errors.Is(err, interfaces.ErrUnknown) { + for try := range interfaces.MaxRetries { + // Wait for the certificate to be in "Ready" state + // Reference: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers + log.Printf("Certificate Status: retry attempt %v, after %v, error: %v", try, interfaces.RetryInterval, err) + time.Sleep(interfaces.RetryInterval) + err = cm.checkCertificateStatus(secretName, namespace, ctx) + if err == nil { + break + } + } + if err != nil { + return err + } + } else { + return err + } + + log.Printf("Certificate Status: %s ready in namespace: %s, validating associated secret...", secretName, namespace) + + err = cm.ValidateCertificateSecret(ctx, secretName, namespace, cfg) + if err != nil { + if k8serrors.IsNotFound(err) { + return err + } else { + return fmt.Errorf("invalid certificate secret: %s present in namespace: %s, please delete and try again.\nError: %s", + secretName, namespace, err) + } + } + + log.Printf("Valid certificate secret: %s already present in namespace: %s", secretName, namespace) + return nil +} + +func (cm *CertManagerProvider) ValidateCertificateSecret(ctx context.Context, secretName, namespace string, + _ *interfaces.Config) error { + var err error + secret := &corev1.Secret{} + err = cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret) + if err != nil && k8serrors.IsNotFound(err) { + for try := range interfaces.MaxRetries { + // Wait for cert-manager reconciler to create the associated certificate secret + // Reference: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers + log.Printf("Valid certificate secret: retry attempt %v, after %v, error: %v", try+1, interfaces.RetryInterval, err) + time.Sleep(interfaces.RetryInterval) + err = cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret) + if err == nil { + break + } + } + if err != nil { + return err + } + } else { + return err + } + + certificateData, exists := secret.Data["tls.crt"] + if !exists { + return interfaces.ErrTLSCert + } + + decodeCertificatePem, _ := pem.Decode(certificateData) + if decodeCertificatePem == nil { + return interfaces.ErrDecodeCert + } + + privateKeyData, keyExists := secret.Data["tls.key"] + if !keyExists { + return interfaces.ErrTLSKey + } + + parseCert, err := x509.ParseCertificate(decodeCertificatePem.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + + if parseCert.NotAfter.Before(time.Now()) { + return interfaces.ErrCertExpired + } + + privateKey, err := parsePrivateKey(privateKeyData) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + if checkKeyPairErr := checkKeyPair(parseCert, privateKey); checkKeyPairErr != nil { + return fmt.Errorf("private key does not match certificate: %w", checkKeyPairErr) + } + + return nil +} + +func (cm *CertManagerProvider) DeleteCertificateSecret(ctx context.Context, secretName, namespace string) error { + cmCertificate := &certmanagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + } + certStatusErr := cm.checkCertificateStatus(secretName, namespace, ctx) + if certStatusErr != nil { + log.Printf("Certificate associated not ready yet, try again later.") + return certStatusErr + } + + dErr := cm.Delete(ctx, cmCertificate) + if dErr != nil { + return dErr + } + + // By default, cert-manager Certificate deletion does not delete the associated secret. + // Existing secret will allow to services relying on that Certificate, so additionally delete it + // More info: https://cert-manager.io/docs/usage/certificate/#cleaning-up-secrets-when-certificates-are-deleted + cmSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + } + dSecretErr := cm.Delete(ctx, cmSecret) + if dSecretErr != nil { + if k8serrors.IsNotFound(dSecretErr) { + fmt.Println("Certificate secret not found, maybe already deleted") + } else { + return dSecretErr + } + + } + + return nil +} + +// RevokeCertificate is not supported, certificates can only be deleted which is handled by DeleteCertificateSecret +// as per official documentation: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers +func (cm *CertManagerProvider) RevokeCertificate(ctx context.Context, secretName string, namespace string) error { + return nil +} + +func (cm *CertManagerProvider) GetCertificateConfig(ctx context.Context, + secretName, namespace string) (*interfaces.Config, error) { + cmCertificate := &certmanagerv1.Certificate{} + err := cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, cmCertificate) + if err != nil { + return nil, fmt.Errorf("failed to get certificate: %w", err) + } + + var ipAddresses []net.IP + if len(cmCertificate.Spec.IPAddresses) != 0 { + ipAddresses = make([]net.IP, len(cmCertificate.Spec.IPAddresses)) + } else { + ipAddresses = nil + } + + cfg := &interfaces.Config{ + CommonName: cmCertificate.Spec.CommonName, + Organization: cmCertificate.Spec.Subject.Organizations, + AltNames: interfaces.AltNames{ + DNSNames: cmCertificate.Spec.DNSNames, + IPs: ipAddresses, + }, + ValidityDuration: cmCertificate.Spec.Duration.Duration, + ExtraConfig: map[string]any{ + IssuerNameKey: cmCertificate.Spec.IssuerRef.Name, + IssuerKindKey: cmCertificate.Spec.IssuerRef.Kind, + }, + } + + return cfg, nil +} + +// checkCertificateStatus returns the current status of the certificate creation +func (cm *CertManagerProvider) checkCertificateStatus(certificateName, namespace string, ctx context.Context) error { + cmCertificate := &certmanagerv1.Certificate{} + err := cm.Get(ctx, client.ObjectKey{Name: certificateName, Namespace: namespace}, cmCertificate) + if err != nil { + return err + } + cmStatus := cmCertificate.Status.Conditions + for _, condition := range cmStatus { + switch condition.Type { + case certmanagerv1.CertificateConditionReady: + log.Printf("Certificate Ready: %v (Reason: %s, Message: %s)\n", + condition.Status, condition.Reason, condition.Message) + return nil + case certmanagerv1.CertificateConditionIssuing: + return fmt.Errorf("certificate Issuing: %v (Reason: %s, Message: %s), error: %w \n", + condition.Status, condition.Reason, condition.Message, interfaces.ErrPending) + default: + return fmt.Errorf("certificate status unknown: %v (Reason: %s, Message: %s), error: %w\n", + condition.Status, condition.Reason, condition.Message, interfaces.ErrUnknown) + } + } + return nil +} + +// checkIssuerExists checks for if the provided issuer is present in the namespace/cluster +func (cm *CertManagerProvider) checkIssuerExists(issuerName, issuerKind, namespace string, ctx context.Context) error { + switch issuerKind { + case "Issuer": + issuer := &certmanagerv1.Issuer{} + err := cm.Get(ctx, client.ObjectKey{Name: issuerName, Namespace: namespace}, issuer) + if k8serrors.IsNotFound(err) { + return fmt.Errorf("issuer %s not found in namespace %s", issuerName, namespace) + } + case "ClusterIssuer": + clusterIssuer := &certmanagerv1.ClusterIssuer{} + err := cm.Get(ctx, client.ObjectKey{Name: issuerName}, clusterIssuer) + if k8serrors.IsNotFound(err) { + return fmt.Errorf("clusterIssuer %s not found", issuerName) + } + default: + return fmt.Errorf("unsupported issuer kind: %s", issuerKind) + } + return nil +} + +// validateCertificateConfig checks if the config passed is valid +func (cm *CertManagerProvider) validateCertificateConfig(ctx context.Context, namespace string, + cfg *interfaces.Config) error { + issuerName, isValid := cfg.ExtraConfig[IssuerNameKey].(string) + if !isValid { + return fmt.Errorf("value for %s not correctly provided, try again", IssuerNameKey) + } + issuerKind, isValid := cfg.ExtraConfig[IssuerKindKey].(string) + if !isValid { + return fmt.Errorf("value for %s not correctly provided, try again", IssuerKindKey) + } + checkIssuerExist := cm.checkIssuerExists(issuerName, issuerKind, namespace, ctx) + if checkIssuerExist != nil { + return checkIssuerExist + } + return nil +} + +// createCertificate creates a cert-manager Certificate resource in the specified namespace. +// DNSNames and IPAddresses if not user-defined, will be set to default value in runtime: +// fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local", ec.Name, index, ec.Name, ec.Namespace) +// returns an error if the Certificate resource cannot be created. +func (cm *CertManagerProvider) createCertificate(ctx context.Context, secretName, namespace string, + cfg *interfaces.Config) error { + issuerName, isValid := cfg.ExtraConfig[IssuerNameKey].(string) + if !isValid { + return fmt.Errorf("value for %s not correctly provided, try again", IssuerNameKey) + } + issuerKind, isValid := cfg.ExtraConfig[IssuerKindKey].(string) + if !isValid { + return fmt.Errorf("value for %s not correctly provided, try again", IssuerKindKey) + } + + certificateResource := &certmanagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Spec: certmanagerv1.CertificateSpec{ + CommonName: cfg.CommonName, + Subject: &certmanagerv1.X509Subject{ + Organizations: cfg.Organization, + }, + SecretName: secretName, + DNSNames: cfg.AltNames.DNSNames, + IPAddresses: strings.Fields(strings.Trim(fmt.Sprint(cfg.AltNames.IPs), "[]")), + IssuerRef: cmmeta.ObjectReference{ + Name: issuerName, + Kind: issuerKind, + }, + Duration: &metav1.Duration{Duration: cfg.ValidityDuration}, + }, + } + + return cm.Create(ctx, certificateResource) +} + +// parsePrivateKey parses the private key from the PEM-encoded data. +func parsePrivateKey(privateKeyData []byte) (crypto.PrivateKey, error) { + block, _ := pem.Decode(privateKeyData) + if block == nil { + return nil, errors.New("failed to decode private key: invalid PEM") + } + + // Parse the private key from the PEM block + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + // Parse the private key in another format (e.g., RSA) + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + } + + return privateKey, nil +} + +// checkKeyPair checks if the private key matches the certificate by validating the public key +func checkKeyPair(cert *x509.Certificate, privateKey crypto.PrivateKey) error { + switch key := privateKey.(type) { + case *rsa.PrivateKey: + pub, ok := cert.PublicKey.(*rsa.PublicKey) + if !ok || !key.PublicKey.Equal(pub) { + return interfaces.ErrRSAKeyPair + } + case *ecdsa.PrivateKey: + pub, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok || !key.PublicKey.Equal(pub) { + return interfaces.ErrECDSAKeyPair + } + case *ed25519.PrivateKey: + pub, ok := cert.PublicKey.(ed25519.PublicKey) + if !ok || !bytes.Equal(key.Public().(ed25519.PublicKey), pub) { + return interfaces.ErrED25519KeyPair + } + default: + return fmt.Errorf("unsupported private key type: %T", key) + } + + return nil +} diff --git a/pkg/certificate/certificate.go b/pkg/certificate/certificate.go index 2eb8481..037ef11 100644 --- a/pkg/certificate/certificate.go +++ b/pkg/certificate/certificate.go @@ -3,6 +3,9 @@ package certificate import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + + certManager "go.etcd.io/etcd-operator/pkg/certificate/cert_manager" certInterface "go.etcd.io/etcd-operator/pkg/certificate/interfaces" ) @@ -14,12 +17,12 @@ const ( // add more ... ) -func NewProvider(pt ProviderType) (certInterface.Provider, error) { +func NewProvider(pt ProviderType, c client.Client) (certInterface.Provider, error) { switch pt { case Auto: return nil, nil // change me later case CertManager: - return nil, nil // change me later + return certManager.New(c), nil } return nil, fmt.Errorf("unknown provider type: %s", pt) diff --git a/pkg/certificate/interfaces/interface.go b/pkg/certificate/interfaces/interface.go index 394a034..1e68c23 100644 --- a/pkg/certificate/interfaces/interface.go +++ b/pkg/certificate/interfaces/interface.go @@ -2,10 +2,47 @@ package certificate import ( "context" + "errors" "net" "time" ) +var ( + // ErrPending is returned when the Certificate is not in "Ready" state + ErrPending = errors.New("certificate creation pending") + + // ErrUnknown is returned when the Certificate status does not match the provider defined states + ErrUnknown = errors.New("certificate status unknown") + + // ErrTLSKey is returned when private key not found in Certificate secret + ErrTLSKey = errors.New("private key not found in secret") + + // ErrTLSCert is returned when private key certificate not found in Certificate secret + ErrTLSCert = errors.New("certificate not found in secret") + + // ErrDecodeCert is returned when failed to decode PEM block of tls.crt of Certificate secret + ErrDecodeCert = errors.New("failed to decode PEM block") + + // ErrCertExpired is returned when certificate has expired + ErrCertExpired = errors.New("certificate has expired") + + // ErrRSAKeyPair is returned when private key(RSA) does not match the public key in the Certificate secret + ErrRSAKeyPair = errors.New("private key(RSA) does not match the public key in the certificate") + + // ErrECDSAKeyPair is returned when private key(ECDSA) does not match the public key in the Certificate secret + ErrECDSAKeyPair = errors.New("private key(ECDSA) does not match the public key in the certificate") + + // ErrED25519KeyPair is returned when private key(ED25519) does not match the public key in the Certificate secret + ErrED25519KeyPair = errors.New("private key(ED25519) does not match the public key in the certificate") +) + +const ( + // MaxRetries is the maximum number of retry attempts for EnsureCertificateSecret, ValidateCertificateSecret + // with a delay of RetryInterval between consecutive retries + MaxRetries = 36 + RetryInterval = 5 * time.Second +) + // AltNames contains the domain names and IP addresses that will be added // to the x509 certificate SubAltNames fields. The values will be passed // directly to the x509.Certificate object.