Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
28b1f27
feat: Support hot-reloading for security configuration files
siegfriedweber Mar 27, 2026
e5d2574
Fix shellcheck warnings
siegfriedweber Mar 31, 2026
75d3683
Merge branch 'main' into feat/security-config-hot-reloading
siegfriedweber Mar 31, 2026
12f20b1
test(smoke): Fix assertions
siegfriedweber Mar 31, 2026
86eb855
fix: Remove broken test script
siegfriedweber Mar 31, 2026
e7fa7be
chore: Improve comments and unit tests
siegfriedweber Mar 31, 2026
c730d83
chore: Update changelog
siegfriedweber Mar 31, 2026
8000c6d
doc: Document hot-reloading of security settings
siegfriedweber Mar 31, 2026
a7dfe3c
chore: Improve comments
siegfriedweber Mar 31, 2026
bcd1480
feat: Add annotations to the StatefulSet to ignore resources with sec…
siegfriedweber Apr 13, 2026
0c51fd2
test(smoke): Fix assertions
siegfriedweber Apr 14, 2026
dd4c2f2
fix: Fix the key of the restarter annotation
siegfriedweber Apr 14, 2026
b03ba92
docs: Remove outdated annotations
siegfriedweber Apr 14, 2026
9d4680b
docs: Improve upgrade guide
siegfriedweber Apr 14, 2026
334d42f
Merge branch 'main' into feat/security-config-hot-reloading
siegfriedweber Apr 14, 2026
3eef786
Merge branch 'main' into feat/security-config-hot-reloading
siegfriedweber Apr 15, 2026
9ca72d2
fix: Fix wait_for_shutdown in the update-security-config.sh script
siegfriedweber Apr 16, 2026
9d130e7
chore: Move restarter annotation code to the framework module; Define…
siegfriedweber Apr 17, 2026
d3698d4
test(security-config): Explain the replica count
siegfriedweber Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added

- Support hot-reloading of security configuration files ([#130]).

### Changed

- Document Helm deployed RBAC permissions and remove unnecessary permissions ([#129]).

[#129]: https://github.com/stackabletech/opensearch-operator/pull/129
[#130]: https://github.com/stackabletech/opensearch-operator/pull/130

## [26.3.0] - 2026-03-16

Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions docs/modules/opensearch/pages/usage-guide/monitoring.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ To make the metrics accessible for all users, especially Prometheus, anonymous a
----
---
apiVersion: v1
kind: Secret
kind: ConfigMap
metadata:
name: opensearch-security-config
stringData:
name: custom-opensearch-security-config
Copy link
Copy Markdown
Member Author

@siegfriedweber siegfriedweber Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operator creates now a ConfigMap named <cluster-name>-security-config, see the upgrade guide. Therefore, another name is used here.

data:
config.yml: |
---
_meta:
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/opensearch/pages/usage-guide/security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ spec:

If this role group is not defined, it will be created by the operator.

Settings managed by the operator are hot-reloaded when changed, i.e. without pod restarts.

== TLS

TLS is also managed by the OpenSearch security plugin, therefore TLS is only available if the security plugin was not disabled.
Expand Down
8 changes: 8 additions & 0 deletions docs/modules/opensearch/pages/usage-guide/upgrade.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
= SDP upgrade notes
:description: Instructions for upgrading the SDP versions.

== Upgrade from SDP 26.3 to 26.7

=== Dedicated ConfigMap for security settings

The security settings defined in the cluster specification are now stored in a separate ConfigMap named `<cluster-name>-security-config`.
If you used this name for your custom security configuration, then you must rename it.
Otherwise the operator will override it.

== Upgrade from SDP 25.11 to 26.3

When upgrading the OpenSearch operator from SDP 25.11 to 26.3, you may encounter several warnings and errors in the operator logs.
Expand Down
16 changes: 12 additions & 4 deletions rust/operator-binary/src/controller/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou
listeners.push(role_group_builder.build_listener());
}

if let Some(discovery_config_map) = role_builder.build_discovery_config_map() {
if let Some(discovery_config_map) = role_builder.build_maybe_discovery_config_map() {
config_maps.push(discovery_config_map);
}
if let Some(security_config_map) = role_builder.build_maybe_security_config_map() {
config_maps.push(security_config_map);
}
services.push(role_builder.build_seed_nodes_service());
listeners.push(role_builder.build_discovery_service_listener());

Expand Down Expand Up @@ -90,7 +93,7 @@ mod tests {
role_utils::GenericProductSpecificCommonConfig,
types::{
common::Port,
kubernetes::{Hostname, ListenerClassName, NamespaceName},
kubernetes::{Hostname, ListenerClassName, NamespaceName, SecretClassName},
operator::{
ClusterName, ControllerName, OperatorName, ProductName, ProductVersion,
RoleGroupName,
Expand Down Expand Up @@ -134,7 +137,8 @@ mod tests {
"my-opensearch",
"my-opensearch-nodes-cluster-manager",
"my-opensearch-nodes-coordinating",
"my-opensearch-nodes-data"
"my-opensearch-nodes-data",
"my-opensearch-security-config"
],
extract_resource_names(&resources.config_maps)
);
Expand Down Expand Up @@ -209,7 +213,11 @@ mod tests {
),
]
.into(),
ValidatedSecurity::Disabled,
ValidatedSecurity::ManagedByApi {
Comment on lines -212 to +216
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable security to trigger the creation of the security ConfigMap.

settings: v1alpha1::SecuritySettings::default(),
tls_server_secret_class: None,
tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"),
},
vec![],
Some(ValidatedDiscoveryEndpoint {
hostname: Hostname::from_str_unsafe("1.2.3.4"),
Expand Down
112 changes: 106 additions & 6 deletions rust/operator-binary/src/controller/build/role_builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Builder for role resources

use std::str::FromStr;
use std::{collections::BTreeMap, str::FromStr};

use stackable_operator::{
builder::meta::ObjectMetaBuilder,
Expand All @@ -23,8 +23,9 @@ use stackable_operator::{
use crate::{
controller::{
ContextNames, HTTP_PORT, HTTP_PORT_NAME, TRANSPORT_PORT, TRANSPORT_PORT_NAME,
ValidatedCluster, build::role_group_builder::RoleGroupBuilder,
ValidatedCluster, ValidatedSecurity, build::role_group_builder::RoleGroupBuilder,
},
crd::v1alpha1,
framework::{
NameIsValidLabelValue,
builder::{
Expand Down Expand Up @@ -166,7 +167,7 @@ impl<'a> RoleBuilder<'a> {
/// The discovery endpoint is derived from the status of the discovery service Listener. If the
/// status is not set yet, the reconciliation process will occur again once the Listener status
/// is updated, leading to the eventual creation of the discovery ConfigMap.
pub fn build_discovery_config_map(&self) -> Option<ConfigMap> {
pub fn build_maybe_discovery_config_map(&self) -> Option<ConfigMap> {
let discovery_endpoint = self.cluster.discovery_endpoint.as_ref()?;

let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name));
Expand Down Expand Up @@ -204,6 +205,40 @@ impl<'a> RoleBuilder<'a> {
})
}

/// Builds the [`ConfigMap`] containing the security configuration files that were defined by
/// value.
///
/// Returns `None` if the security plugin is disabled or all configuration files are
/// references.
pub fn build_maybe_security_config_map(&self) -> Option<ConfigMap> {
Copy link
Copy Markdown
Member Author

@siegfriedweber siegfriedweber Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security settings which are defined by value, are now written to a dedicated (role-wide) ConfigMap instead of the role group ConfigMaps. This security ConfigMap is then excluded from the restart controller.

let metadata = self.common_metadata(security_config_map_name(&self.cluster.name));

let mut data = BTreeMap::new();

if let ValidatedSecurity::ManagedByApi { settings, .. }
| ValidatedSecurity::ManagedByOperator { settings, .. } = &self.cluster.security
{
for file_type in settings {
if let v1alpha1::SecuritySettingsFileTypeContent::Value(
v1alpha1::SecuritySettingsFileTypeContentValue { value },
) = &file_type.content
{
data.insert(file_type.filename.to_owned(), value.to_string());
}
}
}

if data.is_empty() {
None
} else {
Some(ConfigMap {
metadata,
data: Some(data),
..ConfigMap::default()
})
}
}

/// Builds a [`PodDisruptionBudget`] used by all role-groups
pub fn build_pdb(&self) -> Option<PodDisruptionBudget> {
let pdb_config = &self.cluster.role_config.common.pod_disruption_budget;
Expand Down Expand Up @@ -297,6 +332,20 @@ fn discovery_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
ConfigMapName::from_str(cluster_name.as_ref()).expect("should be a valid ConfigMap name")
}

pub fn security_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
const SUFFIX: &str = "-security-config";

// compile-time checks
const _: () = assert!(
ClusterName::MAX_LENGTH + SUFFIX.len() <= ConfigMapName::MAX_LENGTH,
"The string `<cluster_name>-security-config` must not exceed the limit of ConfigMap names."
);
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;

ConfigMapName::from_str(&format!("{}{SUFFIX}", cluster_name.as_ref()))
.expect("should be a valid ConfigMap name")
}

pub fn discovery_service_listener_name(cluster_name: &ClusterName) -> ListenerName {
// compile-time checks
const _: () = assert!(
Expand Down Expand Up @@ -640,12 +689,13 @@ mod tests {
}

#[test]
fn test_build_discovery_config_map() {
fn test_build_maybe_discovery_config_map() {
let context_names = context_names();
let role_builder = role_builder(&context_names);

let discovery_config_map = serde_json::to_value(role_builder.build_discovery_config_map())
.expect("should be serializable");
let discovery_config_map =
serde_json::to_value(role_builder.build_maybe_discovery_config_map())
.expect("should be serializable");

assert_eq!(
json!({
Expand Down Expand Up @@ -683,6 +733,56 @@ mod tests {
);
}

#[test]
fn test_build_maybe_security_config_map() {
let context_names = context_names();
let role_builder = role_builder(&context_names);

let security_config_map =
serde_json::to_value(role_builder.build_maybe_security_config_map())
.expect("should be serializable");

assert_eq!(
json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"labels": {
"app.kubernetes.io/component": "nodes",
"app.kubernetes.io/instance": "my-opensearch-cluster",
"app.kubernetes.io/managed-by": "opensearch.stackable.tech_opensearchcluster",
"app.kubernetes.io/name": "opensearch",
"app.kubernetes.io/version": "3.4.0",
"stackable.tech/vendor": "Stackable",
},
"name": "my-opensearch-cluster-security-config",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "opensearch.stackable.tech/v1alpha1",
"controller": true,
"kind": "OpenSearchCluster",
"name": "my-opensearch-cluster",
"uid": "0b1e30e6-326e-4c1a-868d-ad6598b49e8b",
},
],
},
"data": {
"action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}",
"allowlist.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"allowlist\"},\"config\":{\"enabled\":false}}",
"audit.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"audit\"},\"config\":{\"enabled\":false}}",
"config.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"config\"},\"config\":{\"dynamic\":{\"authc\":{},\"authz\":{},\"http\":{}}}}",
"internal_users.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"internalusers\"}}",
"nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}",
"roles.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"roles\"}}",
"roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}",
"tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}",
},
}),
security_config_map
);
}

#[test]
fn test_build_pdb() {
let context_names = context_names();
Expand Down
Loading
Loading