Skip to content

Commit 6707794

Browse files
committed
fix(main): add support peer-auto-tls
1 parent d7dcf6a commit 6707794

11 files changed

Lines changed: 241 additions & 14 deletions

api/v1alpha2/cel_validation_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,37 @@ func TestCEL_TLSPeerCertManagerAndSecretRefMutuallyExclusive(t *testing.T) {
477477
_ = k8s.Delete(ctx, c)
478478
t.Fatalf("apiserver accepted both peer.secretRef and peer.certManager; expected rejection")
479479
}
480-
if !strings.Contains(err.Error(), "exactly one of spec.tls.peer.secretRef or spec.tls.peer.certManager") {
480+
if !strings.Contains(err.Error(), "exactly one of spec.tls.peer.secretRef") {
481+
t.Fatalf("error did not mention peer mutual exclusion: %v", err)
482+
}
483+
}
484+
485+
// TestCEL_TLSPeerAutoTLS: autoTLS alone is accepted; autoTLS combined with
486+
// another peer source is rejected by the exactly-one-of rule.
487+
func TestCEL_TLSPeerAutoTLS(t *testing.T) {
488+
skipIfNoEnvtest(t)
489+
ctx := context.Background()
490+
491+
// autoTLS alone — accepted.
492+
ok := validCluster("tls-peer-autotls")
493+
ok.Spec.TLS = &lll.EtcdClusterTLS{Peer: &lll.PeerTLS{AutoTLS: &lll.PeerAutoTLS{}}}
494+
if err := k8s.Create(ctx, ok); err != nil {
495+
t.Fatalf("apiserver rejected peer.autoTLS alone: %v", err)
496+
}
497+
_ = k8s.Delete(ctx, ok)
498+
499+
// autoTLS + secretRef — rejected.
500+
bad := validCluster("tls-peer-autotls-both")
501+
bad.Spec.TLS = &lll.EtcdClusterTLS{Peer: &lll.PeerTLS{
502+
AutoTLS: &lll.PeerAutoTLS{},
503+
SecretRef: &corev1.LocalObjectReference{Name: "fake-peer-tls"},
504+
}}
505+
err := k8s.Create(ctx, bad)
506+
if err == nil {
507+
_ = k8s.Delete(ctx, bad)
508+
t.Fatalf("apiserver accepted peer.autoTLS + peer.secretRef; expected rejection")
509+
}
510+
if !strings.Contains(err.Error(), "exactly one of spec.tls.peer.secretRef") {
481511
t.Fatalf("error did not mention peer mutual exclusion: %v", err)
482512
}
483513
}

api/v1alpha2/etcdcluster_types.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ type ClientCertManagerTLS struct {
194194
// PeerTLS configures TLS for the etcd peer API. When PeerTLS is set, peer
195195
// is always mTLS.
196196
//
197-
// +kubebuilder:validation:XValidation:rule="has(self.secretRef) != has(self.certManager)",message="exactly one of spec.tls.peer.secretRef or spec.tls.peer.certManager must be set"
197+
// +kubebuilder:validation:XValidation:rule="[has(self.secretRef), has(self.certManager), has(self.autoTLS)].filter(x, x).size() == 1",message="exactly one of spec.tls.peer.secretRef, spec.tls.peer.certManager or spec.tls.peer.autoTLS must be set"
198198
type PeerTLS struct {
199199
// SecretRef points at a Secret in the cluster's namespace holding
200200
// the peer cert+key in the standard kubernetes.io/tls shape:
@@ -203,17 +203,35 @@ type PeerTLS struct {
203203
// connections — and --peer-trusted-ca-file is always populated).
204204
// The peer cert MUST carry both serverAuth and clientAuth in EKU.
205205
//
206-
// Mutually exclusive with CertManager.
206+
// Mutually exclusive with CertManager and AutoTLS.
207207
// +optional
208208
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
209209

210210
// CertManager configures operator-driven TLS material provisioning
211211
// for the peer plane via cert-manager.io/v1 Certificate resources.
212-
// Mutually exclusive with SecretRef.
212+
// Mutually exclusive with SecretRef and AutoTLS.
213213
// +optional
214214
CertManager *PeerCertManagerTLS `json:"certManager,omitempty"`
215+
216+
// AutoTLS runs the peer API with etcd's --peer-auto-tls: each member
217+
// generates its own self-signed peer certificate and there is NO shared
218+
// CA, so peer traffic is encrypted but NOT authenticated — any workload
219+
// that can reach :2380 and speak TLS can join or impersonate a peer.
220+
// This is INSECURE and exists only to adopt legacy clusters that ran the
221+
// previous operator's unconditional --peer-auto-tls default in place:
222+
// etcd-migrate sets it so replacement/scaled members interoperate with
223+
// the still-auto-tls adopted members without a CA rotation. Move to
224+
// SecretRef or CertManager (real mTLS) when you can — that is a
225+
// delete-and-recreate since spec.tls is immutable. Mutually exclusive
226+
// with SecretRef and CertManager.
227+
// +optional
228+
AutoTLS *PeerAutoTLS `json:"autoTLS,omitempty"`
215229
}
216230

231+
// PeerAutoTLS enables etcd's --peer-auto-tls for the peer plane. Empty struct:
232+
// its presence is the signal. INSECURE — see PeerTLS.AutoTLS.
233+
type PeerAutoTLS struct{}
234+
217235
// PeerCertManagerTLS configures operator-driven TLS for the peer API.
218236
type PeerCertManagerTLS struct {
219237
// IssuerRef is the Issuer or ClusterIssuer that signs the peer cert.

api/v1alpha2/etcdmember_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ type EtcdMemberTLS struct {
4444
// mTLS (--peer-client-cert-auth=true).
4545
// +optional
4646
PeerSecretRef *corev1.LocalObjectReference `json:"peerSecretRef,omitempty"`
47+
48+
// PeerAutoTLS mirrors "EtcdClusterTLS.Peer.AutoTLS is set": run the peer
49+
// API with etcd's --peer-auto-tls (self-signed, no shared CA) instead of
50+
// mounting a peer secret. INSECURE — peer is encrypted but NOT
51+
// authenticated. Mutually exclusive with PeerSecretRef.
52+
// +optional
53+
PeerAutoTLS bool `json:"peerAutoTLS,omitempty"`
4754
}
4855

4956
// Condition types for EtcdMember.

api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,11 +1515,25 @@ spec:
15151515
is symmetric and there is no useful encrypt-only-no-identity mode
15161516
for it.
15171517
properties:
1518+
autoTLS:
1519+
description: |-
1520+
AutoTLS runs the peer API with etcd's --peer-auto-tls: each member
1521+
generates its own self-signed peer certificate and there is NO shared
1522+
CA, so peer traffic is encrypted but NOT authenticated — any workload
1523+
that can reach :2380 and speak TLS can join or impersonate a peer.
1524+
This is INSECURE and exists only to adopt legacy clusters that ran the
1525+
previous operator's unconditional --peer-auto-tls default in place:
1526+
etcd-migrate sets it so replacement/scaled members interoperate with
1527+
the still-auto-tls adopted members without a CA rotation. Move to
1528+
SecretRef or CertManager (real mTLS) when you can — that is a
1529+
delete-and-recreate since spec.tls is immutable. Mutually exclusive
1530+
with SecretRef and CertManager.
1531+
type: object
15181532
certManager:
15191533
description: |-
15201534
CertManager configures operator-driven TLS material provisioning
15211535
for the peer plane via cert-manager.io/v1 Certificate resources.
1522-
Mutually exclusive with SecretRef.
1536+
Mutually exclusive with SecretRef and AutoTLS.
15231537
properties:
15241538
issuerRef:
15251539
description: |-
@@ -1556,7 +1570,7 @@ spec:
15561570
connections — and --peer-trusted-ca-file is always populated).
15571571
The peer cert MUST carry both serverAuth and clientAuth in EKU.
15581572
1559-
Mutually exclusive with CertManager.
1573+
Mutually exclusive with CertManager and AutoTLS.
15601574
properties:
15611575
name:
15621576
default: ""
@@ -1571,9 +1585,10 @@ spec:
15711585
x-kubernetes-map-type: atomic
15721586
type: object
15731587
x-kubernetes-validations:
1574-
- message: exactly one of spec.tls.peer.secretRef or spec.tls.peer.certManager
1575-
must be set
1576-
rule: has(self.secretRef) != has(self.certManager)
1588+
- message: exactly one of spec.tls.peer.secretRef, spec.tls.peer.certManager
1589+
or spec.tls.peer.autoTLS must be set
1590+
rule: '[has(self.secretRef), has(self.certManager), has(self.autoTLS)].filter(x,
1591+
x).size() == 1'
15771592
type: object
15781593
topologySpreadConstraints:
15791594
description: |-

charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdmembers.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,6 +1336,13 @@ spec:
13361336
type: string
13371337
type: object
13381338
x-kubernetes-map-type: atomic
1339+
peerAutoTLS:
1340+
description: |-
1341+
PeerAutoTLS mirrors "EtcdClusterTLS.Peer.AutoTLS is set": run the peer
1342+
API with etcd's --peer-auto-tls (self-signed, no shared CA) instead of
1343+
mounting a peer secret. INSECURE — peer is encrypted but NOT
1344+
authenticated. Mutually exclusive with PeerSecretRef.
1345+
type: boolean
13391346
peerSecretRef:
13401347
description: |-
13411348
PeerSecretRef mirrors EtcdClusterTLS.Peer.SecretRef. When nil, the

controllers/etcdmember_controller.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,15 @@ func (r *EtcdMemberReconciler) buildPod(member *lll.EtcdMember) *corev1.Pod {
712712
Name: "tls-client", MountPath: "/etc/etcd/tls/client", ReadOnly: true,
713713
})
714714
}
715-
if peerTLS {
715+
switch {
716+
case member.Spec.TLS != nil && member.Spec.TLS.PeerAutoTLS:
717+
// INSECURE legacy-compat peer mode: etcd generates a self-signed peer
718+
// cert per member with no shared CA, so peer is encrypted but NOT
719+
// authenticated and there is nothing to mount. Only reached for
720+
// clusters adopted from a --peer-auto-tls legacy cluster (see
721+
// EtcdClusterTLS.Peer.AutoTLS); etcd-migrate sets it.
722+
cmd = append(cmd, "--peer-auto-tls")
723+
case peerTLS:
716724
cmd = append(cmd,
717725
"--peer-cert-file=/etc/etcd/tls/peer/tls.crt",
718726
"--peer-key-file=/etc/etcd/tls/peer/tls.key",

controllers/etcdmember_controller_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,39 @@ func TestBuildPod_PeerTLSAlwaysMTLS(t *testing.T) {
21502150
}
21512151
}
21522152

2153+
// TestBuildPod_PeerAutoTLS: the legacy-compat insecure peer mode emits
2154+
// --peer-auto-tls on an https peer listener and mounts NO peer secret (etcd
2155+
// self-signs; there is no shared CA and no client-cert-auth).
2156+
func TestBuildPod_PeerAutoTLS(t *testing.T) {
2157+
r := &EtcdMemberReconciler{}
2158+
pod := r.buildPod(&lll.EtcdMember{
2159+
ObjectMeta: metav1.ObjectMeta{Name: "m", Namespace: "ns"},
2160+
Spec: lll.EtcdMemberSpec{
2161+
ClusterName: "test", Version: "3.5.17", Storage: lll.StorageSpec{Size: quickQty(t, "1Gi")},
2162+
TLS: &lll.EtcdMemberTLS{PeerAutoTLS: true},
2163+
},
2164+
})
2165+
cmd := pod.Spec.Containers[0].Command
2166+
if !cmdContains(cmd, "--listen-peer-urls=https://0.0.0.0:2380") {
2167+
t.Fatalf("peer listen URL not https: %v", cmd)
2168+
}
2169+
if !cmdContains(cmd, "--peer-auto-tls") {
2170+
t.Fatalf("expected --peer-auto-tls; got %v", cmd)
2171+
}
2172+
for _, unwanted := range []string{
2173+
"--peer-cert-file=/etc/etcd/tls/peer/tls.crt",
2174+
"--peer-trusted-ca-file=/etc/etcd/tls/peer/ca.crt",
2175+
"--peer-client-cert-auth=true",
2176+
} {
2177+
if cmdContains(cmd, unwanted) {
2178+
t.Fatalf("auto-tls must not set BYO peer flag %q: %v", unwanted, cmd)
2179+
}
2180+
}
2181+
if v := volumeFor(pod, "tls-peer"); v != nil {
2182+
t.Fatalf("auto-tls must mount no peer secret; got volume %+v", v)
2183+
}
2184+
}
2185+
21532186
// TestBuildPod_AlwaysExposesMetricsPort guards the cozystack-shaped
21542187
// monitoring contract: VMPodScrape (and equivalent Prometheus scrapers)
21552188
// target the named "metrics" container port unconditionally, and the

controllers/helpers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ func memberClientScheme(member *lll.EtcdMember) string {
155155

156156
// memberPeerScheme is the per-member counterpart to clusterPeerScheme.
157157
func memberPeerScheme(member *lll.EtcdMember) string {
158-
if member != nil && member.Spec.TLS != nil && member.Spec.TLS.PeerSecretRef != nil {
158+
if member != nil && member.Spec.TLS != nil &&
159+
(member.Spec.TLS.PeerSecretRef != nil || member.Spec.TLS.PeerAutoTLS) {
159160
return "https"
160161
}
161162
return "http"
@@ -192,6 +193,8 @@ func deriveMemberTLS(cluster *lll.EtcdCluster) *lll.EtcdMemberTLS {
192193
}
193194
if name := peerSecretName(cluster); name != "" {
194195
out.PeerSecretRef = &corev1.LocalObjectReference{Name: name}
196+
} else if cluster.Spec.TLS.Peer != nil && cluster.Spec.TLS.Peer.AutoTLS != nil {
197+
out.PeerAutoTLS = true
195198
}
196199
return out
197200
}

internal/migrate/adopt.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,33 @@ func BuildAdoption(name, namespace string, spec legacy.EtcdClusterSpec, facts Cl
185185
}
186186
}
187187

188+
// Detect the legacy operator's default --peer-auto-tls. It enables peer TLS
189+
// with self-signed, no-shared-CA certs UNCONDITIONALLY unless a BYO
190+
// peerSecret is set, so a default cluster advertises https:// peer URLs that
191+
// translateTLS — which sees only the spec — cannot represent (it leaves
192+
// spec.tls.peer nil). Carry it forward as spec.tls.peer.autoTLS so the new
193+
// operator runs replacement/scaled members with --peer-auto-tls too and they
194+
// interoperate with the still-auto-tls adopted members (no shared CA exists
195+
// to do real mTLS, and a plaintext-peer replacement could never join). This
196+
// preserves the legacy peer security posture — encrypted but NOT
197+
// authenticated — so warn; moving to real mTLS later is a delete-and-recreate.
198+
peerTLSDeclared := cluster.Spec.TLS != nil && cluster.Spec.TLS.Peer != nil
199+
if !peerTLSDeclared {
200+
for _, m := range members {
201+
if strings.HasPrefix(m.PeerURL, "https://") {
202+
if cluster.Spec.TLS == nil {
203+
cluster.Spec.TLS = &lll.EtcdClusterTLS{}
204+
}
205+
cluster.Spec.TLS.Peer = &lll.PeerTLS{AutoTLS: &lll.PeerAutoTLS{}}
206+
plan.Warnings = append(plan.Warnings, fmt.Sprintf(
207+
"cluster runs etcd --peer-auto-tls (member %q advertises %s; no peerSecret in the legacy spec): migrated as spec.tls.peer.autoTLS so members keep interoperating across replacement/scale. "+
208+
"This is INSECURE — peer traffic is encrypted but NOT authenticated (no shared CA). Move to real mTLS (spec.tls.peer.secretRef or certManager) when you can; that is a delete-and-recreate since spec.tls is immutable.",
209+
m.Name, m.PeerURL))
210+
break
211+
}
212+
}
213+
}
214+
188215
// Replicas follow the LIVE member count. A legacy spec disagreeing with
189216
// reality (mid-scale crash, manual edits) is surfaced, not silently
190217
// trusted — adopting with spec.replicas != len(members) would make the
@@ -300,9 +327,9 @@ func BuildAdoption(name, namespace string, spec legacy.EtcdClusterSpec, facts Cl
300327
}
301328

302329
// deriveAdoptedMemberTLS mirrors the controller's cluster→member TLS
303-
// projection for the BYO-secret mode (the only mode a legacy translation
304-
// produces): server secret ref + the "operator presents a client cert" bit,
305-
// peer secret ref.
330+
// projection for the modes a legacy translation produces: client server
331+
// secret ref + the "operator presents a client cert" bit, and peer either as
332+
// a secret ref (BYO) or auto-tls (legacy --peer-auto-tls carried forward).
306333
func deriveAdoptedMemberTLS(cluster *lll.EtcdCluster) *lll.EtcdMemberTLS {
307334
tls := cluster.Spec.TLS
308335
if tls == nil || (tls.Client == nil && tls.Peer == nil) {
@@ -315,6 +342,8 @@ func deriveAdoptedMemberTLS(cluster *lll.EtcdCluster) *lll.EtcdMemberTLS {
315342
}
316343
if tls.Peer != nil && tls.Peer.SecretRef != nil {
317344
out.PeerSecretRef = &corev1.LocalObjectReference{Name: tls.Peer.SecretRef.Name}
345+
} else if tls.Peer != nil && tls.Peer.AutoTLS != nil {
346+
out.PeerAutoTLS = true
318347
}
319348
return out
320349
}

0 commit comments

Comments
 (0)