Skip to content

Commit 82651bc

Browse files
committed
Implement AutoProvider interface
Signed-off-by: ArkaSaha30 <[email protected]>
1 parent 7e2cc13 commit 82651bc

File tree

3 files changed

+259
-5
lines changed

3 files changed

+259
-5
lines changed

api/v1alpha1/etcdcluster_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ type CommonConfig struct {
9898
AltNames AltNames `json:"altNames,omitempty"`
9999

100100
// ValidityDuration is the expected duration until which the certificate will be valid,
101-
// expects in human-readable duration: 100d12h, if empty defaults to 90d
101+
// expects in human-readable duration: 100d12h, if empty defaults to 90d for cert-manager
102+
// and 365d for auto as per: https://github.com/etcd-io/etcd/blob/main/client/pkg/transport/listener.go#L275
102103
// +optional
103104
ValidityDuration string `json:"validityDuration,omitempty"`
104105

config/crd/bases/operator.etcd.io_etcdclusters.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ spec:
149149
validityDuration:
150150
description: |-
151151
ValidityDuration is the expected duration until which the certificate will be valid,
152-
expects in human-readable duration: 100d12h, if empty defaults to 90d
152+
expects in human-readable duration: 100d12h, if empty defaults to 90d for cert-manager
153+
and 365d for auto as per: https://github.com/etcd-io/etcd/blob/main/client/pkg/transport/listener.go#L275
153154
type: string
154155
type: object
155156
certManagerCfg:
@@ -203,7 +204,8 @@ spec:
203204
validityDuration:
204205
description: |-
205206
ValidityDuration is the expected duration until which the certificate will be valid,
206-
expects in human-readable duration: 100d12h, if empty defaults to 90d
207+
expects in human-readable duration: 100d12h, if empty defaults to 90d for cert-manager
208+
and 365d for auto as per: https://github.com/etcd-io/etcd/blob/main/client/pkg/transport/listener.go#L275
207209
type: string
208210
required:
209211
- issuerKind

pkg/certificate/auto/provider.go

Lines changed: 253 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
package auto
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto"
7+
"crypto/ecdsa"
8+
"crypto/ed25519"
9+
"crypto/rsa"
10+
"crypto/tls"
11+
"crypto/x509"
12+
"encoding/pem"
13+
"errors"
14+
"fmt"
15+
"log"
16+
"os"
17+
"time"
518

19+
"go.uber.org/zap"
20+
corev1 "k8s.io/api/core/v1"
21+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
623
"sigs.k8s.io/controller-runtime/pkg/client"
724

825
interfaces "go.etcd.io/etcd-operator/pkg/certificate/interfaces"
26+
"go.etcd.io/etcd/client/pkg/v3/transport"
27+
)
28+
29+
const (
30+
DefaultValidity = 365 * 24 * time.Hour
931
)
1032

1133
type AutoCertProvider struct {
@@ -22,23 +44,252 @@ func New(c client.Client) interfaces.Provider {
2244

2345
func (ac *AutoCertProvider) EnsureCertificateSecret(ctx context.Context, secretName, namespace string,
2446
cfg *interfaces.Config) error {
47+
var secret corev1.Secret
48+
err := ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, &secret)
49+
if err != nil {
50+
if k8serrors.IsNotFound(err) {
51+
err := ac.createNewSecret(ctx, secretName, namespace, cfg)
52+
if err != nil {
53+
return err
54+
}
55+
} else {
56+
return err
57+
}
58+
}
59+
60+
err = ac.ValidateCertificateSecret(ctx, secretName, namespace, cfg)
61+
if err != nil {
62+
if k8serrors.IsNotFound(err) {
63+
return err
64+
} else {
65+
return fmt.Errorf("invalid certificate secret: %s present in namespace: %s, please delete and try again.\nError: %s",
66+
secretName, namespace, err)
67+
}
68+
}
69+
70+
log.Printf("Valid certificate secret: %s already present in namespace: %s", secretName, namespace)
2571
return nil
2672
}
2773

2874
func (ac *AutoCertProvider) ValidateCertificateSecret(ctx context.Context, secretName, namespace string,
2975
_ *interfaces.Config) error {
76+
var err error
77+
secret := &corev1.Secret{}
78+
err = ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
79+
if err != nil && k8serrors.IsNotFound(err) {
80+
for try := range interfaces.MaxRetries {
81+
// Wait for the certificate secret to get created
82+
log.Printf("Valid certificate secret: retry attempt %v, after %v, error: %v", try+1, interfaces.RetryInterval, err)
83+
time.Sleep(interfaces.RetryInterval)
84+
err = ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
85+
if err == nil {
86+
break
87+
}
88+
}
89+
if err != nil {
90+
return err
91+
}
92+
} else {
93+
return err
94+
}
95+
96+
certificateData, exists := secret.Data["tls.crt"]
97+
if !exists {
98+
return interfaces.ErrTLSCert
99+
}
100+
101+
decodeCertificatePem, _ := pem.Decode(certificateData)
102+
if decodeCertificatePem == nil {
103+
return interfaces.ErrDecodeCert
104+
}
105+
106+
privateKeyData, keyExists := secret.Data["tls.key"]
107+
if !keyExists {
108+
return interfaces.ErrTLSKey
109+
}
110+
111+
parseCert, err := x509.ParseCertificate(decodeCertificatePem.Bytes)
112+
if err != nil {
113+
return fmt.Errorf("failed to parse certificate: %w", err)
114+
}
115+
116+
if parseCert.NotAfter.Before(time.Now()) {
117+
return interfaces.ErrCertExpired
118+
}
119+
120+
privateKey, err := parsePrivateKey(privateKeyData)
121+
if err != nil {
122+
return fmt.Errorf("failed to parse private key: %w", err)
123+
}
124+
125+
if checkKeyPairErr := checkKeyPair(parseCert, privateKey); checkKeyPairErr != nil {
126+
return fmt.Errorf("private key does not match certificate: %w", checkKeyPairErr)
127+
}
128+
30129
return nil
31130
}
32131

33132
func (ac *AutoCertProvider) DeleteCertificateSecret(ctx context.Context, secretName, namespace string) error {
34-
return nil
133+
var secret corev1.Secret
134+
err := ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, &secret)
135+
if k8serrors.IsNotFound(err) {
136+
return nil
137+
}
138+
if err != nil {
139+
return err
140+
}
141+
return ac.Delete(ctx, &secret)
35142
}
36143

37144
func (ac *AutoCertProvider) RevokeCertificate(ctx context.Context, secretName string, namespace string) error {
38-
return nil
145+
return ac.DeleteCertificateSecret(ctx, secretName, namespace)
39146
}
40147

41148
func (ac *AutoCertProvider) GetCertificateConfig(ctx context.Context,
42149
secretName, namespace string) (*interfaces.Config, error) {
150+
var autoCertSecret corev1.Secret
151+
err := ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, &autoCertSecret)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to get certificate: %w", err)
154+
}
155+
43156
return &interfaces.Config{}, nil
44157
}
158+
159+
// parsePrivateKey parses the private key from the PEM-encoded data.
160+
func parsePrivateKey(privateKeyData []byte) (crypto.PrivateKey, error) {
161+
block, _ := pem.Decode(privateKeyData)
162+
if block == nil {
163+
return nil, errors.New("failed to decode private key: invalid PEM")
164+
}
165+
166+
// Parse the private key from the PEM block
167+
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
168+
if err != nil {
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to parse private key: %w", err)
171+
}
172+
}
173+
174+
return privateKey, nil
175+
}
176+
177+
// checkKeyPair checks if the private key matches the certificate by validating the public key
178+
func checkKeyPair(cert *x509.Certificate, privateKey crypto.PrivateKey) error {
179+
switch key := privateKey.(type) {
180+
case *rsa.PrivateKey:
181+
pub, ok := cert.PublicKey.(*rsa.PublicKey)
182+
if !ok || !key.PublicKey.Equal(pub) {
183+
return interfaces.ErrRSAKeyPair
184+
}
185+
case *ecdsa.PrivateKey:
186+
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
187+
if !ok || !key.PublicKey.Equal(pub) {
188+
return interfaces.ErrECDSAKeyPair
189+
}
190+
case *ed25519.PrivateKey:
191+
pub, ok := cert.PublicKey.(ed25519.PublicKey)
192+
if !ok || !bytes.Equal(key.Public().(ed25519.PublicKey), pub) {
193+
return interfaces.ErrED25519KeyPair
194+
}
195+
default:
196+
return fmt.Errorf("unsupported private key type: %T", key)
197+
}
198+
199+
return nil
200+
}
201+
202+
// createNewSecret generates or updates a Kubernetes TLS Secret
203+
// with a self-signed certificate in the specified namespace.
204+
// DNSNames and IPAddresses if not user-defined, will be set to default value in runtime:
205+
// fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local", ec.Name, index, ec.Name, ec.Namespace)
206+
// minimum validityDuration is 365 days
207+
// as per: https://github.com/etcd-io/etcd/blob/main/client/pkg/transport/listener.go#L275
208+
func (ac *AutoCertProvider) createNewSecret(ctx context.Context, secretName, namespace string,
209+
cfg *interfaces.Config) error {
210+
validity := DefaultValidity
211+
if cfg.ValidityDuration != 0 {
212+
validity = cfg.ValidityDuration * time.Hour
213+
}
214+
215+
tmpDir, err := os.MkdirTemp("", fmt.Sprintf("etcd-auto-cert-%s-*", secretName))
216+
if err != nil {
217+
return fmt.Errorf("failed to create temp dir for certificate generation: %w", err)
218+
}
219+
220+
defer func() {
221+
_ = os.RemoveAll(tmpDir)
222+
}()
223+
224+
var hosts []string
225+
if cfg != nil {
226+
if cfg.CommonName != "" {
227+
hosts = append(hosts, cfg.CommonName)
228+
}
229+
if len(cfg.AltNames.DNSNames) > 0 {
230+
hosts = append(hosts, cfg.AltNames.DNSNames...)
231+
}
232+
}
233+
234+
tlsInfo, selfCertErr := transport.SelfCert(zap.NewNop(), tmpDir, hosts, uint(validity/DefaultValidity))
235+
if selfCertErr != nil {
236+
return fmt.Errorf("certificate creation via transport.SelfCert failed: %w", selfCertErr)
237+
}
238+
239+
certPath := tlsInfo.CertFile
240+
keyPath := tlsInfo.KeyFile
241+
caPath := tlsInfo.TrustedCAFile
242+
243+
certPEM, err := os.ReadFile(certPath)
244+
if err != nil {
245+
return fmt.Errorf("failed to read generated cert file %s: %w", certPath, err)
246+
}
247+
248+
keyPEM, err := os.ReadFile(keyPath)
249+
if err != nil {
250+
return fmt.Errorf("failed to read generated key file %s: %w", keyPath, err)
251+
}
252+
253+
caPEM, err := os.ReadFile(caPath)
254+
if err != nil || len(caPEM) == 0 {
255+
// use certPEM when CA file is not found or empty
256+
caPEM = certPEM
257+
}
258+
259+
// Validate cert and key pair
260+
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
261+
return fmt.Errorf("generated keypair invalid: %w", err)
262+
}
263+
264+
secret := &corev1.Secret{
265+
ObjectMeta: metav1.ObjectMeta{
266+
Name: secretName,
267+
Namespace: namespace,
268+
},
269+
Type: corev1.SecretTypeTLS,
270+
Data: map[string][]byte{
271+
"tls.crt": certPEM,
272+
"tls.key": keyPEM,
273+
"ca.crt": caPEM,
274+
},
275+
}
276+
277+
// Create or Update certificate Secret
278+
if err := ac.Create(ctx, secret); err != nil {
279+
if k8serrors.IsAlreadyExists(err) {
280+
var existing corev1.Secret
281+
if getErr := ac.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, &existing); getErr != nil {
282+
return fmt.Errorf("failed to get existing secret for update: %w", getErr)
283+
}
284+
existing.Data = secret.Data
285+
286+
if updateErr := ac.Update(ctx, &existing); updateErr != nil {
287+
return fmt.Errorf("failed to update existing secret: %w", updateErr)
288+
}
289+
return nil
290+
}
291+
return fmt.Errorf("failed to create secret: %w", err)
292+
}
293+
294+
return nil
295+
}

0 commit comments

Comments
 (0)