From e9a33e98dc3424c7b2cdb3eddcc05276904bedc5 Mon Sep 17 00:00:00 2001 From: "shenmu.wy" Date: Thu, 16 Jul 2026 15:02:39 +0800 Subject: [PATCH] chore(tls): cleanup sample yaml + auto cert for client auth Signed-off-by: shenmu.wy --- .../samples/sample_autocert_etcdcluster.yaml | 2 +- .../sample_certmanager_etcdcluster.yaml | 2 +- pkg/certificate/auto/provider.go | 6 +- pkg/certificate/auto/provider_test.go | 66 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 pkg/certificate/auto/provider_test.go diff --git a/config/samples/sample_autocert_etcdcluster.yaml b/config/samples/sample_autocert_etcdcluster.yaml index 9d7bc126..b360b72f 100644 --- a/config/samples/sample_autocert_etcdcluster.yaml +++ b/config/samples/sample_autocert_etcdcluster.yaml @@ -13,4 +13,4 @@ spec: providerCfg: autoCfg: commonName: "etcd-operator-system" - validityDuration: 365d + validityDuration: 8760h \ No newline at end of file diff --git a/config/samples/sample_certmanager_etcdcluster.yaml b/config/samples/sample_certmanager_etcdcluster.yaml index b515db39..5247b686 100644 --- a/config/samples/sample_certmanager_etcdcluster.yaml +++ b/config/samples/sample_certmanager_etcdcluster.yaml @@ -22,6 +22,6 @@ spec: providerCfg: certManagerCfg: commonName: "etcd-operator-system" - validityDuration: "365d" + validityDuration: "8760h" issuerKind: "ClusterIssuer" issuerName: "selfsigned" diff --git a/pkg/certificate/auto/provider.go b/pkg/certificate/auto/provider.go index cf7c7526..1441b668 100644 --- a/pkg/certificate/auto/provider.go +++ b/pkg/certificate/auto/provider.go @@ -294,7 +294,11 @@ func (ac *Provider) createNewSecret(ctx context.Context, secretKey client.Object return fmt.Errorf("validity duration converts to 0 years, must be at least 1 year") } - tlsInfo, selfCertErr := transport.SelfCert(zap.NewNop(), tmpDir, hosts, validityYears) + // transport.SelfCert generates a self-signed cert whose ExtKeyUsage + // defaults to ServerAuth only. The operator reuses the server Secret + // as its own mTLS client identity talking with the etcd cluster. + // The cert must also carry ClientAuth. + tlsInfo, selfCertErr := transport.SelfCert(zap.NewNop(), tmpDir, hosts, validityYears, x509.ExtKeyUsageClientAuth) if selfCertErr != nil { return fmt.Errorf("certificate creation via transport.SelfCert failed: %w", selfCertErr) } diff --git a/pkg/certificate/auto/provider_test.go b/pkg/certificate/auto/provider_test.go new file mode 100644 index 00000000..f8766ac9 --- /dev/null +++ b/pkg/certificate/auto/provider_test.go @@ -0,0 +1,66 @@ +package auto + +import ( + "context" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + interfaces "go.etcd.io/etcd-operator/pkg/certificate/interfaces" +) + +// The operator uses the server certificate as a client to check etcd member +// status, so the auto-generated cert must carry the ClientAuth key usage. +func TestEnsureCertificateSecretCertHasClientAuth(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + + fakeClient := clientfake.NewClientBuilder().WithScheme(scheme).Build() + provider := New(fakeClient) + + secretKey := client.ObjectKey{Name: "test-server-tls", Namespace: "default"} + cfg := &interfaces.Config{ + CommonName: "etcd.test", + ValidityDuration: interfaces.DefaultAutoValidity, + AltNames: interfaces.AltNames{ + DNSNames: []string{"test.default.svc.cluster.local"}, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + require.NoError(t, provider.EnsureCertificateSecret(ctx, secretKey, cfg)) + + // Fetch the generated Secret and verify the cert carries ClientAuth. + secret := &corev1.Secret{} + require.NoError(t, fakeClient.Get(ctx, secretKey, secret)) + + certPEM, ok := secret.Data["tls.crt"] + require.True(t, ok, "secret must contain tls.crt") + require.NotEmpty(t, certPEM) + + block, _ := pem.Decode(certPEM) + require.NotNil(t, block, "tls.crt is valid PEM") + + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "tls.crt parses as a certificate") + + assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth, + "auto cert must carry ClientAuth so the operator can present it as a client (design D4-a)") + assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth, + "auto cert must keep ServerAuth for its primary server role") + + // Sanity: the secret is structurally complete for an etcd TLS mount. + assert.NotEmpty(t, secret.Data["tls.key"]) + assert.NotEmpty(t, secret.Data["ca.crt"]) + assert.Equal(t, corev1.SecretTypeTLS, secret.Type) +}