Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/samples/sample_autocert_etcdcluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ spec:
providerCfg:
autoCfg:
commonName: "etcd-operator-system"
validityDuration: 365d
validityDuration: 8760h

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to consider an alternative utility to replace the time.ParseDuration, we should support duration like "365d"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, there gona be some research work. Do you mind that I handle it in a follow-up PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind that I handle it in a follow-up PR?

sure

2 changes: 1 addition & 1 deletion config/samples/sample_certmanager_etcdcluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ spec:
providerCfg:
certManagerCfg:
commonName: "etcd-operator-system"
validityDuration: "365d"
validityDuration: "8760h"
issuerKind: "ClusterIssuer"
issuerName: "selfsigned"
6 changes: 5 additions & 1 deletion pkg/certificate/auto/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious why we did not see any issue previously without x509.ExtKeyUsageClientAuth? cc @ArkaSaha30

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I see is that etcdserver pod has never enabled TLS args.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I see is that etcdserver pod has never enabled TLS args.

Just double checked the source code on main branch, this is true. We mounted the secret into POD, but actually do not use it at all. Peers talk to each other use http always, and etcd-operator talks to etcd via http always as well.

We missed such an elephant!

if selfCertErr != nil {
return fmt.Errorf("certificate creation via transport.SelfCert failed: %w", selfCertErr)
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/certificate/auto/provider_test.go
Original file line number Diff line number Diff line change
@@ -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)
}