Skip to content
Open
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: 2 additions & 0 deletions charts/opensearch-cluster/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ The following table lists the configurable parameters of the Helm chart.
| `cluster.security.tls.http.customFQDN` | string | `""` | Optional custom FQDN to use for the HTTP certificate. If provided, this FQDN will be used as the primary DNS name in the certificate's Subject Alternative Names (SAN), along with the default cluster DNS names. This allows you to use a single certificate that works with both your custom domain and the internal Kubernetes DNS names. |
| `cluster.security.tls.http.secret` | object | `{}` | Optional, name of a TLS secret that contains ca.crt, tls.key and tls.crt data. If ca.crt is in a different secret provide it via the caSecret field |
| `cluster.security.tls.http.duration` | string | `"8760h"` | Duration controls the validity period of generated certificates (e.g. "8760h", "720h"). This is used only when generate is true. |
| `cluster.security.tls.http.enableHotReload` | bool | `false` | Enable hot reloading of TLS certificates. |
| `cluster.security.tls.transport.caSecret` | object | `{}` | 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 |
| `cluster.security.tls.transport.generate` | bool | `true` | If set to true the operator will generate a CA and certificates for the cluster to use, if false secrets with existing certificates must be supplied |
| `cluster.security.tls.transport.nodesDn` | list | `[]` | Allowed Certificate DNs for nodes, only used when existing certificates are provided |
| `cluster.security.tls.transport.perNode` | bool | `true` | Separate certificate per node |
| `cluster.security.tls.transport.secret` | object | `{}` | Optional, name of a TLS secret that contains ca.crt, tls.key and tls.crt data. If ca.crt is in a different secret provide it via the caSecret field |
| `cluster.security.tls.transport.duration` | string | `"8760h"` | Duration controls the validity period of generated certificates (e.g. "8760h", "720h"). This is used only when generate is true. |
| `cluster.security.tls.transport.enableHotReload` | bool | `false` | Enable hot reloading of TLS certificates. |
| `cluster.ingress.opensearch.enabled` | bool | `false` | Enable ingress for Opensearch service |
| `cluster.ingress.opensearch.annotations` | object | `{}` | Opensearch ingress annotations |
| `cluster.ingress.opensearch.className` | string | `""` | Opensearch Ingress class name |
Expand Down
10 changes: 8 additions & 2 deletions charts/opensearch-cluster/templates/cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ spec:
secret: {{ . | toYaml | nindent 10 }}
{{- end }}
{{- if .tls.transport.duration }}
duration: {{ .tls.transport.duration | nindent 10 }}
duration: {{ .tls.transport.duration }}
{{- end }}
{{- if .tls.transport.enableHotReload }}
enableHotReload: {{ .tls.transport.enableHotReload }}
{{- end }}
http:
{{- with .tls.http.adminDn }}
Expand All @@ -70,7 +73,10 @@ spec:
caSecret: {{ . | toYaml | nindent 10 }}
{{- end }}
{{- if .tls.http.duration }}
duration: {{ .tls.http.duration | nindent 10 }}
duration: {{ .tls.http.duration }}
{{- end }}
{{- if .tls.http.enableHotReload }}
enableHotReload: {{ .tls.http.enableHotReload }}
{{- end }}
{{- with .config }}
config: {{ . | toYaml | nindent 6 }}
Expand Down
6 changes: 6 additions & 0 deletions charts/opensearch-cluster/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ cluster:
# This is used only when generate is true.
duration: "8760h"

# -- Enable hot reloading of TLS certificates.
enableHotReload: false

transport:
# -- 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
Expand All @@ -377,6 +380,9 @@ cluster:
# This is used only when generate is true.
duration: "8760h"

# -- Enable hot reloading of TLS certificates.
enableHotReload: false

# Opensearch Ingress configuration
ingress:
opensearch:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3346,6 +3346,12 @@ spec:
enable:
description: Enable HTTPS for Dashboards
type: boolean
enableHotReload:
description: 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.
type: boolean
generate:
description: Generate certificate, if false secret must be
provided
Expand Down Expand Up @@ -6293,6 +6299,12 @@ spec:
description: Duration controls the validity period of
generated certificates (e.g. "8760h", "720h").
type: string
enableHotReload:
description: 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.
type: boolean
generate:
description: If set to true the operator will generate
a CA and certificates for the cluster to use, if false
Expand Down Expand Up @@ -6344,6 +6356,12 @@ spec:
description: Duration controls the validity period of
generated certificates (e.g. "8760h", "720h").
type: string
enableHotReload:
description: 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.
type: boolean
generate:
description: If set to true the operator will generate
a CA and certificates for the cluster to use, if false
Expand Down
2 changes: 2 additions & 0 deletions opensearch-operator/api/v1/opensearch_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ type TlsCertificateConfig struct {
// Duration controls the validity period of generated certificates (e.g. "8760h", "720h").
//+kubebuilder:default:="8760h"
Duration *metav1.Duration `json:"duration,omitempty"`
// 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.
EnableHotReload bool `json:"enableHotReload,omitempty"`
}

// Reference to a secret
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3346,6 +3346,12 @@ spec:
enable:
description: Enable HTTPS for Dashboards
type: boolean
enableHotReload:
description: 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.
type: boolean
generate:
description: Generate certificate, if false secret must be
provided
Expand Down Expand Up @@ -6303,6 +6309,12 @@ spec:
description: Duration controls the validity period of
generated certificates (e.g. "8760h", "720h").
type: string
enableHotReload:
description: 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.
type: boolean
generate:
description: If set to true the operator will generate
a CA and certificates for the cluster to use, if false
Expand Down Expand Up @@ -6354,6 +6366,12 @@ spec:
description: Duration controls the validity period of
generated certificates (e.g. "8760h", "720h").
type: string
enableHotReload:
description: 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.
type: boolean
generate:
description: If set to true the operator will generate
a CA and certificates for the cluster to use, if false
Expand Down
124 changes: 94 additions & 30 deletions opensearch-operator/pkg/reconcilers/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,34 @@ func (r *TLSReconciler) handleAdminCertificate() (*ctrl.Result, error) {
return res, nil
}

func (r *TLSReconciler) securityChangeVersion() bool {
newVersionConstraint, err := semver.NewConstraint(">=2.0.0")
func (r *TLSReconciler) checkVersionConstraint(constraint string, defaultOnError bool, errMsg string) bool {
versionConstraint, err := semver.NewConstraint(constraint)
if err != nil {
panic(err)
}

version, err := semver.NewVersion(r.instance.Spec.General.Version)
if err != nil {
r.logger.Error(err, "unable to parse version, assuming >= 2.0.0")
return true
r.logger.Error(err, errMsg)
return defaultOnError
}
return newVersionConstraint.Check(version)
return versionConstraint.Check(version)
}

func (r *TLSReconciler) securityChangeVersion() bool {
return r.checkVersionConstraint(
">=2.0.0",
true,
"unable to parse version, assuming >= 2.0.0",
)
}

func (r *TLSReconciler) supportsHotReload() bool {
return r.checkVersionConstraint(
">=2.19.1",
false,
"unable to parse version for hot reload check, assuming not supported",
)
}

func (r *TLSReconciler) adminCAName() string {
Expand Down Expand Up @@ -494,19 +510,42 @@ func (r *TLSReconciler) handleTransportExistingCerts() error {
// r.recorder.Event(r.instance, "Warning", "Security", "Notice - Not all secrets for transport provided")
return err
}
if tlsConfig.CaSecret.Name == "" {

// Implement new mounting logic based on CaSecret.Name configuration
switch name := tlsConfig.CaSecret.Name; name {
case "":
// If CaSecret.Name is empty, mount Secret.Name as a directory
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
case tlsConfig.Secret.Name:
// If CaSecret.Name is same as Secret.Name, mount only Secret.Name as a directory
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
default:
// If CaSecret.Name is different from Secret.Name, mount both secrets as directories
// Mount Secret.Name as tls-transport/
mountFolder("transport", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
// Mount CaSecret.Name as tls-transport-ca/
mountFolder("transport", "ca", tlsConfig.CaSecret.Name, r.reconcilerContext)
}

// Extend opensearch.yml with appropriate file paths based on mounting logic
if tlsConfig.CaSecret.Name == "" || tlsConfig.CaSecret.Name == tlsConfig.Secret.Name {
// Single secret mounted as directory
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport/%s", CaCertKey))
} else {
mount("transport", "ca", CaCertKey, tlsConfig.CaSecret.Name, r.reconcilerContext)
mount("transport", "key", corev1.TLSPrivateKeyKey, tlsConfig.Secret.Name, r.reconcilerContext)
mount("transport", "cert", corev1.TLSCertKey, tlsConfig.Secret.Name, r.reconcilerContext)
// Separate secrets mounted as directories
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport-ca/%s", CaCertKey))
}
// Extend opensearch.yml
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemcert_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemkey_filepath", fmt.Sprintf("tls-transport/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.enforce_hostname_verification", "false")

// Enable hot reload if configured and version supports it
if tlsConfig.EnableHotReload && r.supportsHotReload() {
r.reconcilerContext.AddConfig("plugins.security.ssl.certificates_hot_reload.enabled", "true")
}
}
r.reconcilerContext.AddConfig("plugins.security.ssl.transport.pemtrustedcas_filepath", fmt.Sprintf("tls-transport/%s", CaCertKey))
dnList := strings.Join(tlsConfig.NodesDn, "\",\"")
r.reconcilerContext.AddConfig("plugins.security.nodes_dn", fmt.Sprintf("[\"%s\"]", dnList))
return nil
Expand Down Expand Up @@ -605,19 +644,43 @@ func (r *TLSReconciler) handleHttp() error {
// r.recorder.Event(r.instance, "Warning", "Security", "Notice - Not all secrets for http provided")
return err
}
if tlsConfig.CaSecret.Name == "" {

// Implement new mounting logic based on CaSecret.Name configuration
switch name := tlsConfig.CaSecret.Name; name {
case "":
// If CaSecret.Name is empty, mount Secret.Name as a directory
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
} else {
mount("http", "ca", CaCertKey, tlsConfig.CaSecret.Name, r.reconcilerContext)
mount("http", "key", corev1.TLSPrivateKeyKey, tlsConfig.Secret.Name, r.reconcilerContext)
mount("http", "cert", corev1.TLSCertKey, tlsConfig.Secret.Name, r.reconcilerContext)
case tlsConfig.Secret.Name:
// If CaSecret.Name is same as Secret.Name, mount only Secret.Name as a directory
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
default:
// If CaSecret.Name is different from Secret.Name, mount both secrets as directories
// Mount Secret.Name as tls-http/
mountFolder("http", "certs", tlsConfig.Secret.Name, r.reconcilerContext)
// Mount CaSecret.Name as tls-http-ca/
mountFolder("http", "ca", tlsConfig.CaSecret.Name, r.reconcilerContext)
}
}
// Extend opensearch.yml
// Extend opensearch.yml with appropriate file paths based on mounting logic
r.reconcilerContext.AddConfig("plugins.security.ssl.http.enabled", "true")
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http/%s", CaCertKey))

// Set certificate file paths based on mounting configuration
if tlsConfig.CaSecret.Name == "" || tlsConfig.CaSecret.Name == tlsConfig.Secret.Name {
// Single secret mounted as directory
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http/%s", CaCertKey))
} else {
// Separate secrets mounted as directories
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemcert_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSCertKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemkey_filepath", fmt.Sprintf("tls-http/%s", corev1.TLSPrivateKeyKey))
r.reconcilerContext.AddConfig("plugins.security.ssl.http.pemtrustedcas_filepath", fmt.Sprintf("tls-http-ca/%s", CaCertKey))
}

// Enable hot reload if configured and version supports it
if tlsConfig.EnableHotReload && r.supportsHotReload() {
r.reconcilerContext.AddConfig("plugins.security.ssl.certificates_hot_reload.enabled", "true")
}
return nil
}

Expand All @@ -638,17 +701,18 @@ func (r *TLSReconciler) providedCaCert(secretName string, namespace string) (tls
return ca, nil
}

func mount(interfaceName string, name string, filename string, secretName string, reconcilerContext *ReconcilerContext) {
volume := corev1.Volume{Name: interfaceName + "-" + name, VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: secretName}}}
reconcilerContext.Volumes = append(reconcilerContext.Volumes, volume)
mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: fmt.Sprintf("/usr/share/opensearch/config/tls-%s/%s", interfaceName, filename), SubPath: filename}
reconcilerContext.VolumeMounts = append(reconcilerContext.VolumeMounts, mount)
}

func mountFolder(interfaceName string, name string, secretName string, reconcilerContext *ReconcilerContext) {
volume := corev1.Volume{Name: interfaceName + "-" + name, VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: secretName}}}
reconcilerContext.Volumes = append(reconcilerContext.Volumes, volume)
mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: fmt.Sprintf("/usr/share/opensearch/config/tls-%s", interfaceName)}

var mountPath string
if name == "ca" {
mountPath = fmt.Sprintf("/usr/share/opensearch/config/tls-%s-ca", interfaceName)
} else {
mountPath = fmt.Sprintf("/usr/share/opensearch/config/tls-%s", interfaceName)
}

mount := corev1.VolumeMount{Name: interfaceName + "-" + name, MountPath: mountPath}
reconcilerContext.VolumeMounts = append(reconcilerContext.VolumeMounts, mount)
}

Expand Down
Loading
Loading