Skip to content

Commit 2639c34

Browse files
synhershkojosedev-union
authored andcommitted
Support hot-reloading TLS certificates
Signed-off-by: josedev-union <[email protected]>
1 parent 9b10349 commit 2639c34

File tree

8 files changed

+266
-36
lines changed

8 files changed

+266
-36
lines changed

charts/opensearch-cluster/templates/cluster.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ spec:
5656
{{- with .tls.transport.secret }}
5757
secret: {{ . | toYaml | nindent 10 }}
5858
{{- end }}
59+
{{- if .tls.transport.enableHotReload }}
60+
enableHotReload: {{ .tls.transport.enableHotReload }}
61+
{{- end }}
5962
http:
6063
{{- if .tls.http.generate }}
6164
generate: {{ .tls.http.generate }}
@@ -66,6 +69,9 @@ spec:
6669
{{- with .tls.http.caSecret }}
6770
caSecret: {{ . | toYaml | nindent 10 }}
6871
{{- end }}
72+
{{- if .tls.http.enableHotReload }}
73+
enableHotReload: {{ .tls.http.enableHotReload }}
74+
{{- end }}
6975
{{- with .config }}
7076
config: {{ . | toYaml | nindent 6 }}
7177
{{- end }}

charts/opensearch-cluster/values.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,9 @@ cluster:
326326
secret: {}
327327
# name: "secret-name"
328328

329+
# -- Enable hot reloading of TLS certificates.
330+
enableHotReload: false
331+
329332
transport:
330333
# -- DNs of certificates that should have admin access, mainly used for securityconfig updates via securityadmin.sh,
331334
# only used when existing certificates are provided
@@ -351,6 +354,8 @@ cluster:
351354
secret: {}
352355
# name: "secret-name"
353356

357+
# -- Enable hot reloading of TLS certificates.
358+
enableHotReload: false
354359

355360
# Opensearch Ingress configuration
356361
ingress:

charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3341,6 +3341,12 @@ spec:
33413341
enable:
33423342
description: Enable HTTPS for Dashboards
33433343
type: boolean
3344+
enableHotReload:
3345+
description: Enable hot reloading of TLS certificates. When
3346+
enabled, certificates are mounted as directories instead
3347+
of using subPath, allowing Kubernetes to update certificate
3348+
files when secrets are updated.
3349+
type: boolean
33443350
generate:
33453351
description: Generate certificate, if false secret must be
33463352
provided
@@ -6264,6 +6270,12 @@ spec:
62646270
type: string
62656271
type: object
62666272
x-kubernetes-map-type: atomic
6273+
enableHotReload:
6274+
description: Enable hot reloading of TLS certificates.
6275+
When enabled, certificates are mounted as directories
6276+
instead of using subPath, allowing Kubernetes to update
6277+
certificate files when secrets are updated.
6278+
type: boolean
62676279
generate:
62686280
description: If set to true the operator will generate
62696281
a CA and certificates for the cluster to use, if false
@@ -6317,6 +6329,12 @@ spec:
63176329
type: string
63186330
type: object
63196331
x-kubernetes-map-type: atomic
6332+
enableHotReload:
6333+
description: Enable hot reloading of TLS certificates.
6334+
When enabled, certificates are mounted as directories
6335+
instead of using subPath, allowing Kubernetes to update
6336+
certificate files when secrets are updated.
6337+
type: boolean
63206338
generate:
63216339
description: If set to true the operator will generate
63226340
a CA and certificates for the cluster to use, if false

go.work.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
778778
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
779779
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
780780
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
781+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
781782
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
782783
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
783784
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -858,6 +859,7 @@ gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA
858859
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
859860
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
860861
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
862+
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
861863
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
862864
helm.sh/helm/v3 v3.12.0 h1:rOq2TPVzg5jt4q5ermAZGZFxNW2uQhKjRhBneAutMEM=
863865
helm.sh/helm/v3 v3.12.0/go.mod h1:8K/469yxjUMu6BaD2EagCitkPjELUL/l2AgCO142G94=

opensearch-operator/api/v1/opensearch_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ type TlsCertificateConfig struct {
275275
Secret corev1.LocalObjectReference `json:"secret,omitempty"`
276276
// Optional, secret that contains the ca certificate as ca.crt. If this and generate=true is set the existing CA cert from that secret is used to generate the node certs. In this case must contain ca.crt and ca.key fields
277277
CaSecret corev1.LocalObjectReference `json:"caSecret,omitempty"`
278+
// Enable hot reloading of TLS certificates. When enabled, certificates are mounted as directories instead of using subPath, allowing Kubernetes to update certificate files when secrets are updated.
279+
EnableHotReload bool `json:"enableHotReload,omitempty"`
278280
}
279281

280282
// Reference to a secret

opensearch-operator/config/crd/bases/opensearch.opster.io_opensearchclusters.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3341,6 +3341,12 @@ spec:
33413341
enable:
33423342
description: Enable HTTPS for Dashboards
33433343
type: boolean
3344+
enableHotReload:
3345+
description: Enable hot reloading of TLS certificates. When
3346+
enabled, certificates are mounted as directories instead
3347+
of using subPath, allowing Kubernetes to update certificate
3348+
files when secrets are updated.
3349+
type: boolean
33443350
generate:
33453351
description: Generate certificate, if false secret must be
33463352
provided
@@ -6264,6 +6270,12 @@ spec:
62646270
type: string
62656271
type: object
62666272
x-kubernetes-map-type: atomic
6273+
enableHotReload:
6274+
description: Enable hot reloading of TLS certificates.
6275+
When enabled, certificates are mounted as directories
6276+
instead of using subPath, allowing Kubernetes to update
6277+
certificate files when secrets are updated.
6278+
type: boolean
62676279
generate:
62686280
description: If set to true the operator will generate
62696281
a CA and certificates for the cluster to use, if false
@@ -6317,6 +6329,12 @@ spec:
63176329
type: string
63186330
type: object
63196331
x-kubernetes-map-type: atomic
6332+
enableHotReload:
6333+
description: Enable hot reloading of TLS certificates.
6334+
When enabled, certificates are mounted as directories
6335+
instead of using subPath, allowing Kubernetes to update
6336+
certificate files when secrets are updated.
6337+
type: boolean
63206338
generate:
63216339
description: If set to true the operator will generate
63226340
a CA and certificates for the cluster to use, if false

opensearch-operator/pkg/reconcilers/tls.go

Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,34 @@ func (r *TLSReconciler) handleAdminCertificate() (*ctrl.Result, error) {
129129
return res, nil
130130
}
131131

132-
func (r *TLSReconciler) securityChangeVersion() bool {
133-
newVersionConstraint, err := semver.NewConstraint(">=2.0.0")
132+
func (r *TLSReconciler) checkVersionConstraint(constraint string, defaultOnError bool, errMsg string) bool {
133+
versionConstraint, err := semver.NewConstraint(constraint)
134134
if err != nil {
135135
panic(err)
136136
}
137137

138138
version, err := semver.NewVersion(r.instance.Spec.General.Version)
139139
if err != nil {
140-
r.logger.Error(err, "unable to parse version, assuming >= 2.0.0")
141-
return true
140+
r.logger.Error(err, errMsg)
141+
return defaultOnError
142142
}
143-
return newVersionConstraint.Check(version)
143+
return versionConstraint.Check(version)
144+
}
145+
146+
func (r *TLSReconciler) securityChangeVersion() bool {
147+
return r.checkVersionConstraint(
148+
">=2.0.0",
149+
true,
150+
"unable to parse version, assuming >= 2.0.0",
151+
)
152+
}
153+
154+
func (r *TLSReconciler) supportsHotReload() bool {
155+
return r.checkVersionConstraint(
156+
">=2.19.1",
157+
false,
158+
"unable to parse version for hot reload check, assuming not supported",
159+
)
144160
}
145161

146162
func (r *TLSReconciler) adminCAName() string {
@@ -488,19 +504,42 @@ func (r *TLSReconciler) handleTransportExistingCerts() error {
488504
// r.recorder.Event(r.instance, "Warning", "Security", "Notice - Not all secrets for transport provided")
489505
return err
490506
}
491-
if tlsConfig.CaSecret.Name == "" {
507+
508+
// Implement new mounting logic based on CaSecret.Name configuration
509+
switch name := tlsConfig.CaSecret.Name; name {
510+
case "":
511+
// If CaSecret.Name is empty, mount Secret.Name as a directory
492512
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
513+
case tlsConfig.Secret.Name:
514+
// If CaSecret.Name is same as Secret.Name, mount only Secret.Name as a directory
515+
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
516+
default:
517+
// If CaSecret.Name is different from Secret.Name, mount both secrets as directories
518+
// Mount Secret.Name as tls-transport/
519+
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
520+
// Mount CaSecret.Name as tls-transport-ca/
521+
mountFolder("transport", "ca", tlsConfig.CaSecret.Name, r.reconcilerContext)
522+
}
523+
524+
// Extend opensearch.yml with appropriate file paths based on mounting logic
525+
if tlsConfig.CaSecret.Name == "" || tlsConfig.CaSecret.Name == tlsConfig.Secret.Name {
526+
// Single secret mounted as directory
527+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
528+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
529+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport/%s", CaCertKey))
493530
} else {
494-
mount("transport", "ca", CaCertKey, tlsConfig.CaSecret.Name, r.reconcilerContext)
495-
mount("transport", "key", corev1.TLSPrivateKeyKey, tlsConfig.Secret.Name, r.reconcilerContext)
496-
mount("transport", "cert", corev1.TLSCertKey, tlsConfig.Secret.Name, r.reconcilerContext)
531+
// Separate secrets mounted as directories
532+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
533+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
534+
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport-ca/%s", CaCertKey))
497535
}
498-
// Extend opensearch.yml
499-
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
500-
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
501536
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.enforce_hostname_verification", "false")
537+
538+
// Enable hot reload if configured and version supports it
539+
if tlsConfig.EnableHotReload && r.supportsHotReload() {
540+
r.reconcilerContext.AddConfig("plugins.security.ssl.certificates_hot_reload.enabled", "true")
541+
}
502542
}
503-
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport/%s", CaCertKey))
504543
dnList := strings.Join(tlsConfig.NodesDn, "\",\"")
505544
r.reconcilerContext.AddConfig("plugins.security.nodes_dn", fmt.Sprintf("[\"%s\"]", dnList))
506545
return nil
@@ -592,19 +631,43 @@ func (r *TLSReconciler) handleHttp() error {
592631
// r.recorder.Event(r.instance, "Warning", "Security", "Notice - Not all secrets for http provided")
593632
return err
594633
}
595-
if tlsConfig.CaSecret.Name == "" {
634+
635+
// Implement new mounting logic based on CaSecret.Name configuration
636+
switch name := tlsConfig.CaSecret.Name; name {
637+
case "":
638+
// If CaSecret.Name is empty, mount Secret.Name as a directory
596639
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
597-
} else {
598-
mount("http", "ca", CaCertKey, tlsConfig.CaSecret.Name, r.reconcilerContext)
599-
mount("http", "key", corev1.TLSPrivateKeyKey, tlsConfig.Secret.Name, r.reconcilerContext)
600-
mount("http", "cert", corev1.TLSCertKey, tlsConfig.Secret.Name, r.reconcilerContext)
640+
case tlsConfig.Secret.Name:
641+
// If CaSecret.Name is same as Secret.Name, mount only Secret.Name as a directory
642+
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
643+
default:
644+
// If CaSecret.Name is different from Secret.Name, mount both secrets as directories
645+
// Mount Secret.Name as tls-http/
646+
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
647+
// Mount CaSecret.Name as tls-http-ca/
648+
mountFolder("http", "ca", tlsConfig.CaSecret.Name, r.reconcilerContext)
601649
}
602650
}
603-
// Extend opensearch.yml
651+
// Extend opensearch.yml with appropriate file paths based on mounting logic
604652
r.reconcilerContext.AddConfig("plugins.security.ssl.http.enabled", "true")
605-
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
606-
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
607-
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http/%s", CaCertKey))
653+
654+
// Set certificate file paths based on mounting configuration
655+
if tlsConfig.CaSecret.Name == "" || tlsConfig.CaSecret.Name == tlsConfig.Secret.Name {
656+
// Single secret mounted as directory
657+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
658+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
659+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http/%s", CaCertKey))
660+
} else {
661+
// Separate secrets mounted as directories
662+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
663+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
664+
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http-ca/%s", CaCertKey))
665+
}
666+
667+
// Enable hot reload if configured and version supports it
668+
if tlsConfig.EnableHotReload && r.supportsHotReload() {
669+
r.reconcilerContext.AddConfig("plugins.security.ssl.certificates_hot_reload.enabled", "true")
670+
}
608671
return nil
609672
}
610673

@@ -625,17 +688,18 @@ func (r *TLSReconciler) providedCaCert(secretName string, namespace string) (tls
625688
return ca, nil
626689
}
627690

628-
func mount(interfaceName string, name string, filename string, secretName string, reconcilerContext *ReconcilerContext) {
629-
volume := corev1.Volume{Name: interfaceName + "-" + name, VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: secretName}}}
630-
reconcilerContext.Volumes = append(reconcilerContext.Volumes, volume)
631-
mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: fmt.Sprintf("/usr/share/opensearch/config/tls-%s/%s", interfaceName, filename), SubPath: filename}
632-
reconcilerContext.VolumeMounts = append(reconcilerContext.VolumeMounts, mount)
633-
}
634-
635691
func mountFolder(interfaceName string, name string, secretName string, reconcilerContext *ReconcilerContext) {
636692
volume := corev1.Volume{Name: interfaceName + "-" + name, VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: secretName}}}
637693
reconcilerContext.Volumes = append(reconcilerContext.Volumes, volume)
638-
mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: fmt.Sprintf("/usr/share/opensearch/config/tls-%s", interfaceName)}
694+
695+
var mountPath string
696+
if name == "ca" {
697+
mountPath = fmt.Sprintf("/usr/share/opensearch/config/tls-%s-ca", interfaceName)
698+
} else {
699+
mountPath = fmt.Sprintf("/usr/share/opensearch/config/tls-%s", interfaceName)
700+
}
701+
702+
mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: mountPath}
639703
reconcilerContext.VolumeMounts = append(reconcilerContext.VolumeMounts, mount)
640704
}
641705

0 commit comments

Comments
 (0)