Skip to content

Commit

Permalink
Implement mounting user certificates and keys (#125)
Browse files Browse the repository at this point in the history
This PR allows to define user-managed peer, client-server certificates.
It implements security specification:

```yaml
spec:
...
  security:
    tls:
      peerTrustedCASecret: ca-peer-secret
      peerSecret: peer-secret
      serverSecret: server-secret
      clientTrustedCASecret: ca-client-secret
      clientSecret: client-secret
...
```

Enabled RBAC and root certificate watching **is not** part of this PR.

---------

Co-authored-by: Kirill Garbar <[email protected]>
  • Loading branch information
Kirill-Garbar and Kirill Garbar authored Apr 11, 2024
1 parent 7c5d2d1 commit 765d551
Show file tree
Hide file tree
Showing 10 changed files with 793 additions and 71 deletions.
31 changes: 31 additions & 0 deletions api/v1alpha1/etcdcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

const (
Expand Down Expand Up @@ -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 {
Expand Down
47 changes: 47 additions & 0 deletions api/v1alpha1/etcdcluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions api/v1alpha1/etcdcluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
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
Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions config/crd/bases/etcd.aenix.io_etcdclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 765d551

Please sign in to comment.