Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement mounting user certificates and keys #125

Merged
merged 13 commits into from
Apr 11, 2024
31 changes: 31 additions & 0 deletions api/v1alpha1/etcdcluster_types.go
Original file line number Diff line number Diff line change
@@ -41,6 +41,9 @@ type EtcdClusterSpec struct {
// +optional
PodDisruptionBudgetTemplate *EmbeddedPodDisruptionBudget `json:"podDisruptionBudgetTemplate,omitempty"`
Storage StorageSpec `json:"storage"`
// Security describes security settings of etcd (authentication, certificates, rbac)
// +optional
Security *SecuritySpec `json:"security,omitempty"`
Kirill-Garbar marked this conversation as resolved.
Show resolved Hide resolved
}

const (
@@ -200,6 +203,34 @@ type StorageSpec struct {
VolumeClaimTemplate EmbeddedPersistentVolumeClaim `json:"volumeClaimTemplate,omitempty"`
}

// SecuritySpec defines security settings for etcd.
// +k8s:openapi-gen=true
type SecuritySpec struct {
// Section for user-managed tls certificates
// +optional
TLS TLSSpec `json:"tls,omitempty"`
}

// TLSSpec defines user-managed certificates names.
type TLSSpec struct {
// Trusted CA certificate secret to secure peer-to-peer communication between etcd nodes. It is expected to have tls.crt field in the secret.
// +optional
PeerTrustedCASecret string `json:"peerTrustedCASecret,omitempty"`
// Certificate secret to secure peer-to-peer communication between etcd nodes. It is expected to have tls.crt and tls.key fields in the secret.
// +optional
PeerSecret string `json:"peerSecret,omitempty"`
// Server certificate secret to secure client-server communication. Is provided to the client who connects to etcd by client port (2379 by default).
// It is expected to have tls.crt and tls.key fields in the secret.
// +optional
ServerSecret string `json:"serverSecret,omitempty"`
// Trusted CA for client certificates that are provided by client to etcd. It is expected to have tls.crt field in the secret.
// +optional
ClientTrustedCASecret string `json:"clientTrustedCASecret,omitempty"`
// Client certificate for etcd-operator to do maintenance. It is expected to have tls.crt and tls.key fields in the secret.
// +optional
ClientSecret string `json:"clientSecret,omitempty"`
}

// EmbeddedPersistentVolumeClaim is an embedded version of k8s.io/api/core/v1.PersistentVolumeClaim.
// It contains TypeMeta and a reduced ObjectMeta.
type EmbeddedPersistentVolumeClaim struct {
47 changes: 47 additions & 0 deletions api/v1alpha1/etcdcluster_webhook.go
Original file line number Diff line number Diff line change
@@ -95,6 +95,11 @@ func (r *EtcdCluster) ValidateCreate() (admission.Warnings, error) {
allErrors = append(allErrors, pdbErr...)
}

securityErr := r.validateSecurity()
if securityErr != nil {
allErrors = append(allErrors, securityErr...)
}

if errOptions := validateOptions(r); errOptions != nil {
allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "options"),
@@ -139,6 +144,11 @@ func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, er
warnings = append(warnings, pdbWarnings...)
}

securityErr := r.validateSecurity()
if securityErr != nil {
allErrors = append(allErrors, securityErr...)
}

if errOptions := validateOptions(r); errOptions != nil {
allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "options"),
@@ -256,6 +266,43 @@ func (r *EtcdCluster) validatePdb() (admission.Warnings, field.ErrorList) {
return warnings, nil
}

func (r *EtcdCluster) validateSecurity() field.ErrorList {

var allErrors field.ErrorList

if r.Spec.Security == nil {
return nil
}

security := r.Spec.Security

if (security.TLS.PeerSecret != "" && security.TLS.PeerTrustedCASecret == "") ||
(security.TLS.PeerSecret == "" && security.TLS.PeerTrustedCASecret != "") {

allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "security", "tls"),
security.TLS,
"both spec.security.tls.peerSecret and spec.security.tls.peerTrustedCASecret must be filled or empty"),
)
}

if (security.TLS.ClientSecret != "" && security.TLS.ClientTrustedCASecret == "") ||
(security.TLS.ClientSecret == "" && security.TLS.ClientTrustedCASecret != "") {

allErrors = append(allErrors, field.Invalid(
field.NewPath("spec", "security", "tls"),
security.TLS,
"both spec.security.tls.clientSecret and spec.security.tls.clientTrustedCASecret must be filled or empty"),
)
}

if len(allErrors) > 0 {
return allErrors
}

return nil
}

func validateOptions(cluster *EtcdCluster) error {
if len(cluster.Spec.Options) == 0 {
return nil
86 changes: 86 additions & 0 deletions api/v1alpha1/etcdcluster_webhook_test.go
Original file line number Diff line number Diff line change
@@ -130,6 +130,92 @@ var _ = Describe("EtcdCluster Webhook", func() {
})
})

Context("Validate Security", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
Replicas: ptr.To(int32(3)),
Security: &SecuritySpec{},
},
}
It("Should admit enabled empty security", func() {
localCluster := etcdCluster.DeepCopy()
err := localCluster.validateSecurity()
Expect(err).To(BeNil())
})

It("Should reject if only one peer secret is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.TLS = TLSSpec{
PeerTrustedCASecret: "test-peer-ca-cert",
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "tls"),
localCluster.Spec.Security.TLS,
"both spec.security.tls.peerSecret and spec.security.tls.peerTrustedCASecret must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one peer secret is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.TLS = TLSSpec{
PeerSecret: "test-peer-cert",
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "tls"),
localCluster.Spec.Security.TLS,
"both spec.security.tls.peerSecret and spec.security.tls.peerTrustedCASecret must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one client secret is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.TLS = TLSSpec{
ClientTrustedCASecret: "test-client-ca-cert",
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "tls"),
localCluster.Spec.Security.TLS,
"both spec.security.tls.clientSecret and spec.security.tls.clientTrustedCASecret must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})

It("Should reject if only one client secret is defined", func() {
localCluster := etcdCluster.DeepCopy()
localCluster.Spec.Security.TLS = TLSSpec{
ClientTrustedCASecret: "test-client-cert",
}
err := localCluster.validateSecurity()
if Expect(err).NotTo(BeNil()) {
expectedFieldErr := field.Invalid(
field.NewPath("spec", "security", "tls"),
localCluster.Spec.Security.TLS,
"both spec.security.tls.clientSecret and spec.security.tls.clientTrustedCASecret must be filled or empty",
)
if Expect(err).To(HaveLen(1)) {
Expect(*(err[0])).To(Equal(*expectedFieldErr))
}
}
})
})

Context("Validate PDB", func() {
etcdCluster := &EtcdCluster{
Spec: EtcdClusterSpec{
36 changes: 36 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions charts/etcd-operator/crds/etcd-cluster.yaml
Original file line number Diff line number Diff line change
@@ -4318,6 +4318,31 @@ spec:
format: int32
minimum: 0
type: integer
security:
description: Security describes security settings of etcd (authentication, certificates, rbac)
properties:
tls:
description: Section for user-managed tls certificates
properties:
clientSecret:
description: Client certificate for etcd-operator to do maintenance. It is expected to have tls.crt and tls.key fields in the secret.
type: string
clientTrustedCASecret:
description: Trusted CA for client certificates that are provided by client to etcd. It is expected to have tls.crt field in the secret.
type: string
peerSecret:
description: Certificate secret to secure peer-to-peer communication between etcd nodes. It is expected to have tls.crt and tls.key fields in the secret.
type: string
peerTrustedCASecret:
description: Trusted CA certificate secret to secure peer-to-peer communication between etcd nodes. It is expected to have tls.crt field in the secret.
type: string
serverSecret:
description: |-
Server certificate secret to secure client-server communication. Is provided to the client who connects to etcd by client port (2379 by default).
It is expected to have tls.crt and tls.key fields in the secret.
type: string
type: object
type: object
storage:
description: |-
StorageSpec defines the configured storage for a etcd members.
34 changes: 34 additions & 0 deletions config/crd/bases/etcd.aenix.io_etcdclusters.yaml
Original file line number Diff line number Diff line change
@@ -4571,6 +4571,40 @@ spec:
format: int32
minimum: 0
type: integer
security:
description: Security describes security settings of etcd (authentication,
certificates, rbac)
properties:
tls:
description: Section for user-managed tls certificates
properties:
clientSecret:
description: Client certificate for etcd-operator to do maintenance.
It is expected to have tls.crt and tls.key fields in the
secret.
type: string
clientTrustedCASecret:
description: Trusted CA for client certificates that are provided
by client to etcd. It is expected to have tls.crt field
in the secret.
type: string
peerSecret:
description: Certificate secret to secure peer-to-peer communication
between etcd nodes. It is expected to have tls.crt and tls.key
fields in the secret.
type: string
peerTrustedCASecret:
description: Trusted CA certificate secret to secure peer-to-peer
communication between etcd nodes. It is expected to have
tls.crt field in the secret.
type: string
serverSecret:
description: |-
Server certificate secret to secure client-server communication. Is provided to the client who connects to etcd by client port (2379 by default).
It is expected to have tls.crt and tls.key fields in the secret.
type: string
type: object
type: object
storage:
description: |-
StorageSpec defines the configured storage for a etcd members.
5 changes: 4 additions & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -2,4 +2,7 @@ resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images: []
images:
- name: ghcr.io/aenix-io/etcd-operator
newName: ghcr.io/aenix-io/etcd-operator
newTag: latest
Loading