From d45f8fb99da7ed6d2c5bc0e17fa5e486c30b3279 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 14 Aug 2025 18:15:08 +0200 Subject: [PATCH 01/15] wip: add run-securityadmin job --- rust/operator-binary/src/controller.rs | 9 +- rust/operator-binary/src/controller/apply.rs | 3 + rust/operator-binary/src/controller/build.rs | 8 + .../src/controller/build/job_builder.rs | 185 ++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 rust/operator-binary/src/controller/build/job_builder.rs diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 4e23072..35fb559 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -5,10 +5,13 @@ use build::build; use snafu::{ResultExt, Snafu}; use stackable_operator::{ cluster_resources::ClusterResourceApplyStrategy, - commons::{affinity::StackableAffinity, product_image_selection::ProductImage}, + commons::{ + affinity::StackableAffinity, networking::DomainName, product_image_selection::ProductImage, + }, crd::listener::v1alpha1::Listener, k8s_openapi::api::{ apps::v1::StatefulSet, + batch::v1::Job, core::v1::{ConfigMap, Service, ServiceAccount}, policy::v1::PodDisruptionBudget, rbac::v1::RoleBinding, @@ -43,6 +46,7 @@ pub struct ContextNames { pub product_name: ProductName, pub operator_name: OperatorName, pub controller_name: ControllerName, + pub cluster_domain_name: DomainName, } pub struct Context { @@ -52,6 +56,7 @@ pub struct Context { impl Context { pub fn new(client: stackable_operator::client::Client, operator_name: OperatorName) -> Self { + let cluster_domain_name = client.kubernetes_cluster_info.cluster_domain.clone(); Context { client, names: ContextNames { @@ -60,6 +65,7 @@ impl Context { operator_name, controller_name: ControllerName::from_str("opensearchcluster") .expect("should be a valid controller name"), + cluster_domain_name, }, } } @@ -282,5 +288,6 @@ struct KubernetesResources { service_accounts: Vec, role_bindings: Vec, pod_disruption_budgets: Vec, + jobs: Vec, status: PhantomData, } diff --git a/rust/operator-binary/src/controller/apply.rs b/rust/operator-binary/src/controller/apply.rs index f0bd893..69a442f 100644 --- a/rust/operator-binary/src/controller/apply.rs +++ b/rust/operator-binary/src/controller/apply.rs @@ -72,6 +72,8 @@ impl<'a> Applier<'a> { let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?; + let jobs = self.add_resources(resources.jobs).await?; + self.cluster_resources .delete_orphaned_resources(self.client) .await @@ -85,6 +87,7 @@ impl<'a> Applier<'a> { service_accounts, role_bindings, pod_disruption_budgets, + jobs, status: PhantomData, }) } diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index db8e35e..051f33d 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -2,8 +2,11 @@ use std::marker::PhantomData; use role_builder::RoleBuilder; +use crate::controller::build::job_builder::JobBuilder; + use super::{ContextNames, KubernetesResources, Prepared, ValidatedCluster}; +pub mod job_builder; pub mod node_config; pub mod role_builder; pub mod role_group_builder; @@ -13,8 +16,10 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou let mut stateful_sets = vec![]; let mut services = vec![]; let mut listeners = vec![]; + let mut jobs = vec![]; let role_builder = RoleBuilder::new(cluster.clone(), names); + let job_builder = JobBuilder::new(cluster.clone(), names); for role_group_builder in role_builder.role_group_builders() { config_maps.push(role_group_builder.build_config_map()); @@ -32,6 +37,8 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou let pod_disruption_budgets = role_builder.build_pdb().into_iter().collect(); + jobs.push(job_builder.build_run_securityadmin_job()); + KubernetesResources { stateful_sets, services, @@ -40,6 +47,7 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou service_accounts, role_bindings, pod_disruption_budgets, + jobs, status: PhantomData, } } diff --git a/rust/operator-binary/src/controller/build/job_builder.rs b/rust/operator-binary/src/controller/build/job_builder.rs new file mode 100644 index 0000000..ee2beb7 --- /dev/null +++ b/rust/operator-binary/src/controller/build/job_builder.rs @@ -0,0 +1,185 @@ +use stackable_operator::{ + builder::{ + meta::ObjectMetaBuilder, + pod::{container::ContainerBuilder, resources::ResourceRequirementsBuilder}, + }, + k8s_openapi::api::{ + batch::v1::{Job, JobSpec}, + core::v1::{ + PodSecurityContext, PodSpec, PodTemplateSpec, SecretVolumeSource, Volume, VolumeMount, + }, + }, + kube::api::ObjectMeta, + kvp::{ + Label, Labels, + consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, + }, +}; + +use crate::{ + controller::{ContextNames, ValidatedCluster}, + framework::{ + IsLabelValue, builder::meta::ownerreference_from_resource, role_utils::ResourceNames, + }, +}; + +const RUN_SECURITYADMIN_CERT_VOLUME_NAME: &str = "tls"; +const RUN_SECURITYADMIN_CERT_VOLUME_MOUNT: &str = "/stackable/cert"; +const SECURITY_CONFIG_VOLUME_NAME: &str = "security-config"; +const SECURITY_CONFIG_VOLUME_MOUNT: &str = "/stackable/opensearch/config/opensearch-security"; +const RUN_SECURITYADMIN_CONTAINER_NAME: &str = "run-securityadmin"; + +pub struct JobBuilder<'a> { + cluster: ValidatedCluster, + context_names: &'a ContextNames, + resource_names: ResourceNames, +} + +impl<'a> JobBuilder<'a> { + pub fn new(cluster: ValidatedCluster, context_names: &'a ContextNames) -> JobBuilder<'a> { + JobBuilder { + cluster: cluster.clone(), + context_names, + resource_names: ResourceNames { + cluster_name: cluster.name.clone(), + product_name: context_names.product_name.clone(), + }, + } + } + + pub fn build_run_securityadmin_job(&self) -> Job { + let product_image = self + .cluster + .image + .resolve("opensearch", crate::built_info::PKG_VERSION); + // Maybe add a suffix for consecutive + let metadata = self.common_metadata(format!( + "{}-run-securityadmin", + self.resource_names.cluster_name, + )); + + let args = [ + "plugins/opensearch-security/tools/securityadmin.sh".to_string(), + "-cacert".to_string(), + "config/tls-client/ca.crt".to_string(), + "-cert".to_string(), + "config/tls-client/tls.crt".to_string(), + "-key".to_string(), + "config/tls-client/tls.key".to_string(), + "--hostname".to_string(), + self.opensearch_master_fqdn(), + "--configdir".to_string(), + "config/opensearch-security/".to_string(), + ]; + let mut cb = ContainerBuilder::new(RUN_SECURITYADMIN_CONTAINER_NAME) + .expect("should be a valid container name"); + let container = cb + .image_from_product_image(&product_image) + .command(vec!["sh".to_string(), "-c".to_string()]) + .args(vec![args.join(" ")]) + // The VolumeMount for the secret operator key store certificates + .add_volume_mounts([ + VolumeMount { + mount_path: RUN_SECURITYADMIN_CERT_VOLUME_MOUNT.to_owned(), + name: RUN_SECURITYADMIN_CERT_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: SECURITY_CONFIG_VOLUME_MOUNT.to_owned(), + name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, + ]) + .expect("the mount paths are statically defined and there should be no duplicates") + .resources( + ResourceRequirementsBuilder::new() + .with_cpu_request("100m") + .with_cpu_limit("400m") + .with_memory_request("128Mi") + .with_memory_limit("512Mi") + .build(), + ) + .build(); + + let pod_template = PodTemplateSpec { + metadata: Some(metadata.clone()), + spec: Some(PodSpec { + containers: vec![container], + + security_context: Some(PodSecurityContext { + fs_group: Some(1000), + ..PodSecurityContext::default() + }), + service_account_name: Some(self.resource_names.service_account_name()), + volumes: Some(vec![Volume { + name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), + secret: Some(SecretVolumeSource { + secret_name: Some("opensearch-security-config".to_string()), + ..Default::default() + }), + ..Volume::default() + }]), + ..PodSpec::default() + }), + }; + + Job { + metadata, + spec: Some(JobSpec { + backoff_limit: Some(100), + ttl_seconds_after_finished: Some(120), + template: pod_template, + ..JobSpec::default() + }), + ..Job::default() + } + } + + fn opensearch_master_fqdn(&self) -> String { + let cluster_manager_service_name = self.resource_names.discovery_service_name(); + let namespace = &self.cluster.namespace; + let cluster_domain = &self.context_names.cluster_domain_name; + format!("{cluster_manager_service_name}.{namespace}.svc.{cluster_domain}") + } + + fn common_metadata(&self, resource_name: impl Into) -> ObjectMeta { + ObjectMetaBuilder::new() + .name(resource_name) + .namespace(&self.cluster.namespace) + .ownerreference(ownerreference_from_resource( + &self.cluster, + None, + Some(true), + )) + .with_labels(self.labels()) + .build() + } + + /// Labels on role resources + fn labels(&self) -> Labels { + // Well-known Kubernetes labels + let mut labels = Labels::role_selector( + &self.cluster, + &self.context_names.product_name.to_label_value(), + &ValidatedCluster::role_name().to_label_value(), + ) + .unwrap(); + + let managed_by = Label::managed_by( + &self.context_names.operator_name.to_string(), + &self.context_names.controller_name.to_string(), + ) + .unwrap(); + let version = Label::version(&self.cluster.product_version.to_string()).unwrap(); + + labels.insert(managed_by); + labels.insert(version); + + // Stackable-specific labels + labels + .parse_insert((STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE)) + .unwrap(); + + labels + } +} From c489b44d089ac47266552edf4a69f7f89e1b414e Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 15 Aug 2025 15:48:04 +0200 Subject: [PATCH 02/15] configure tls on run-securityadmin --- rust/operator-binary/src/controller/build.rs | 3 +- .../src/controller/build/job_builder.rs | 45 ++++++++++++------- rust/operator-binary/src/framework/builder.rs | 1 + .../src/framework/builder/volume.rs | 34 ++++++++++++++ 4 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 rust/operator-binary/src/framework/builder/volume.rs diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 051f33d..a3683bb 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -2,9 +2,8 @@ use std::marker::PhantomData; use role_builder::RoleBuilder; -use crate::controller::build::job_builder::JobBuilder; - use super::{ContextNames, KubernetesResources, Prepared, ValidatedCluster}; +use crate::controller::build::job_builder::JobBuilder; pub mod job_builder; pub mod node_config; diff --git a/rust/operator-binary/src/controller/build/job_builder.rs b/rust/operator-binary/src/controller/build/job_builder.rs index ee2beb7..dfde724 100644 --- a/rust/operator-binary/src/controller/build/job_builder.rs +++ b/rust/operator-binary/src/controller/build/job_builder.rs @@ -1,7 +1,10 @@ use stackable_operator::{ builder::{ meta::ObjectMetaBuilder, - pod::{container::ContainerBuilder, resources::ResourceRequirementsBuilder}, + pod::{ + container::ContainerBuilder, resources::ResourceRequirementsBuilder, + volume::SecretFormat, + }, }, k8s_openapi::api::{ batch::v1::{Job, JobSpec}, @@ -14,17 +17,20 @@ use stackable_operator::{ Label, Labels, consts::{STACKABLE_VENDOR_KEY, STACKABLE_VENDOR_VALUE}, }, + time::Duration, }; use crate::{ controller::{ContextNames, ValidatedCluster}, framework::{ - IsLabelValue, builder::meta::ownerreference_from_resource, role_utils::ResourceNames, + IsLabelValue, + builder::{meta::ownerreference_from_resource, volume::build_tls_volume}, + role_utils::ResourceNames, }, }; const RUN_SECURITYADMIN_CERT_VOLUME_NAME: &str = "tls"; -const RUN_SECURITYADMIN_CERT_VOLUME_MOUNT: &str = "/stackable/cert"; +const RUN_SECURITYADMIN_CERT_VOLUME_MOUNT: &str = "/stackable/tls-client"; const SECURITY_CONFIG_VOLUME_NAME: &str = "security-config"; const SECURITY_CONFIG_VOLUME_MOUNT: &str = "/stackable/opensearch/config/opensearch-security"; const RUN_SECURITYADMIN_CONTAINER_NAME: &str = "run-securityadmin"; @@ -61,11 +67,11 @@ impl<'a> JobBuilder<'a> { let args = [ "plugins/opensearch-security/tools/securityadmin.sh".to_string(), "-cacert".to_string(), - "config/tls-client/ca.crt".to_string(), + "/stackable/tls-client/ca.crt".to_string(), "-cert".to_string(), - "config/tls-client/tls.crt".to_string(), + "/stackable/tls-client/tls.crt".to_string(), "-key".to_string(), - "config/tls-client/tls.key".to_string(), + "/stackable/tls-client/tls.key".to_string(), "--hostname".to_string(), self.opensearch_master_fqdn(), "--configdir".to_string(), @@ -105,20 +111,29 @@ impl<'a> JobBuilder<'a> { metadata: Some(metadata.clone()), spec: Some(PodSpec { containers: vec![container], - security_context: Some(PodSecurityContext { fs_group: Some(1000), ..PodSecurityContext::default() }), + restart_policy: Some("OnFailure".to_string()), service_account_name: Some(self.resource_names.service_account_name()), - volumes: Some(vec![Volume { - name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), - secret: Some(SecretVolumeSource { - secret_name: Some("opensearch-security-config".to_string()), - ..Default::default() - }), - ..Volume::default() - }]), + volumes: Some(vec![ + Volume { + name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), + secret: Some(SecretVolumeSource { + secret_name: Some("opensearch-security-config".to_string()), + ..Default::default() + }), + ..Volume::default() + }, + build_tls_volume( + RUN_SECURITYADMIN_CERT_VOLUME_NAME, + Vec::::new(), + SecretFormat::TlsPem, + &Duration::from_days_unchecked(15), + None, + ), + ]), ..PodSpec::default() }), }; diff --git a/rust/operator-binary/src/framework/builder.rs b/rust/operator-binary/src/framework/builder.rs index 40caba1..078acfa 100644 --- a/rust/operator-binary/src/framework/builder.rs +++ b/rust/operator-binary/src/framework/builder.rs @@ -1,3 +1,4 @@ pub mod meta; pub mod pdb; pub mod pod; +pub mod volume; diff --git a/rust/operator-binary/src/framework/builder/volume.rs b/rust/operator-binary/src/framework/builder/volume.rs new file mode 100644 index 0000000..173eeb5 --- /dev/null +++ b/rust/operator-binary/src/framework/builder/volume.rs @@ -0,0 +1,34 @@ +use stackable_operator::{ + builder::pod::volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + k8s_openapi::api::core::v1::Volume, + time::Duration, +}; + +pub fn build_tls_volume( + volume_name: &str, + service_scopes: impl IntoIterator>, + secret_format: SecretFormat, + requested_secret_lifetime: &Duration, + listener_scope: Option<&str>, +) -> Volume { + let mut secret_volume_source_builder = + SecretOperatorVolumeSourceBuilder::new("tls".to_string()); + + for scope in service_scopes { + secret_volume_source_builder.with_service_scope(scope.as_ref()); + } + if let Some(listener_scope) = listener_scope { + secret_volume_source_builder.with_listener_volume_scope(listener_scope); + } + + VolumeBuilder::new(volume_name) + .ephemeral( + secret_volume_source_builder + .with_pod_scope() + .with_format(secret_format) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .expect("volume should be built"), + ) + .build() +} From ffa0585186c8cf1ebe342e9f4c001928b8146b3a Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 19 Aug 2025 14:47:53 +0200 Subject: [PATCH 03/15] add tls secret class to crd --- .../helm/opensearch-operator/crds/crds.yaml | 13 ++++++++ rust/operator-binary/src/controller.rs | 3 +- .../src/controller/build/job_builder.rs | 21 ++++++------ .../src/controller/build/node_config.rs | 10 +++++- .../src/controller/validate.rs | 24 ++++++++++++-- rust/operator-binary/src/crd/mod.rs | 33 +++++++++++++++++++ rust/operator-binary/src/framework.rs | 9 ++++- .../src/framework/builder/volume.rs | 5 +-- 8 files changed, 101 insertions(+), 17 deletions(-) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 7b9d514..b39df8e 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -25,6 +25,18 @@ spec: spec: description: A OpenSearch cluster stacklet. This resource is managed by the Stackable operator for OpenSearch. Find more information on how to use it and the resources that the operator generates in the [operator documentation](https://docs.stackable.tech/home/nightly/opensearch/). properties: + clusterConfig: + properties: + tls: + properties: + secretClass: + default: tls + description: 'Only affects client connections. This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client' + type: string + type: object + required: + - tls + type: object clusterOperation: default: reconciliationPaused: false @@ -461,6 +473,7 @@ spec: - roleGroups type: object required: + - clusterConfig - image - nodes type: object diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 35fb559..4c0303a 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -28,7 +28,7 @@ use validate::validate; use crate::{ crd::{ NodeRoles, - v1alpha1::{self}, + v1alpha1::{self, OpenSearchClusterConfig}, }, framework::{ ClusterName, ControllerName, HasNamespace, HasObjectName, HasUid, IsLabelValue, @@ -132,6 +132,7 @@ pub struct ValidatedCluster { pub name: ClusterName, pub namespace: String, pub uid: String, + pub cluster_config: OpenSearchClusterConfig, pub role_config: GenericRoleConfig, // "validated" means that labels are valid and no ugly rolegroup name broke them pub role_group_configs: BTreeMap, diff --git a/rust/operator-binary/src/controller/build/job_builder.rs b/rust/operator-binary/src/controller/build/job_builder.rs index dfde724..7cabf28 100644 --- a/rust/operator-binary/src/controller/build/job_builder.rs +++ b/rust/operator-binary/src/controller/build/job_builder.rs @@ -66,17 +66,18 @@ impl<'a> JobBuilder<'a> { let args = [ "plugins/opensearch-security/tools/securityadmin.sh".to_string(), + "--hostname".to_string(), + self.opensearch_master_fqdn(), + "--configdir".to_string(), + "config/opensearch-security/".to_string(), "-cacert".to_string(), "/stackable/tls-client/ca.crt".to_string(), "-cert".to_string(), "/stackable/tls-client/tls.crt".to_string(), "-key".to_string(), "/stackable/tls-client/tls.key".to_string(), - "--hostname".to_string(), - self.opensearch_master_fqdn(), - "--configdir".to_string(), - "config/opensearch-security/".to_string(), ]; + let mut cb = ContainerBuilder::new(RUN_SECURITYADMIN_CONTAINER_NAME) .expect("should be a valid container name"); let container = cb @@ -84,15 +85,15 @@ impl<'a> JobBuilder<'a> { .command(vec!["sh".to_string(), "-c".to_string()]) .args(vec![args.join(" ")]) // The VolumeMount for the secret operator key store certificates - .add_volume_mounts([ + .add_volume_mounts(vec![ VolumeMount { - mount_path: RUN_SECURITYADMIN_CERT_VOLUME_MOUNT.to_owned(), - name: RUN_SECURITYADMIN_CERT_VOLUME_NAME.to_owned(), + mount_path: SECURITY_CONFIG_VOLUME_MOUNT.to_owned(), + name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), ..VolumeMount::default() }, VolumeMount { - mount_path: SECURITY_CONFIG_VOLUME_MOUNT.to_owned(), - name: SECURITY_CONFIG_VOLUME_NAME.to_owned(), + mount_path: RUN_SECURITYADMIN_CERT_VOLUME_MOUNT.to_owned(), + name: RUN_SECURITYADMIN_CERT_VOLUME_NAME.to_owned(), ..VolumeMount::default() }, ]) @@ -106,7 +107,6 @@ impl<'a> JobBuilder<'a> { .build(), ) .build(); - let pod_template = PodTemplateSpec { metadata: Some(metadata.clone()), spec: Some(PodSpec { @@ -128,6 +128,7 @@ impl<'a> JobBuilder<'a> { }, build_tls_volume( RUN_SECURITYADMIN_CERT_VOLUME_NAME, + &self.cluster.cluster_config.tls.secret_class, Vec::::new(), SecretFormat::TlsPem, &Duration::from_days_unchecked(15), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 6eb9ba6..046e8d3 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -258,7 +258,10 @@ mod tests { use super::*; use crate::{ controller::ValidatedOpenSearchConfig, - crd::NodeRoles, + crd::{ + NodeRoles, + v1alpha1::{OpenSearchClusterConfig, OpenSearchTls}, + }, framework::{ClusterName, ProductVersion, role_utils::GenericProductSpecificCommonConfig}, }; @@ -275,6 +278,11 @@ mod tests { .expect("should be a valid ClusterName"), namespace: "default".to_owned(), uid: "0b1e30e6-326e-4c1a-868d-ad6598b49e8b".to_owned(), + cluster_config: OpenSearchClusterConfig { + tls: OpenSearchTls { + secret_class: "my-tls-secret-class".to_owned(), + }, + }, role_config: GenericRoleConfig::default(), role_group_configs: BTreeMap::new(), }; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 959ef80..ac20660 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -13,9 +13,11 @@ use super::{ ValidatedOpenSearchConfig, }; use crate::{ - crd::v1alpha1::{self, OpenSearchConfig, OpenSearchConfigFragment}, + crd::v1alpha1::{ + self, OpenSearchClusterConfig, OpenSearchConfig, OpenSearchConfigFragment, OpenSearchTls, + }, framework::{ - ClusterName, + ClusterName, TlsSecretClassName, role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig, with_validated_config}, }, }; @@ -41,6 +43,9 @@ pub enum Error { #[snafu(display("failed to set role-group name"))] ParseRoleGroupName { source: crate::framework::Error }, + #[snafu(display("failed to set tls secret class"))] + ParseTlsSecretClassName { source: crate::framework::Error }, + #[snafu(display("fragment validation failure"))] ValidateOpenSearchConfig { source: stackable_operator::config::fragment::ValidationError, @@ -70,6 +75,8 @@ pub fn validate( let product_version = ProductVersion::from_str(cluster.spec.image.product_version()) .context(ParseProductVersionSnafu)?; + let validated_cluster_config = validate_cluster_config(cluster.spec.cluster_config.clone())?; + let mut role_group_configs = BTreeMap::new(); for (raw_role_group_name, role_group_config) in &cluster.spec.nodes.role_groups { let role_group_name = @@ -88,11 +95,24 @@ pub fn validate( name: cluster_name, namespace, uid, + cluster_config: validated_cluster_config, role_config: cluster.spec.nodes.role_config.clone(), role_group_configs, }) } +fn validate_cluster_config( + cluster_config: OpenSearchClusterConfig, +) -> Result { + validate_tls_config(&cluster_config.tls)?; + Ok(cluster_config) +} + +fn validate_tls_config(tls_config: &OpenSearchTls) -> Result<()> { + TlsSecretClassName::from_str(&tls_config.secret_class).context(ParseTlsSecretClassNameSnafu)?; + Ok(()) +} + fn validate_role_group_config( context_names: &ContextNames, cluster_name: &ClusterName, diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 84a6efa..c31636d 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -31,6 +31,7 @@ use crate::framework::{ }; const DEFAULT_LISTENER_CLASS: &str = "cluster-internal"; +const TLS_DEFAULT_SECRET_CLASS: &str = "tls"; #[versioned(version(name = "v1alpha1"))] pub mod versioned { @@ -57,6 +58,9 @@ pub mod versioned { // no doc string - see ProductImage struct pub image: ProductImage, + // no doc string - see ProductImage struct + pub cluster_config: OpenSearchClusterConfig, + // no doc string - see ClusterOperation struct #[serde(default)] pub cluster_operation: ClusterOperation, @@ -66,6 +70,23 @@ pub mod versioned { Role, } + #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct OpenSearchClusterConfig { + pub tls: OpenSearchTls, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct OpenSearchTls { + /// Only affects client connections. + /// This setting controls: + /// - If TLS encryption is used at all + /// - Which cert the servers should use to authenticate themselves against the client + #[serde(default = "tls_secret_class_default")] + pub secret_class: String, + } + // The possible node roles are by default the built-in roles and the search role, see // https://github.com/opensearch-project/OpenSearch/blob/3.0.0/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java#L609-L614. // @@ -243,6 +264,18 @@ impl v1alpha1::OpenSearchConfig { } } +impl Default for v1alpha1::OpenSearchTls { + fn default() -> Self { + v1alpha1::OpenSearchTls { + secret_class: tls_secret_class_default(), + } + } +} + +fn tls_secret_class_default() -> String { + TLS_DEFAULT_SECRET_CLASS.to_string() +} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct NodeRoles(Vec); diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 07c5699..4ee212e 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -38,6 +38,9 @@ pub enum Error { #[allow(dead_code)] pub const MAX_OBJECT_NAME_LENGTH: usize = 253; +#[allow(dead_code)] +pub const MAX_ANNOTATION_LENGTH: usize = 253; + /// Has a name that can be used as a DNS subdomain name as defined in RFC 1123. /// Most resource types, e.g. a Pod, require such a compliant name. pub trait HasObjectName { @@ -172,7 +175,11 @@ attributed_string_type! { is_object_name, is_valid_label_value } - +attributed_string_type! { + TlsSecretClassName, + "The TLS SecretClass name", + (max_length = MAX_ANNOTATION_LENGTH - 30) +} #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/rust/operator-binary/src/framework/builder/volume.rs b/rust/operator-binary/src/framework/builder/volume.rs index 173eeb5..14eb9a0 100644 --- a/rust/operator-binary/src/framework/builder/volume.rs +++ b/rust/operator-binary/src/framework/builder/volume.rs @@ -6,13 +6,14 @@ use stackable_operator::{ pub fn build_tls_volume( volume_name: &str, + tls_secret_class_name: &str, service_scopes: impl IntoIterator>, secret_format: SecretFormat, requested_secret_lifetime: &Duration, listener_scope: Option<&str>, ) -> Volume { let mut secret_volume_source_builder = - SecretOperatorVolumeSourceBuilder::new("tls".to_string()); + SecretOperatorVolumeSourceBuilder::new(tls_secret_class_name); for scope in service_scopes { secret_volume_source_builder.with_service_scope(scope.as_ref()); @@ -28,7 +29,7 @@ pub fn build_tls_volume( .with_format(secret_format) .with_auto_tls_cert_lifetime(*requested_secret_lifetime) .build() - .expect("volume should be built"), + .expect("volume should be built without parse errors"), ) .build() } From 4d7496c92516eb3251c68e98d56b02e02e4a20b8 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Wed, 20 Aug 2025 14:35:24 +0200 Subject: [PATCH 04/15] add tls volume to sts --- .../controller/build/role_group_builder.rs | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 49d5f22..7263343 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -1,5 +1,8 @@ use stackable_operator::{ - builder::{meta::ObjectMetaBuilder, pod::container::ContainerBuilder}, + builder::{ + meta::ObjectMetaBuilder, + pod::{container::ContainerBuilder, volume::SecretFormat}, + }, crd::listener::{self}, k8s_openapi::{ DeepMerge, @@ -15,6 +18,7 @@ use stackable_operator::{ }, kube::api::ObjectMeta, kvp::{Label, Labels}, + time::Duration, }; use super::node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}; @@ -23,7 +27,7 @@ use crate::{ crd::v1alpha1, framework::{ RoleGroupName, - builder::meta::ownerreference_from_resource, + builder::{meta::ownerreference_from_resource, volume::build_tls_volume}, kvp::label::{recommended_labels, role_group_selector, role_selector}, listener::listener_pvc, role_group_utils::ResourceNames, @@ -40,6 +44,8 @@ const DATA_VOLUME_NAME: &str = "data"; const LISTENER_VOLUME_NAME: &str = "listener"; const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; +const TLS_VOLUME_NAME: &str = "tls"; +const TLS_VOLUME_DIR: &str = "/stackable/tls"; const DEFAULT_OPENSEARCH_HOME: &str = "/stackable/opensearch"; @@ -152,8 +158,13 @@ impl<'a> RoleGroupBuilder<'a> { fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); + let mut service_scopes = vec![self.resource_names.headless_service_name()]; + for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert(Self::build_node_role_label(node_role)); + if let v1alpha1::NodeRole::ClusterManager = node_role { + service_scopes.push(self.cluster.name.to_string()) + } } let metadata = ObjectMetaBuilder::new() @@ -198,14 +209,24 @@ impl<'a> RoleGroupBuilder<'a> { .config .termination_grace_period_seconds, ), - volumes: Some(vec![Volume { - name: CONFIG_VOLUME_NAME.to_owned(), - config_map: Some(ConfigMapVolumeSource { - name: self.resource_names.role_group_config_map(), - ..Default::default() - }), - ..Volume::default() - }]), + volumes: Some(vec![ + Volume { + name: CONFIG_VOLUME_NAME.to_owned(), + config_map: Some(ConfigMapVolumeSource { + name: self.resource_names.role_group_config_map(), + ..Default::default() + }), + ..Volume::default() + }, + build_tls_volume( + TLS_VOLUME_NAME, + &self.cluster.cluster_config.tls.secret_class, + service_scopes, + SecretFormat::TlsPem, + &Duration::from_days_unchecked(15), + Some(LISTENER_VOLUME_NAME), + ), + ]), ..PodSpec::default() }), }; @@ -312,6 +333,11 @@ impl<'a> RoleGroupBuilder<'a> { name: LISTENER_VOLUME_NAME.to_owned(), ..VolumeMount::default() }, + VolumeMount { + mount_path: TLS_VOLUME_DIR.to_owned(), + name: TLS_VOLUME_NAME.to_owned(), + ..VolumeMount::default() + }, ]) .expect("The mount paths are statically defined and there should be no duplicates.") .add_container_ports(vec![ From 7f805b8b000a56aa1350561b5c08c1f63e098c09 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 21 Aug 2025 10:45:35 +0200 Subject: [PATCH 05/15] add tls config to opensearch.yml --- .../src/controller/build/node_config.rs | 77 +++++++++++++++++-- .../controller/build/role_group_builder.rs | 7 +- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 046e8d3..74d2af7 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -1,4 +1,4 @@ -use serde_json::{Value, json}; +use serde_json::{Map, Value, json}; use stackable_operator::builder::pod::container::FieldPathEnvVar; use super::ValidatedCluster; @@ -66,6 +66,32 @@ pub const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; /// type: list of strings pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; +/// type: string +pub const TLS_HTTP_ENABLED: &str = "plugins.security.ssl.http.enabled"; + +/// type: string +pub const TLS_HTTP_PEMCERT_FILEPATH: &str = "plugins.security.ssl.http.pemcert_filepath"; + +/// type: string +pub const TLS_HTTP_PEMKEY_FILEPATH: &str = "plugins.security.ssl.http.pemkey_filepath"; + +/// type: string +pub const TLS_HTTP_PEMTRUSTEDCAS_FILEPATH: &str = + "plugins.security.ssl.http.pemtrustedcas_filepath"; + +/// type: string +pub const TLS_TRANSPORT_ENABLED: &str = "plugins.security.ssl.transport.enabled"; + +/// type: string +pub const TLS_TRANSPORT_PEMCERT_FILEPATH: &str = "plugins.security.ssl.transport.pemcert_filepath"; + +/// type: string +pub const TLS_TRANSPORT_PEMKEY_FILEPATH: &str = "plugins.security.ssl.transport.pemkey_filepath"; + +/// type: string +pub const TLS_TRANSPORT_PEMTRUSTEDCAS_FILEPATH: &str = + "plugins.security.ssl.transport.pemtrustedcas_filepath"; + pub struct NodeConfig { cluster: ValidatedCluster, role_group_config: OpenSearchRoleGroupConfig, @@ -88,9 +114,8 @@ impl NodeConfig { } /// static for the cluster - pub fn static_opensearch_config(&self) -> String { - let mut config = serde_json::Map::new(); - + pub fn static_opensearch_config(&self) -> Map { + let mut config = Map::new(); config.insert( CONFIG_OPTION_CLUSTER_NAME.to_owned(), json!(self.cluster.name.to_string()), @@ -111,10 +136,52 @@ impl NodeConfig { json!(["CN=generated certificate for pod".to_owned()]), ); + config + } + + pub fn tls_config(&self) -> serde_json::Map { + let mut config = Map::new(); + // TLS config for HTTP port + config.insert(TLS_HTTP_ENABLED.to_owned(), json!("true".to_string())); + config.insert( + TLS_HTTP_PEMCERT_FILEPATH.to_owned(), + json!("/stackable/tls/tls.crt".to_string()), + ); + config.insert( + TLS_HTTP_PEMKEY_FILEPATH.to_owned(), + json!("/stackable/tls/tls.key".to_string()), + ); + config.insert( + TLS_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!("/stackable/tls/ca.crt".to_string()), + ); + // TLS config for TRANSPORT port + config.insert(TLS_TRANSPORT_ENABLED.to_owned(), json!("true".to_string())); + config.insert( + TLS_TRANSPORT_PEMCERT_FILEPATH.to_owned(), + json!("/stackable/tls/tls.crt".to_string()), + ); + config.insert( + TLS_TRANSPORT_PEMKEY_FILEPATH.to_owned(), + json!("/stackable/tls/tls.key".to_string()), + ); + config.insert( + TLS_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!("/stackable/tls/ca.crt".to_string()), + ); + + config + } + + pub fn build_config_file( + &self, + file_name: &str, + mut config: serde_json::Map, + ) -> String { for (setting, value) in self .role_group_config .config_overrides - .get(CONFIGURATION_FILE_OPENSEARCH_YML) + .get(file_name) .into_iter() .flatten() { diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 7263343..c69fce4 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -1,3 +1,4 @@ +use serde_json::Map; use stackable_operator::{ builder::{ meta::ObjectMetaBuilder, @@ -91,9 +92,13 @@ impl<'a> RoleGroupBuilder<'a> { let metadata = self.common_metadata(self.resource_names.role_group_config_map(), Labels::new()); + let mut opensearch_yml = Map::new(); + opensearch_yml.append(&mut self.node_config.static_opensearch_config()); + opensearch_yml.append(&mut self.node_config.tls_config()); let data = [( CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), - self.node_config.static_opensearch_config(), + self.node_config + .build_config_file(CONFIGURATION_FILE_OPENSEARCH_YML, opensearch_yml), )] .into(); From c6db525100cf43031d48bf1313474542780287fa Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Fri, 22 Aug 2025 10:54:54 +0200 Subject: [PATCH 06/15] disable security demo install --- .../src/controller/build/node_config.rs | 56 ++++++++++++++++++- .../controller/build/role_group_builder.rs | 1 + 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 74d2af7..e976b6d 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -66,6 +66,28 @@ pub const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; /// type: list of strings pub const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_DISABLE_INSTALL_DEMO: &str = "DISABLE_INSTALL_DEMO_CONFIG"; + +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_AUDIT_TYPE: &str = "plugins.security.audit.type"; + +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_AUDIT_LOG4J_LEVEL: &str = + "plugins.security.audit.config.log4j.level"; + +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_LOG4J_LOGGER_NAME: &str = + "plugins.security.audit.config.log4j.logger_name"; + +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED: &str = + "plugins.security.restapi.roles_enabled"; + +/// type: string +pub const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = + "allow_default_init_securityindex"; + /// type: string pub const TLS_HTTP_ENABLED: &str = "plugins.security.ssl.http.enabled"; @@ -139,7 +161,7 @@ impl NodeConfig { config } - pub fn tls_config(&self) -> serde_json::Map { + pub fn tls_config(&self) -> Map { let mut config = Map::new(); // TLS config for HTTP port config.insert(TLS_HTTP_ENABLED.to_owned(), json!("true".to_string())); @@ -173,6 +195,32 @@ impl NodeConfig { config } + pub fn security_config(&self) -> Map { + let mut config = Map::new(); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_AUDIT_TYPE.to_owned(), + json!("log4j".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_AUDIT_LOG4J_LEVEL.to_owned(), + json!("INFO".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_LOG4J_LOGGER_NAME.to_owned(), + json!("oseaudit".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED.to_owned(), + json!("all_access".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), + json!("true".to_string()), + ); + + config + } + pub fn build_config_file( &self, file_name: &str, @@ -208,6 +256,7 @@ impl NodeConfig { CONFIG_OPTION_INITIAL_CLUSTER_MANAGER_NODES, self.initial_cluster_manager_nodes(), ) + .with_value(CONFIG_OPTION_PLUGINS_SECURITY_DISABLE_INSTALL_DEMO, "true") .with_value( CONFIG_OPTION_NODE_ROLES, self.role_group_config @@ -381,6 +430,11 @@ mod tests { // TODO Test EnvVarSet and compare EnvVarSets assert_eq!( vec![ + EnvVar { + name: "DISABLE_INSTALL_DEMO_CONFIG".to_owned(), + value: Some("true".to_owned()), + value_from: None + }, EnvVar { name: "TEST".to_owned(), value: Some("value".to_owned()), diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index c69fce4..07a8a21 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -95,6 +95,7 @@ impl<'a> RoleGroupBuilder<'a> { let mut opensearch_yml = Map::new(); opensearch_yml.append(&mut self.node_config.static_opensearch_config()); opensearch_yml.append(&mut self.node_config.tls_config()); + opensearch_yml.append(&mut self.node_config.security_config()); let data = [( CONFIGURATION_FILE_OPENSEARCH_YML.to_owned(), self.node_config From bbbdbb6de924fff734f5fd6e3b15a0fcd44baacc Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Mon, 27 Oct 2025 15:15:48 +0100 Subject: [PATCH 07/15] wip amend integration tests --- .../controller/build/role_group_builder.rs | 9 ++-- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 24 +++++----- .../kuttl/smoke/10-install-opensearch.yaml.j2 | 46 ++----------------- 3 files changed, 20 insertions(+), 59 deletions(-) diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index 363a1e1..abdc51e 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -215,7 +215,10 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the [`PodTemplateSpec`] for the role-group [`StatefulSet`] fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); - let service_scopes = vec![self.resource_names.headless_service_name()]; + let service_scopes = vec![ + self.resource_names.cluster_name.to_string(), + self.resource_names.headless_service_name().to_string(), + ]; for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert(Self::build_node_role_label(node_role)); @@ -261,8 +264,6 @@ impl<'a> RoleGroupBuilder<'a> { self.resource_names.role_group_config_map() }; - let requested_secret_lifetime = self.role_group_config.config.requested_secret_lifetime; - let mut volumes = vec![ Volume { name: CONFIG_VOLUME_NAME.to_string(), @@ -300,7 +301,7 @@ impl<'a> RoleGroupBuilder<'a> { tls_secret_class_name, service_scopes, SecretFormat::TlsPem, - &requested_secret_lifetime, + &self.role_group_config.config.requested_secret_lifetime, Some(&LISTENER_VOLUME_NAME.to_string()), )) }; diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 60c58ed..33e96be 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -656,13 +656,13 @@ data: plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "/stackable/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/stackable/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/stackable/tls/ca.crt" plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "/stackable/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/stackable/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/stackable/tls/ca.crt" --- apiVersion: v1 kind: ConfigMap @@ -691,13 +691,13 @@ data: plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "/stackable/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "/stackable/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "/stackable/tls/ca.crt" plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "/stackable/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "/stackable/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "/stackable/tls/ca.crt" --- apiVersion: v1 kind: Service diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 index ee70e27..dcbc66f 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -31,15 +33,6 @@ spec: capacity: 100Mi listenerClass: external-stable replicas: 3 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-cluster-manager-headless data: config: nodeRoles: @@ -52,15 +45,6 @@ spec: capacity: 2Gi listenerClass: cluster-internal replicas: 2 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data-headless envOverrides: # Only required for the official image # The official image (built with https://github.com/opensearch-project/opensearch-build) @@ -78,14 +62,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt podOverrides: spec: containers: @@ -94,27 +70,11 @@ spec: - name: security-config mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security readOnly: true - - name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret From ab42f5ac77a8243047b6e2b3c5047a2bab3ffa82 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 12:10:28 +0100 Subject: [PATCH 08/15] fix smoke test --- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 35fcedd..38aa340 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -169,12 +169,11 @@ spec: name: listener - mountPath: /stackable/log name: log + - mountPath: /stackable/tls + name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config readOnly: true - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - name: tls - readOnly: true {% if lookup('env', 'VECTOR_AGGREGATOR') %} - args: - |- @@ -256,16 +255,12 @@ spec: - emptyDir: sizeLimit: 30Mi name: log - - name: security-config - secret: - defaultMode: 0o660 - secretName: opensearch-security-config - ephemeral: volumeClaimTemplate: metadata: annotations: secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-cluster-manager-headless + secrets.stackable.tech/scope: service=opensearch,service=opensearch-nodes-cluster-manager-headless,listener-volume=listener,pod spec: accessModes: - ReadWriteOnce @@ -275,6 +270,10 @@ spec: storageClassName: secrets.stackable.tech volumeMode: Filesystem name: tls + - name: security-config + secret: + defaultMode: 0o660 + secretName: opensearch-security-config volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim @@ -481,12 +480,11 @@ spec: name: listener - mountPath: /stackable/log name: log + - mountPath: /stackable/tls + name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config readOnly: true - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - name: tls - readOnly: true {% if lookup('env', 'VECTOR_AGGREGATOR') %} - args: - |- @@ -568,16 +566,12 @@ spec: - emptyDir: sizeLimit: 30Mi name: log - - name: security-config - secret: - defaultMode: 0o660 - secretName: opensearch-security-config - ephemeral: volumeClaimTemplate: metadata: annotations: secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data-headless + secrets.stackable.tech/scope: service=opensearch,service=opensearch-nodes-data-headless,listener-volume=listener,pod spec: accessModes: - ReadWriteOnce @@ -587,6 +581,10 @@ spec: storageClassName: secrets.stackable.tech volumeMode: Filesystem name: tls + - name: security-config + secret: + defaultMode: 0o660 + secretName: opensearch-security-config volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim From 97681a376c89828955ed954ef0cc3332ef04b4d7 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 14:35:08 +0100 Subject: [PATCH 09/15] restore properties file --- deploy/helm/opensearch-operator/configs/properties.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 deploy/helm/opensearch-operator/configs/properties.yaml diff --git a/deploy/helm/opensearch-operator/configs/properties.yaml b/deploy/helm/opensearch-operator/configs/properties.yaml new file mode 100644 index 0000000..9bd8c3b --- /dev/null +++ b/deploy/helm/opensearch-operator/configs/properties.yaml @@ -0,0 +1,5 @@ +--- +version: 0.1.0 +spec: + units: [] +properties: [] From 815d243bdf5205dd9ae992d45deaa7caedac067e Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 15:15:09 +0100 Subject: [PATCH 10/15] mount tls volume in default directory of official image --- .../src/controller/build/node_config.rs | 12 ++++---- .../controller/build/role_group_builder.rs | 4 +-- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 28 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index d7cb717..6ac274c 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -183,15 +183,15 @@ impl NodeConfig { ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!("/stackable/tls/tls.crt".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/tls.crt".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!("/stackable/tls/tls.key".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/tls.key".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("/stackable/tls/ca.crt".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/ca.crt".to_string()), ); // TLS config for TRANSPORT port config.insert( @@ -200,15 +200,15 @@ impl NodeConfig { ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), - json!("/stackable/tls/tls.crt".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/tls.crt".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), - json!("/stackable/tls/tls.key".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/tls.key".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("/stackable/tls/ca.crt".to_string()), + json!("${OPENSEARCH_HOME}/config/tls/ca.crt".to_string()), ); config diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index abdc51e..e8cd846 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -71,8 +71,8 @@ constant!(DATA_VOLUME_NAME: VolumeName = "data"); constant!(LISTENER_VOLUME_NAME: PersistentVolumeClaimName = "listener"); const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; + constant!(TLS_VOLUME_NAME: VolumeName = "tls"); -const TLS_VOLUME_DIR: &str = "/stackable/tls"; constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; @@ -461,7 +461,7 @@ impl<'a> RoleGroupBuilder<'a> { if self.cluster.cluster_config.tls.secret_class.is_some() { volume_mounts.push(VolumeMount { - mount_path: TLS_VOLUME_DIR.to_owned(), + mount_path: format!("{opensearch_home}/config/tls"), name: TLS_VOLUME_NAME.to_string(), ..VolumeMount::default() }) diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 38aa340..88e63a8 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -169,7 +169,7 @@ spec: name: listener - mountPath: /stackable/log name: log - - mountPath: /stackable/tls + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config @@ -480,7 +480,7 @@ spec: name: listener - mountPath: /stackable/log name: log - - mountPath: /stackable/tls + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config @@ -654,13 +654,13 @@ data: plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "/stackable/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "/stackable/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "/stackable/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "/stackable/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "/stackable/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "/stackable/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" --- apiVersion: v1 kind: ConfigMap @@ -689,13 +689,13 @@ data: plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "/stackable/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "/stackable/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "/stackable/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "/stackable/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "/stackable/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "/stackable/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" --- apiVersion: v1 kind: Service From 95986a5cfff172a56b92d9a16b6fcfafa0831161 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 15:43:08 +0100 Subject: [PATCH 11/15] use tls feature in all integration tests --- .../kuttl/external-access/opensearch.yaml.j2 | 55 +------------------ .../kuttl/ldap/21-install-opensearch.yaml.j2 | 29 +--------- .../logging/20-install-opensearch.yaml.j2 | 28 +--------- .../metrics/20-install-opensearch.yaml.j2 | 29 +--------- .../10-install-opensearch.yaml.j2 | 37 +------------ .../snapshot-s3/20-install-opensearch.yaml.j2 | 29 +--------- 6 files changed, 17 insertions(+), 190 deletions(-) diff --git a/tests/templates/kuttl/external-access/opensearch.yaml.j2 b/tests/templates/kuttl/external-access/opensearch.yaml.j2 index 2b7da52..9e232bb 100644 --- a/tests/templates/kuttl/external-access/opensearch.yaml.j2 +++ b/tests/templates/kuttl/external-access/opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -27,45 +29,18 @@ spec: - cluster_manager listenerClass: test-external-stable-$NAMESPACE replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-cluster-manager-headless data1: config: nodeRoles: - data listenerClass: test-external-unstable-$NAMESPACE replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data1-headless data2: config: nodeRoles: - data listenerClass: test-cluster-internal-$NAMESPACE replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch-nodes-data2-headless envOverrides: # Only required for the official image # The official image (built with https://github.com/opensearch-project/opensearch-build) @@ -83,14 +58,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt podOverrides: spec: containers: @@ -99,27 +66,11 @@ spec: - name: security-config mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security readOnly: true - - name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret diff --git a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 index fb42f4e..3b03da7 100644 --- a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -40,14 +42,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt podOverrides: spec: containers: @@ -56,25 +50,8 @@ spec: - name: security-config mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security readOnly: true - - name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-default-headless - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" diff --git a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 index f17b427..de7d10b 100644 --- a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 @@ -27,6 +27,8 @@ spec: {% endif %} pullPolicy: IfNotPresent clusterConfig: + tls: + secretClass: tls vectorAggregatorConfigMapName: opensearch-vector-aggregator-discovery nodes: roleGroups: @@ -52,15 +54,6 @@ spec: ROOT: level: INFO replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-automatic-headless custom: config: logging: @@ -70,15 +63,6 @@ spec: custom: configMap: custom-log-config replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-custom-headless configOverrides: opensearch.yml: # Disable memory mapping in this test; If memory mapping were activated, the kernel setting @@ -90,14 +74,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt podOverrides: spec: containers: diff --git a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 index ad50b21..0de56c2 100644 --- a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -40,14 +42,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt podOverrides: spec: containers: @@ -56,28 +50,11 @@ spec: - name: security-config mountPath: /stackable/opensearch/config/opensearch-security readOnly: true - - name: tls - mountPath: /stackable/opensearch/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-default-headless - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret diff --git a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 index 0d620e1..c92794c 100644 --- a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -25,15 +27,6 @@ spec: config: listenerClass: external-unstable replicas: 1 - podOverrides: - spec: - volumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-default-headless envOverrides: # Only required for the official image # The official image (built with https://github.com/opensearch-project/opensearch-build) @@ -51,14 +44,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt podOverrides: spec: containers: @@ -67,26 +52,10 @@ spec: - name: security-config mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security readOnly: true - - name: tls - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret diff --git a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 index 19c88bc..cd4f98c 100644 --- a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 @@ -12,8 +12,10 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + tls: + secretClass: tls +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -35,14 +37,6 @@ spec: # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.allow_default_init_securityindex: "true" - plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.transport.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.transport.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt - plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: /stackable/opensearch/config/tls/tls.crt - plugins.security.ssl.http.pemkey_filepath: /stackable/opensearch/config/tls/tls.key - plugins.security.ssl.http.pemtrustedcas_filepath: /stackable/opensearch/config/tls/ca.crt s3.client.default.endpoint: http://minio:9000/ s3.client.default.protocol: http s3.client.default.region: unused # but required @@ -80,9 +74,6 @@ spec: - name: security-config mountPath: /stackable/opensearch/config/opensearch-security readOnly: true - - name: tls - mountPath: /stackable/opensearch/config/tls - readOnly: true - name: keystore mountPath: /stackable/opensearch/config/opensearch.keystore subPath: opensearch.keystore @@ -99,20 +90,6 @@ spec: secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-default-headless - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret From 910a58d3eefd147df52f18c605849c236dbd2989 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 16:26:00 +0100 Subject: [PATCH 12/15] use OPENSEARCH_PATH_CONF for tls config --- .../src/controller/build/node_config.rs | 12 ++++++------ .../src/controller/build/role_group_builder.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 6ac274c..006212b 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -183,15 +183,15 @@ impl NodeConfig { ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/tls.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/tls.crt".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/tls.key".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/tls.key".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/ca.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/ca.crt".to_string()), ); // TLS config for TRANSPORT port config.insert( @@ -200,15 +200,15 @@ impl NodeConfig { ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/tls.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/tls.crt".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/tls.key".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/tls.key".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_HOME}/config/tls/ca.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/ca.crt".to_string()), ); config diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index e8cd846..a482029 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -461,7 +461,7 @@ impl<'a> RoleGroupBuilder<'a> { if self.cluster.cluster_config.tls.secret_class.is_some() { volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_home}/config/tls"), + mount_path: format!("{opensearch_path_conf}/tls"), name: TLS_VOLUME_NAME.to_string(), ..VolumeMount::default() }) From beb2affbde4cf30f2395e2698b82eb7a3c950485 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Tue, 28 Oct 2025 16:33:47 +0100 Subject: [PATCH 13/15] fix incorrectly resolved merge conflict --- .../snapshot-s3/20-install-opensearch.yaml.j2 | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 index 41d3aae..5284693 100644 --- a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 @@ -90,8 +90,9 @@ spec: - name: security-config mountPath: /stackable/opensearch/config/opensearch-security readOnly: true - - name: tls - mountPath: /stackable/opensearch/config/tls + - name: system-trust-store + mountPath: /etc/pki/java/cacerts + subPath: java/cacerts readOnly: true - name: keystore mountPath: /stackable/opensearch/config/opensearch.keystore @@ -113,20 +114,9 @@ spec: secret: secretName: opensearch-security-config defaultMode: 0o660 - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/scope: node,pod,service=opensearch,service=opensearch-nodes-default-headless - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" + - name: system-trust-store + emptyDir: + sizeLimit: 10Mi --- apiVersion: v1 kind: Secret From 25745d11b798ac63828569489ab665af5d8cae2e Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 6 Nov 2025 11:53:18 +0100 Subject: [PATCH 14/15] wip: adress feedback on pr --- .../helm/opensearch-operator/crds/crds.yaml | 26 +++++++-- .../src/controller/build/node_config.rs | 56 ++++++++++--------- .../controller/build/role_group_builder.rs | 34 +++++++---- .../src/controller/validate.rs | 11 ++-- rust/operator-binary/src/crd/mod.rs | 38 +++++++++++-- rust/operator-binary/src/framework.rs | 10 ++-- .../src/framework/builder/volume.rs | 8 +-- .../logging/20-install-opensearch.yaml.j2 | 16 ------ 8 files changed, 121 insertions(+), 78 deletions(-) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 8ee20bc..d483c41 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -31,21 +31,37 @@ spec: clusterConfig: default: tls: - secretClass: null + restSecretClass: tls + transportSecretClass: tls description: Configuration that applies to all roles and role groups properties: tls: + default: + restSecretClass: tls + transportSecretClass: tls + description: TLS configuration options for the REST API and internal communication (transport). properties: - secretClass: + restSecretClass: + default: tls description: |- - Affects client connections and internal transport connections. + Only affects client connections to the REST API. This setting controls: - If TLS encryption is used at all - Which cert the servers should use to authenticate themselves against the client - maxLength: 223 + maxLength: 253 minLength: 1 nullable: true type: string + transportSecretClass: + default: tls + description: |- + Only affects internal communication (transport). Used for mutual verification between OpenSearch nodes. + This setting controls: + - Which cert the servers should use to authenticate themselves against other servers + - Which ca.crt to use when validating the other server + maxLength: 253 + minLength: 1 + type: string type: object vectorAggregatorConfigMapName: description: |- @@ -57,8 +73,6 @@ spec: minLength: 1 nullable: true type: string - required: - - tls type: object clusterOperation: default: diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 006212b..83d67d9 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -97,7 +97,7 @@ pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH: & pub struct NodeConfig { cluster: ValidatedCluster, role_group_config: OpenSearchRoleGroupConfig, - discovery_service_name: ServiceName, + pub discovery_service_name: ServiceName, } // Most functions are public because their configuration values could also be used in environment @@ -123,9 +123,7 @@ impl NodeConfig { pub fn opensearch_config(&self) -> serde_json::Map { let mut config = self.static_opensearch_config(); - if self.cluster.cluster_config.tls.secret_class.is_some() { - config.append(&mut self.tls_config()); - } + config.append(&mut self.tls_config()); for (setting, value) in self .role_group_config @@ -176,41 +174,49 @@ impl NodeConfig { pub fn tls_config(&self) -> serde_json::Map { let mut config = serde_json::Map::new(); - // TLS config for HTTP port - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), - json!("true".to_string()), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/tls.crt".to_string()), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/tls.key".to_string()), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/ca.crt".to_string()), - ); - // TLS config for TRANSPORT port + // TLS config for TRANSPORT port which is always enabled. config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), json!("true".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/tls.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/transport/tls.crt".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/tls.key".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/transport/tls.key".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/ca.crt".to_string()), + json!("${OPENSEARCH_PATH_CONF}/tls/transport/ca.crt".to_string()), ); + // TLS config for HTTP port which is optional. + if self.cluster.cluster_config.tls.rest_secret_class.is_some() { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!("true".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), + json!("${OPENSEARCH_PATH_CONF}/tls/rest/tls.crt".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), + json!("${OPENSEARCH_PATH_CONF}/tls/rest/tls.key".to_string()), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!("${OPENSEARCH_PATH_CONF}/tls/rest/ca.crt".to_string()), + ); + } else { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!("false".to_string()), + ); + } + config } diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index a482029..eea6de4 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -72,7 +72,8 @@ constant!(DATA_VOLUME_NAME: VolumeName = "data"); constant!(LISTENER_VOLUME_NAME: PersistentVolumeClaimName = "listener"); const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; -constant!(TLS_VOLUME_NAME: VolumeName = "tls"); +constant!(TLS_REST_VOLUME_NAME: VolumeName = "tls-rest"); +constant!(TLS_TRANSPORT_VOLUME_NAME: VolumeName = "tls-transport"); constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; @@ -215,10 +216,7 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the [`PodTemplateSpec`] for the role-group [`StatefulSet`] fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); - let service_scopes = vec![ - self.resource_names.cluster_name.to_string(), - self.resource_names.headless_service_name().to_string(), - ]; + let service_scopes = vec![self.node_config.discovery_service_name.clone()]; for node_role in self.role_group_config.config.node_roles.iter() { node_role_labels.insert(Self::build_node_role_label(node_role)); @@ -293,12 +291,21 @@ impl<'a> RoleGroupBuilder<'a> { }), ..Volume::default() }, + build_tls_volume( + &TLS_TRANSPORT_VOLUME_NAME.to_string(), + &self.cluster.cluster_config.tls.transport_secret_class, + service_scopes.clone(), + SecretFormat::TlsPem, + &self.role_group_config.config.requested_secret_lifetime, + Some(&LISTENER_VOLUME_NAME.to_string()), + ), ]; - if let Some(tls_secret_class_name) = &self.cluster.cluster_config.tls.secret_class { + if let Some(tls_rest_secret_class_name) = &self.cluster.cluster_config.tls.rest_secret_class + { volumes.push(build_tls_volume( - &TLS_VOLUME_NAME.to_string(), - tls_secret_class_name, + &TLS_REST_VOLUME_NAME.to_string(), + tls_rest_secret_class_name, service_scopes, SecretFormat::TlsPem, &self.role_group_config.config.requested_secret_lifetime, @@ -457,12 +464,17 @@ impl<'a> RoleGroupBuilder<'a> { name: LOG_VOLUME_NAME.to_string(), ..VolumeMount::default() }, + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/transport"), + name: TLS_TRANSPORT_VOLUME_NAME.to_string(), + ..VolumeMount::default() + }, ]; - if self.cluster.cluster_config.tls.secret_class.is_some() { + if self.cluster.cluster_config.tls.rest_secret_class.is_some() { volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls"), - name: TLS_VOLUME_NAME.to_string(), + mount_path: format!("{opensearch_path_conf}/tls/rest"), + name: TLS_REST_VOLUME_NAME.to_string(), ..VolumeMount::default() }) } diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index e878b4d..7fd1eb1 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -74,9 +74,6 @@ pub enum Error { source: crate::framework::product_logging::framework::Error, }, - #[snafu(display("failed to set tls secret class"))] - ParseTlsSecretClassName { source: crate::framework::Error }, - #[snafu(display("fragment validation failure"))] ValidateOpenSearchConfig { source: stackable_operator::config::fragment::ValidationError, @@ -282,7 +279,7 @@ mod tests { }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, - OperatorName, ProductName, ProductVersion, RoleGroupName, TlsSecretClassName, + OperatorName, ProductName, ProductVersion, RoleGroupName, SecretClassName, builder::pod::container::{EnvVarName, EnvVarSet}, product_logging::framework::{ ValidatedContainerLogConfigChoice, VectorContainerLogConfig, @@ -311,7 +308,8 @@ mod tests { uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), OpenSearchClusterConfig { tls: OpenSearchTls { - secret_class: Some(TlsSecretClassName::from_str_unsafe("tls")) + rest_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + transport_secret_class: SecretClassName::from_str_unsafe("tls") }, vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( "vector-aggregator" @@ -678,7 +676,8 @@ mod tests { .expect("should be a valid ProductImage structure"), cluster_config: v1alpha1::OpenSearchClusterConfig { tls: OpenSearchTls { - secret_class: Some(TlsSecretClassName::from_str_unsafe("tls")), + rest_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + transport_secret_class: SecretClassName::from_str_unsafe("tls"), }, vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( "vector-aggregator", diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 92b399a..49a6d0c 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -30,11 +30,12 @@ use crate::{ constant, framework::{ ClusterName, ConfigMapName, ContainerName, ListenerClassName, NameIsValidLabelValue, - ProductName, RoleName, TlsSecretClassName, role_utils::GenericProductSpecificCommonConfig, + ProductName, RoleName, SecretClassName, role_utils::GenericProductSpecificCommonConfig, }, }; constant!(DEFAULT_LISTENER_CLASS: ListenerClassName = "cluster-internal"); +constant!(TLS_DEFAULT_SECRET_CLASS: SecretClassName = "tls"); #[versioned( version(name = "v1alpha1"), @@ -80,6 +81,8 @@ pub mod versioned { #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OpenSearchClusterConfig { + /// TLS configuration options for the REST API and internal communication (transport). + #[serde(default)] pub tls: OpenSearchTls, /// Name of the Vector aggregator [discovery ConfigMap](DOCS_BASE_URL_PLACEHOLDER/concepts/service_discovery). /// It must contain the key `ADDRESS` with the address of the Vector aggregator. @@ -89,14 +92,24 @@ pub mod versioned { pub vector_aggregator_config_map_name: Option, } - #[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OpenSearchTls { - /// Affects client connections and internal transport connections. + /// Only affects client connections to the REST API. /// This setting controls: /// - If TLS encryption is used at all /// - Which cert the servers should use to authenticate themselves against the client - pub secret_class: Option, + #[serde( + default = "rest_secret_class_default", + skip_serializing_if = "Option::is_none" + )] + pub rest_secret_class: Option, + /// Only affects internal communication (transport). Used for mutual verification between OpenSearch nodes. + /// This setting controls: + /// - Which cert the servers should use to authenticate themselves against other servers + /// - Which ca.crt to use when validating the other server + #[serde(default = "transport_secret_class_default")] + pub transport_secret_class: SecretClassName, } // The possible node roles are by default the built-in roles and the search role, see @@ -320,6 +333,23 @@ impl v1alpha1::OpenSearchConfig { } } +impl Default for v1alpha1::OpenSearchTls { + fn default() -> Self { + v1alpha1::OpenSearchTls { + rest_secret_class: rest_secret_class_default(), + transport_secret_class: transport_secret_class_default(), + } + } +} + +fn rest_secret_class_default() -> Option { + Some(TLS_DEFAULT_SECRET_CLASS.to_owned()) +} + +fn transport_secret_class_default() -> SecretClassName { + TLS_DEFAULT_SECRET_CLASS.to_owned() +} + #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct NodeRoles(pub Vec); diff --git a/rust/operator-binary/src/framework.rs b/rust/operator-binary/src/framework.rs index 9b03c46..f4f3cbf 100644 --- a/rust/operator-binary/src/framework.rs +++ b/rust/operator-binary/src/framework.rs @@ -79,9 +79,6 @@ pub enum Error { /// Duplicates the private constant [`stackable-operator::kvp::label::value::LABEL_VALUE_MAX_LEN`] pub const MAX_LABEL_VALUE_LENGTH: usize = 63; -/// Maximum length of annotation values -pub const MAX_ANNOTATION_LENGTH: usize = 253; - /// Has a non-empty name /// /// Useful as an object reference; Should not be used to create an object because the name could @@ -516,11 +513,12 @@ attributed_string_type! { is_valid_label_value } attributed_string_type! { - TlsSecretClassName, + SecretClassName, "The TLS SecretClass name", "tls", - // The secret class name is used in an annotation on the tls volume. To make sure the - (max_length = MAX_ANNOTATION_LENGTH - 30) + // The secret class name is used in an annotation on the tls volume. + (max_length = RFC_1123_SUBDOMAIN_MAX_LENGTH), + is_rfc_1123_dns_subdomain_name } #[cfg(test)] mod tests { diff --git a/rust/operator-binary/src/framework/builder/volume.rs b/rust/operator-binary/src/framework/builder/volume.rs index b0ac896..944b900 100644 --- a/rust/operator-binary/src/framework/builder/volume.rs +++ b/rust/operator-binary/src/framework/builder/volume.rs @@ -4,12 +4,12 @@ use stackable_operator::{ shared::time::Duration, }; -use crate::framework::TlsSecretClassName; +use crate::framework::{SecretClassName, ServiceName}; pub fn build_tls_volume( volume_name: &String, - tls_secret_class_name: &TlsSecretClassName, - service_scopes: impl IntoIterator>, + tls_secret_class_name: &SecretClassName, + service_scopes: Vec, secret_format: SecretFormat, requested_secret_lifetime: &Duration, listener_scope: Option<&str>, @@ -18,7 +18,7 @@ pub fn build_tls_volume( SecretOperatorVolumeSourceBuilder::new(tls_secret_class_name); for scope in service_scopes { - secret_volume_source_builder.with_service_scope(scope.as_ref()); + secret_volume_source_builder.with_service_scope(scope); } if let Some(listener_scope) = listener_scope { secret_volume_source_builder.with_listener_volume_scope(listener_scope); diff --git a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 index de7d10b..e0ad9c9 100644 --- a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 @@ -82,26 +82,10 @@ spec: - name: security-config mountPath: /stackable/opensearch/config/opensearch-security readOnly: true - - name: tls - mountPath: /stackable/opensearch/config/tls - readOnly: true volumes: - name: security-config secret: secretName: opensearch-security-config - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" --- apiVersion: v1 kind: Secret From 8257b95d4a5660837e7dbd9e2f40b1084c5df0d7 Mon Sep 17 00:00:00 2001 From: Benedikt Labrenz Date: Thu, 13 Nov 2025 16:39:50 +0100 Subject: [PATCH 15/15] address feedback on PR --- .../helm/opensearch-operator/crds/crds.yaml | 6 +- .../pages/usage-guide/security.adoc | 55 +++++++ rust/operator-binary/src/controller.rs | 14 +- rust/operator-binary/src/controller/build.rs | 6 +- .../src/controller/build/node_config.rs | 52 ++++-- .../src/controller/build/role_builder.rs | 6 +- .../controller/build/role_group_builder.rs | 155 +++++++++++++----- .../src/controller/validate.rs | 24 +-- rust/operator-binary/src/crd/mod.rs | 10 +- rust/operator-binary/src/framework/builder.rs | 1 - .../src/framework/builder/volume.rs | 37 ----- .../kuttl/external-access/opensearch.yaml.j2 | 3 - .../20_opensearch-security-config.yaml.j2 | 4 +- .../kuttl/ldap/21-install-opensearch.yaml.j2 | 4 +- .../logging/20-install-opensearch.yaml.j2 | 2 - .../metrics/20-install-opensearch.yaml.j2 | 4 +- .../10-install-opensearch.yaml.j2 | 4 +- .../20-install-opensearch-dashboards.yaml.j2 | 4 + .../20_opensearch-dashboards-values.yaml.j2 | 6 + tests/templates/kuttl/smoke/10-assert.yaml.j2 | 96 ++++++++--- .../kuttl/smoke/10-install-opensearch.yaml.j2 | 4 +- ...search.yaml => 20-test-opensearch.yaml.j2} | 7 +- .../snapshot-s3/20-install-opensearch.yaml.j2 | 4 +- tests/test-definition.yaml | 6 + 24 files changed, 351 insertions(+), 163 deletions(-) create mode 100644 docs/modules/opensearch/pages/usage-guide/security.adoc delete mode 100644 rust/operator-binary/src/framework/builder/volume.rs rename tests/templates/kuttl/smoke/{20-test-opensearch.yaml => 20-test-opensearch.yaml.j2} (95%) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index d483c41..e273c8e 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -31,17 +31,17 @@ spec: clusterConfig: default: tls: - restSecretClass: tls + httpSecretClass: tls transportSecretClass: tls description: Configuration that applies to all roles and role groups properties: tls: default: - restSecretClass: tls + httpSecretClass: tls transportSecretClass: tls description: TLS configuration options for the REST API and internal communication (transport). properties: - restSecretClass: + httpSecretClass: default: tls description: |- Only affects client connections to the REST API. diff --git a/docs/modules/opensearch/pages/usage-guide/security.adoc b/docs/modules/opensearch/pages/usage-guide/security.adoc new file mode 100644 index 0000000..7297327 --- /dev/null +++ b/docs/modules/opensearch/pages/usage-guide/security.adoc @@ -0,0 +1,55 @@ += Security +:description: Configure TLS encryption, authentication, and Open Policy Agent (OPA) authorization for Kafka with the Stackable Operator. + +== TLS + +The internal and client communication at the REST API can be encrypted with TLS. This requires the xref:secret-operator:index.adoc[Secret Operator] to be running in the Kubernetes cluster providing certificates. +The used certificates can be changed in a cluster-wide config. TLS encryption on the REST API may be disabled, while it is always enabled for the internal communication between nodes using the `transport` port. + +[source,yaml] +---- +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: + productVersion: 3.1.0 + clusterConfig: + tls: + httpSecretClass: tls # <1> + transportSecretClass: opensearch-transport-tls # <2> + nodes: + config: + requestedSecretLifetime: 7d # <3> + roleGroups: + default: + replicas: 3 +---- +<1> The `spec.clusterConfig.tls.httpSecretClass` refers to the client-to-server encryption at the REST API. Defaults to the `tls` SecretClass and can be disabled by setting `httpSecretClass` to `null`. +<2> The `spec.clusterConfig.tls.transportSecretClass` refers to the internal encryption between OpenSearch nodes using mTLS (transport). Defaults to the `tls` SecretClass` and can't be disabled. +<3> The lifetime for autoTls certificates generated by the secret operator. + Only a lifetime up to the `maxCertificateLifetime` setting in the SecretClass is applied. + +The `tls` secret is deployed from the xref:secret-operator:index.adoc[Secret Operator] and looks like this: + +[source,yaml] +---- +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: SecretClass +metadata: + name: tls +spec: + backend: + autoTls: + ca: + secret: + name: secret-provisioner-tls-ca + namespace: default + autoGenerate: true + maxCertificateLifetime: 15d +---- + +You can create your own secrets and reference them e.g. in the `spec.clusterConfig.tls.httpSecretClass` or `spec.clusterConfig.tls.transportSecretClass` to use different certificates. diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 3c3a88d..843d349 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -30,7 +30,7 @@ use validate::validate; use crate::{ crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ControllerName, HasName, HasUid, ListenerClassName, NameIsValidLabelValue, @@ -165,9 +165,9 @@ pub struct ValidatedCluster { pub name: ClusterName, pub namespace: NamespaceName, pub uid: Uid, - pub cluster_config: OpenSearchClusterConfig, pub role_config: GenericRoleConfig, pub role_group_configs: BTreeMap, + pub tls_config: OpenSearchTls, } impl ValidatedCluster { @@ -178,9 +178,9 @@ impl ValidatedCluster { name: ClusterName, namespace: NamespaceName, uid: impl Into, - cluster_config: OpenSearchClusterConfig, role_config: GenericRoleConfig, role_group_configs: BTreeMap, + tls_config: OpenSearchTls, ) -> Self { let uid = uid.into(); ValidatedCluster { @@ -195,9 +195,9 @@ impl ValidatedCluster { name, namespace, uid, - cluster_config, role_config, role_group_configs, + tls_config, } } @@ -386,7 +386,7 @@ mod tests { controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ListenerClassName, NamespaceName, OperatorName, ProductVersion, @@ -469,7 +469,6 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), - OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [ ( @@ -504,6 +503,7 @@ mod tests { ), ] .into(), + OpenSearchTls::default(), ) } @@ -523,7 +523,7 @@ mod tests { vector_container: None, }, node_roles: NodeRoles(node_roles.to_vec()), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), termination_grace_period_seconds: 120, diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index bc1da16..e95ba33 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -80,7 +80,7 @@ mod tests { }, crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, @@ -172,7 +172,6 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), - OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [ ( @@ -196,6 +195,7 @@ mod tests { ), ] .into(), + OpenSearchTls::default(), ) } @@ -215,7 +215,7 @@ mod tests { vector_container: None, }, node_roles: NodeRoles(node_roles.to_vec()), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), termination_grace_period_seconds: 120, diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 83d67d9..a971e4b 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -93,6 +93,8 @@ pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH: &str = pub const CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH: &str = "plugins.security.ssl.transport.pemtrustedcas_filepath"; +const DEFAULT_OPENSEARCH_HOME: &str = "/stackable/opensearch"; + /// Configuration of an OpenSearch node based on the cluster and role-group configuration pub struct NodeConfig { cluster: ValidatedCluster, @@ -173,6 +175,7 @@ impl NodeConfig { pub fn tls_config(&self) -> serde_json::Map { let mut config = serde_json::Map::new(); + let opensearch_path_conf = self.opensearch_path_conf(); // TLS config for TRANSPORT port which is always enabled. config.insert( @@ -181,34 +184,34 @@ impl NodeConfig { ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/transport/tls.crt".to_string()), + json!(format!("{opensearch_path_conf}/tls/transport/tls.crt")), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/transport/tls.key".to_string()), + json!(format!("{opensearch_path_conf}/tls/transport/tls.key")), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/transport/ca.crt".to_string()), + json!(format!("{opensearch_path_conf}/tls/transport/ca.crt")), ); // TLS config for HTTP port which is optional. - if self.cluster.cluster_config.tls.rest_secret_class.is_some() { + if self.cluster.tls_config.http_secret_class.is_some() { config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), json!("true".to_string()), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/rest/tls.crt".to_string()), + json!(format!("{opensearch_path_conf}/tls/http/tls.crt")), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/rest/tls.key".to_string()), + json!(format!("{opensearch_path_conf}/tls/http/tls.key")), ); config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!("${OPENSEARCH_PATH_CONF}/tls/rest/ca.crt".to_string()), + json!(format!("{opensearch_path_conf}/tls/http/ca.crt")), ); } else { config.insert( @@ -353,6 +356,23 @@ impl NodeConfig { String::new() } } + + /// Return content of the `OPENSEARCH_HOME` environment variable from envOverrides or default to `DEFAULT_OPENSEARCH_HOME` + pub fn opensearch_home(&self) -> String { + self.environment_variables() + .get(&EnvVarName::from_str_unsafe("OPENSEARCH_HOME")) + .and_then(|env_var| env_var.value.clone()) + .unwrap_or(DEFAULT_OPENSEARCH_HOME.to_owned()) + } + + /// Return content of the `OPENSEARCH_PATH_CONF` environment variable from envOverrides or default to `OPENSEARCH_HOME/config` + pub fn opensearch_path_conf(&self) -> String { + let opensearch_home = self.opensearch_home(); + self.environment_variables() + .get(&EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF")) + .and_then(|env_var| env_var.value.clone()) + .unwrap_or(format!("{opensearch_home}/config")) + } } #[cfg(test)] @@ -376,7 +396,7 @@ mod tests { use super::*; use crate::{ controller::{ValidatedLogging, ValidatedOpenSearchConfig}, - crd::{NodeRoles, v1alpha1::OpenSearchClusterConfig}, + crd::{NodeRoles, v1alpha1::OpenSearchTls}, framework::{ ClusterName, ListenerClassName, NamespaceName, ProductVersion, RoleGroupName, product_logging::framework::ValidatedContainerLogConfigChoice, @@ -421,7 +441,7 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, @@ -459,13 +479,13 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), - OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), role_group_config.clone(), )] .into(), + OpenSearchTls::default(), ); NodeConfig::new( @@ -488,7 +508,15 @@ mod tests { "discovery.type: \"zen\"\n", "network.host: \"0.0.0.0\"\n", "plugins.security.nodes_dn: [\"CN=generated certificate for pod\"]\n", - "test: \"value\"" + "plugins.security.ssl.http.enabled: \"true\"\n", + "plugins.security.ssl.http.pemcert_filepath: \"/stackable/opensearch/config/tls/http/tls.crt\"\n", + "plugins.security.ssl.http.pemkey_filepath: \"/stackable/opensearch/config/tls/http/tls.key\"\n", + "plugins.security.ssl.http.pemtrustedcas_filepath: \"/stackable/opensearch/config/tls/http/ca.crt\"\n", + "plugins.security.ssl.transport.enabled: \"true\"\n", + "plugins.security.ssl.transport.pemcert_filepath: \"/stackable/opensearch/config/tls/transport/tls.crt\"\n", + "plugins.security.ssl.transport.pemkey_filepath: \"/stackable/opensearch/config/tls/transport/tls.key\"\n", + "plugins.security.ssl.transport.pemtrustedcas_filepath: \"/stackable/opensearch/config/tls/transport/ca.crt\"\n", + "test: \"value\"", ) .to_owned(), node_config.opensearch_config_file_content() @@ -509,7 +537,7 @@ mod tests { ..TestConfig::default() }); - assert!(!node_config_tls_undefined.tls_on_http_port_enabled()); + assert!(node_config_tls_undefined.tls_on_http_port_enabled()); assert!(node_config_tls_enabled.tls_on_http_port_enabled()); assert!(!node_config_tls_disabled.tls_on_http_port_enabled()); } diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 3199817..aa99d21 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -242,7 +242,7 @@ mod tests { }, crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ControllerName, ListenerClassName, NamespaceName, OperatorName, @@ -280,7 +280,7 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, @@ -305,13 +305,13 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), - OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), role_group_config.clone(), )] .into(), + OpenSearchTls::default(), ); RoleBuilder::new(cluster, context_names) diff --git a/rust/operator-binary/src/controller/build/role_group_builder.rs b/rust/operator-binary/src/controller/build/role_group_builder.rs index eea6de4..b1298f7 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -3,7 +3,10 @@ use std::{collections::BTreeMap, str::FromStr}; use stackable_operator::{ - builder::{meta::ObjectMetaBuilder, pod::volume::SecretFormat}, + builder::{ + meta::ObjectMetaBuilder, + pod::volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + }, crd::listener::{self}, k8s_openapi::{ DeepMerge, @@ -23,6 +26,7 @@ use stackable_operator::{ VECTOR_CONFIG_FILE, calculate_log_volume_size_limit, create_vector_shutdown_file_command, remove_vector_shutdown_file_command, }, + shared::time::Duration, utils::COMMON_BASH_TRAP_FUNCTIONS, }; @@ -42,14 +46,14 @@ use crate::{ }, crd::v1alpha1, framework::{ - PersistentVolumeClaimName, RoleGroupName, ServiceAccountName, ServiceName, VolumeName, + PersistentVolumeClaimName, RoleGroupName, SecretClassName, ServiceAccountName, ServiceName, + VolumeName, builder::{ meta::ownerreference_from_resource, pod::{ - container::{EnvVarName, new_container_builder}, + container::new_container_builder, volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, }, - volume::build_tls_volume, }, kvp::label::{recommended_labels, role_group_selector, role_selector}, product_logging::framework::{ @@ -72,14 +76,12 @@ constant!(DATA_VOLUME_NAME: VolumeName = "data"); constant!(LISTENER_VOLUME_NAME: PersistentVolumeClaimName = "listener"); const LISTENER_VOLUME_DIR: &str = "/stackable/listener"; -constant!(TLS_REST_VOLUME_NAME: VolumeName = "tls-rest"); +constant!(TLS_HTTP_VOLUME_NAME: VolumeName = "tls-http"); constant!(TLS_TRANSPORT_VOLUME_NAME: VolumeName = "tls-transport"); constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; -const DEFAULT_OPENSEARCH_HOME: &str = "/stackable/opensearch"; - /// Builder for role-group resources pub struct RoleGroupBuilder<'a> { service_account_name: ServiceAccountName, @@ -291,21 +293,20 @@ impl<'a> RoleGroupBuilder<'a> { }), ..Volume::default() }, - build_tls_volume( + self.build_tls_volume( &TLS_TRANSPORT_VOLUME_NAME.to_string(), - &self.cluster.cluster_config.tls.transport_secret_class, - service_scopes.clone(), + &self.cluster.tls_config.transport_secret_class, + vec![], SecretFormat::TlsPem, &self.role_group_config.config.requested_secret_lifetime, Some(&LISTENER_VOLUME_NAME.to_string()), ), ]; - if let Some(tls_rest_secret_class_name) = &self.cluster.cluster_config.tls.rest_secret_class - { - volumes.push(build_tls_volume( - &TLS_REST_VOLUME_NAME.to_string(), - tls_rest_secret_class_name, + if let Some(tls_http_secret_class_name) = &self.cluster.tls_config.http_secret_class { + volumes.push(self.build_tls_volume( + &TLS_HTTP_VOLUME_NAME.to_string(), + tls_http_secret_class_name, service_scopes, SecretFormat::TlsPem, &self.role_group_config.config.requested_secret_lifetime, @@ -418,19 +419,8 @@ impl<'a> RoleGroupBuilder<'a> { ..Probe::default() }; - let env_vars = self.node_config.environment_variables(); - - // Use `OPENSEARCH_HOME` from envOverrides or default to `DEFAULT_OPENSEARCH_HOME`. - let opensearch_home = env_vars - .get(&EnvVarName::from_str_unsafe("OPENSEARCH_HOME")) - .and_then(|env_var| env_var.value.clone()) - .unwrap_or(DEFAULT_OPENSEARCH_HOME.to_owned()); - // Use `OPENSEARCH_PATH_CONF` from envOverrides or default to `OPENSEARCH_HOME/config`, - // i.e. depend on `OPENSEARCH_HOME`. - let opensearch_path_conf = env_vars - .get(&EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF")) - .and_then(|env_var| env_var.value.clone()) - .unwrap_or(format!("{opensearch_home}/config")); + let opensearch_home = self.node_config.opensearch_home(); + let opensearch_path_conf = self.node_config.opensearch_path_conf(); let mut volume_mounts = vec![ VolumeMount { @@ -471,10 +461,10 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; - if self.cluster.cluster_config.tls.rest_secret_class.is_some() { + if self.cluster.tls_config.http_secret_class.is_some() { volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/rest"), - name: TLS_REST_VOLUME_NAME.to_string(), + mount_path: format!("{opensearch_path_conf}/tls/http"), + name: TLS_HTTP_VOLUME_NAME.to_string(), ..VolumeMount::default() }) } @@ -506,7 +496,7 @@ impl<'a> RoleGroupBuilder<'a> { create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR), )]) - .add_env_vars(env_vars.into()) + .add_env_vars(self.node_config.environment_variables().into()) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") .add_container_ports(vec![ @@ -666,6 +656,37 @@ impl<'a> RoleGroupBuilder<'a> { &self.role_group_name, ) } + + fn build_tls_volume( + &self, + volume_name: &String, + tls_secret_class_name: &SecretClassName, + service_scopes: Vec, + secret_format: SecretFormat, + requested_secret_lifetime: &Duration, + listener_scope: Option<&str>, + ) -> Volume { + let mut secret_volume_source_builder = + SecretOperatorVolumeSourceBuilder::new(tls_secret_class_name); + + for scope in service_scopes { + secret_volume_source_builder.with_service_scope(scope); + } + if let Some(listener_scope) = listener_scope { + secret_volume_source_builder.with_listener_volume_scope(listener_scope); + } + + VolumeBuilder::new(volume_name) + .ephemeral( + secret_volume_source_builder + .with_pod_scope() + .with_format(secret_format) + .with_auto_tls_cert_lifetime(*requested_secret_lifetime) + .build() + .expect("volume should be built without parse errors"), + ) + .build() + } } #[cfg(test)] @@ -702,7 +723,7 @@ mod tests { }, crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, @@ -765,7 +786,7 @@ mod tests { v1alpha1::NodeRole::Ingest, v1alpha1::NodeRole::RemoteClusterClient, ]), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), termination_grace_period_seconds: 30, @@ -783,13 +804,13 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch-cluster"), NamespaceName::from_str_unsafe("default"), uuid!("0b1e30e6-326e-4c1a-868d-ad6598b49e8b"), - OpenSearchClusterConfig::default(), GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), role_group_config.clone(), )] .into(), + OpenSearchTls::default(), ) } @@ -1064,6 +1085,14 @@ mod tests { { "mountPath": "/stackable/log", "name": "log" + }, + { + "mountPath": "/stackable/opensearch/config/tls/transport", + "name": "tls-transport" + }, + { + "mountPath": "/stackable/opensearch/config/tls/http", + "name": "tls-http", } ] }, @@ -1190,7 +1219,59 @@ mod tests { "sizeLimit": "30Mi" }, "name": "log" - } + }, + { + "ephemeral": { + "volumeClaimTemplate": { + "metadata": { + "annotations": { + "secrets.stackable.tech/backend.autotls.cert.lifetime": "1d", + "secrets.stackable.tech/class": "tls", + "secrets.stackable.tech/format": "tls-pem", + "secrets.stackable.tech/scope": "listener-volume=listener,pod" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-transport" + }, + { + "ephemeral": { + "volumeClaimTemplate": { + "metadata": { + "annotations": { + "secrets.stackable.tech/backend.autotls.cert.lifetime": "1d", + "secrets.stackable.tech/class": "tls", + "secrets.stackable.tech/format": "tls-pem", + "secrets.stackable.tech/scope": "service=my-opensearch-cluster,listener-volume=listener,pod" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-http" + }, ] } }, @@ -1282,7 +1363,7 @@ mod tests { "annotations": { "prometheus.io/path": "/_prometheus/metrics", "prometheus.io/port": "9200", - "prometheus.io/scheme": "http", + "prometheus.io/scheme": "https", "prometheus.io/scrape": "true" }, "labels": { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index c5d265d..f17b009 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -138,9 +138,9 @@ pub fn validate( cluster_name, namespace, uid, - cluster.spec.cluster_config.clone(), cluster.spec.nodes.role_config.clone(), role_group_configs, + cluster.spec.cluster_config.tls.clone(), )) } @@ -276,7 +276,7 @@ mod tests { controller::{ContextNames, ValidatedCluster, ValidatedLogging, ValidatedOpenSearchConfig}, crd::{ NodeRoles, - v1alpha1::{self, OpenSearchClusterConfig, OpenSearchTls}, + v1alpha1::{self, OpenSearchTls}, }, framework::{ ClusterName, ConfigMapName, ControllerName, ListenerClassName, NamespaceName, @@ -313,15 +313,6 @@ mod tests { ClusterName::from_str_unsafe("my-opensearch"), NamespaceName::from_str_unsafe("default"), uuid!("e6ac237d-a6d4-43a1-8135-f36506110912"), - OpenSearchClusterConfig { - tls: OpenSearchTls { - rest_secret_class: Some(SecretClassName::from_str_unsafe("tls")), - transport_secret_class: SecretClassName::from_str_unsafe("tls") - }, - vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( - "vector-aggregator" - )) - }, GenericRoleConfig::default(), [( RoleGroupName::from_str_unsafe("default"), @@ -418,7 +409,7 @@ mod tests { ] .into() ), - requested_secret_lifetime: Duration::from_str("15d") + requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources { memory: MemoryLimits { @@ -505,6 +496,10 @@ mod tests { } )] .into(), + OpenSearchTls { + http_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + transport_secret_class: SecretClassName::from_str_unsafe("tls") + }, )), result.ok() ); @@ -682,10 +677,7 @@ mod tests { image: serde_json::from_str(r#"{"productVersion": "3.1.0"}"#) .expect("should be a valid ProductImage structure"), cluster_config: v1alpha1::OpenSearchClusterConfig { - tls: OpenSearchTls { - rest_secret_class: Some(SecretClassName::from_str_unsafe("tls")), - transport_secret_class: SecretClassName::from_str_unsafe("tls"), - }, + tls: OpenSearchTls::default(), vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( "vector-aggregator", )), diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 49a6d0c..c071fbc 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -100,10 +100,10 @@ pub mod versioned { /// - If TLS encryption is used at all /// - Which cert the servers should use to authenticate themselves against the client #[serde( - default = "rest_secret_class_default", + default = "http_secret_class_default", skip_serializing_if = "Option::is_none" )] - pub rest_secret_class: Option, + pub http_secret_class: Option, /// Only affects internal communication (transport). Used for mutual verification between OpenSearch nodes. /// This setting controls: /// - Which cert the servers should use to authenticate themselves against other servers @@ -302,7 +302,7 @@ impl v1alpha1::OpenSearchConfig { v1alpha1::NodeRole::RemoteClusterClient, ])), requested_secret_lifetime: Some( - Duration::from_str("15d").expect("should be a valid duration"), + Duration::from_str("1d").expect("should be a valid duration"), ), resources: ResourcesFragment { memory: MemoryLimitsFragment { @@ -336,13 +336,13 @@ impl v1alpha1::OpenSearchConfig { impl Default for v1alpha1::OpenSearchTls { fn default() -> Self { v1alpha1::OpenSearchTls { - rest_secret_class: rest_secret_class_default(), + http_secret_class: http_secret_class_default(), transport_secret_class: transport_secret_class_default(), } } } -fn rest_secret_class_default() -> Option { +fn http_secret_class_default() -> Option { Some(TLS_DEFAULT_SECRET_CLASS.to_owned()) } diff --git a/rust/operator-binary/src/framework/builder.rs b/rust/operator-binary/src/framework/builder.rs index 078acfa..40caba1 100644 --- a/rust/operator-binary/src/framework/builder.rs +++ b/rust/operator-binary/src/framework/builder.rs @@ -1,4 +1,3 @@ pub mod meta; pub mod pdb; pub mod pod; -pub mod volume; diff --git a/rust/operator-binary/src/framework/builder/volume.rs b/rust/operator-binary/src/framework/builder/volume.rs deleted file mode 100644 index 944b900..0000000 --- a/rust/operator-binary/src/framework/builder/volume.rs +++ /dev/null @@ -1,37 +0,0 @@ -use stackable_operator::{ - builder::pod::volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, - k8s_openapi::api::core::v1::Volume, - shared::time::Duration, -}; - -use crate::framework::{SecretClassName, ServiceName}; - -pub fn build_tls_volume( - volume_name: &String, - tls_secret_class_name: &SecretClassName, - service_scopes: Vec, - secret_format: SecretFormat, - requested_secret_lifetime: &Duration, - listener_scope: Option<&str>, -) -> Volume { - let mut secret_volume_source_builder = - SecretOperatorVolumeSourceBuilder::new(tls_secret_class_name); - - for scope in service_scopes { - secret_volume_source_builder.with_service_scope(scope); - } - if let Some(listener_scope) = listener_scope { - secret_volume_source_builder.with_listener_volume_scope(listener_scope); - } - - VolumeBuilder::new(volume_name) - .ephemeral( - secret_volume_source_builder - .with_pod_scope() - .with_format(secret_format) - .with_auto_tls_cert_lifetime(*requested_secret_lifetime) - .build() - .expect("volume should be built without parse errors"), - ) - .build() -} diff --git a/tests/templates/kuttl/external-access/opensearch.yaml.j2 b/tests/templates/kuttl/external-access/opensearch.yaml.j2 index 9e232bb..0f9682d 100644 --- a/tests/templates/kuttl/external-access/opensearch.yaml.j2 +++ b/tests/templates/kuttl/external-access/opensearch.yaml.j2 @@ -12,9 +12,6 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent - clusterConfig: - tls: - secretClass: tls {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 b/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 index 6ee2c86..971bfbf 100644 --- a/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 +++ b/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 @@ -58,7 +58,7 @@ stringData: enable_ssl: true hosts: - openldap.$NAMESPACE.svc.cluster.local:1636 - pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt + pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/ca.crt userbase: ou=users,dc=stackable,dc=tech username_attribute: uid usersearch: (cn={0}) @@ -73,7 +73,7 @@ stringData: enable_ssl: true hosts: - openldap.$NAMESPACE.svc.cluster.local:1636 - pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt + pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/ca.crt userbase: ou=users,dc=stackable,dc=tech username_attribute: uid usersearch: (cn={0}) diff --git a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 index 0de3d69..988515f 100644 --- a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 @@ -12,10 +12,8 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent - clusterConfig: - tls: - secretClass: tls {% if lookup('env', 'VECTOR_AGGREGATOR') %} + clusterConfig: vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: diff --git a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 index e0ad9c9..c10385b 100644 --- a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 @@ -27,8 +27,6 @@ spec: {% endif %} pullPolicy: IfNotPresent clusterConfig: - tls: - secretClass: tls vectorAggregatorConfigMapName: opensearch-vector-aggregator-discovery nodes: roleGroups: diff --git a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 index 0de56c2..10d630b 100644 --- a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 @@ -13,8 +13,10 @@ spec: {% endif %} pullPolicy: IfNotPresent clusterConfig: +{% if test_scenario['values']['http-use-tls'] == 'false' %} tls: - secretClass: tls + httpSecretClass: null +{% endif %} {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 index c92794c..912298e 100644 --- a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 @@ -13,8 +13,10 @@ spec: {% endif %} pullPolicy: IfNotPresent clusterConfig: +{% if test_scenario['values']['http-use-tls'] == 'false' %} tls: - secretClass: tls + httpSecretClass: null +{% endif %} {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/opensearch-dashboards/20-install-opensearch-dashboards.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/20-install-opensearch-dashboards.yaml.j2 index 14dc539..4d6d116 100644 --- a/tests/templates/kuttl/opensearch-dashboards/20-install-opensearch-dashboards.yaml.j2 +++ b/tests/templates/kuttl/opensearch-dashboards/20-install-opensearch-dashboards.yaml.j2 @@ -7,7 +7,11 @@ commands: --repo https://opensearch-project.github.io/helm-charts --version "{{ test_scenario['values']['opensearch'].split(',')[0] }}" --values 20_opensearch-dashboards-values.yaml +{% if test_scenario['values']['http-use-tls'] == 'true' %} --set opensearchHosts=https://opensearch.$NAMESPACE.svc.cluster.local:9200 +{% else %} + --set opensearchHosts=http://opensearch.$NAMESPACE.svc.cluster.local:9200 +{% endif %} --namespace $NAMESPACE --wait timeout: 600 diff --git a/tests/templates/kuttl/opensearch-dashboards/20_opensearch-dashboards-values.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/20_opensearch-dashboards-values.yaml.j2 index d1caea0..5d6a5d7 100644 --- a/tests/templates/kuttl/opensearch-dashboards/20_opensearch-dashboards-values.yaml.j2 +++ b/tests/templates/kuttl/opensearch-dashboards/20_opensearch-dashboards-values.yaml.j2 @@ -21,15 +21,18 @@ config: requestHeadersWhitelist: - authorization - securitytenant +{% if test_scenario['values']['http-use-tls'] == 'true' %} ssl: verificationMode: full certificateAuthorities: [/stackable/opensearch-dashboards/config/tls/ca.crt] +{% endif %} opensearch_security: multitenancy: enabled: true tenants.preferred: [Private, Global] readonly_mode.roles: [kibana_read_only] cookie.secure: false +{% if test_scenario['values']['http-use-tls'] == 'true' %} extraVolumes: - name: tls ephemeral: @@ -44,9 +47,12 @@ extraVolumes: resources: requests: storage: "1" +{% endif %} extraVolumeMounts: +{% if test_scenario['values']['http-use-tls'] == 'true' %} - name: tls mountPath: /stackable/opensearch-dashboards/config/tls +{% endif %} # The Helm chart only adds a volume mount at /usr/share/opensearch-dashboards - mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml name: config diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 88e63a8..c1e9d13 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -4,7 +4,7 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 600 +timeout: 120 --- apiVersion: apps/v1 kind: StatefulSet @@ -169,8 +169,12 @@ spec: name: listener - mountPath: /stackable/log name: log - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - name: tls + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/transport + name: tls-transport +{% if test_scenario['values']['http-use-tls'] == 'true' %} + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/http + name: tls-http +{% endif %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config readOnly: true @@ -260,7 +264,25 @@ spec: metadata: annotations: secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch,service=opensearch-nodes-cluster-manager-headless,listener-volume=listener,pod + secrets.stackable.tech/scope: listener-volume=listener,pod + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls-transport +{% if test_scenario['values']['http-use-tls'] == 'true' %} + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/backend.autotls.cert.lifetime: 1d + secrets.stackable.tech/class: tls + secrets.stackable.tech/format: tls-pem + secrets.stackable.tech/scope: service=opensearch,listener-volume=listener,pod spec: accessModes: - ReadWriteOnce @@ -269,7 +291,8 @@ spec: storage: "1" storageClassName: secrets.stackable.tech volumeMode: Filesystem - name: tls + name: tls-http +{% endif %} - name: security-config secret: defaultMode: 0o660 @@ -480,8 +503,12 @@ spec: name: listener - mountPath: /stackable/log name: log - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls - name: tls + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/transport + name: tls-transport +{% if test_scenario['values']['http-use-tls'] == 'true' %} + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/http + name: tls-http +{% endif %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security name: security-config readOnly: true @@ -571,7 +598,7 @@ spec: metadata: annotations: secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch,service=opensearch-nodes-data-headless,listener-volume=listener,pod + secrets.stackable.tech/scope: listener-volume=listener,pod spec: accessModes: - ReadWriteOnce @@ -580,7 +607,26 @@ spec: storage: "1" storageClassName: secrets.stackable.tech volumeMode: Filesystem - name: tls + name: tls-transport +{% if test_scenario['values']['http-use-tls'] == 'true' %} + - ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/backend.autotls.cert.lifetime: 1d + secrets.stackable.tech/class: tls + secrets.stackable.tech/format: tls-pem + secrets.stackable.tech/scope: service=opensearch,listener-volume=listener,pod + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" + storageClassName: secrets.stackable.tech + volumeMode: Filesystem + name: tls-http +{% endif %} - name: security-config secret: defaultMode: 0o660 @@ -653,14 +699,18 @@ data: node.store.allow_mmap: "false" plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] +{% if test_scenario['values']['http-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/ca.crt" +{% else %} + plugins.security.ssl.http.enabled: "false" +{% endif %} plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/ca.crt" --- apiVersion: v1 kind: ConfigMap @@ -688,14 +738,18 @@ data: node.store.allow_mmap: "false" plugins.security.allow_default_init_securityindex: "true" plugins.security.nodes_dn: ["CN=generated certificate for pod"] +{% if test_scenario['values']['http-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: "true" - plugins.security.ssl.http.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" - plugins.security.ssl.http.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" - plugins.security.ssl.http.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" + plugins.security.ssl.http.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/tls.crt" + plugins.security.ssl.http.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/tls.key" + plugins.security.ssl.http.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/http/ca.crt" +{% else %} + plugins.security.ssl.http.enabled: "false" +{% endif %} plugins.security.ssl.transport.enabled: "true" - plugins.security.ssl.transport.pemcert_filepath: "${OPENSEARCH_HOME}/config/tls/tls.crt" - plugins.security.ssl.transport.pemkey_filepath: "${OPENSEARCH_HOME}/config/tls/tls.key" - plugins.security.ssl.transport.pemtrustedcas_filepath: "${OPENSEARCH_HOME}/config/tls/ca.crt" + plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/tls.crt" + plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/tls.key" + plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/transport/ca.crt" --- apiVersion: v1 kind: Service diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 index f141954..23e2679 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 @@ -13,8 +13,10 @@ spec: {% endif %} pullPolicy: IfNotPresent clusterConfig: +{% if test_scenario['values']['http-use-tls'] == 'false' %} tls: - secretClass: tls + httpSecretClass: null +{% endif %} {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 similarity index 95% rename from tests/templates/kuttl/smoke/20-test-opensearch.yaml rename to tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 index fcc3fcc..19afde3 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 @@ -22,6 +22,8 @@ spec: # required for pip install - name: HOME value: /stackable + - name: HTTP_USE_TLS + value: "{{ test_scenario['values']['http-use-tls'] }}" - name: NAMESPACE valueFrom: fieldRef: @@ -78,6 +80,7 @@ data: from opensearchpy import OpenSearch namespace = os.environ['NAMESPACE'] + http_use_tls = os.environ['HTTP_USE_TLS'] == 'true' host = f'opensearch.{namespace}.svc.cluster.local' port = 9200 @@ -89,8 +92,8 @@ data: hosts = [{'host': host, 'port': port}], http_compress = True, # enables gzip compression for request bodies http_auth = auth, - use_ssl = True, - verify_certs = True, + use_ssl = http_use_tls, + verify_certs = http_use_tls, ca_certs = ca_certs_path ) diff --git a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 index 5284693..303374e 100644 --- a/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/snapshot-s3/20-install-opensearch.yaml.j2 @@ -12,10 +12,8 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'] }}" {% endif %} pullPolicy: IfNotPresent - clusterConfig: - tls: - secretClass: tls {% if lookup('env', 'VECTOR_AGGREGATOR') %} + clusterConfig: vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 40792a1..a61f677 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -9,11 +9,16 @@ dimensions: - name: opensearch_home values: - /stackable/opensearch + - name: http-use-tls + values: + - "true" + - "false" tests: - name: smoke dimensions: - opensearch - opensearch_home + - http-use-tls - name: external-access dimensions: - opensearch @@ -34,6 +39,7 @@ tests: dimensions: - opensearch - opensearch_home + - http-use-tls # requires the repository-s3 plugin - name: snapshot-s3 dimensions: