Skip to content

Commit 74b8986

Browse files
feat: Support hot-reloading for security configuration files (#130)
* feat: Support hot-reloading for security configuration files * Fix shellcheck warnings * test(smoke): Fix assertions * fix: Remove broken test script * chore: Improve comments and unit tests * chore: Update changelog * doc: Document hot-reloading of security settings * chore: Improve comments * feat: Add annotations to the StatefulSet to ignore resources with security settings * test(smoke): Fix assertions * fix: Fix the key of the restarter annotation * docs: Remove outdated annotations * docs: Improve upgrade guide * fix: Fix wait_for_shutdown in the update-security-config.sh script * chore: Move restarter annotation code to the framework module; Define the security managing role group in the test explicitly * test(security-config): Explain the replica count
1 parent ffd6292 commit 74b8986

15 files changed

Lines changed: 618 additions & 281 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Support hot-reloading of security configuration files ([#130]).
10+
711
### Changed
812

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

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

1318
## [26.3.0] - 2026-03-16
1419

docs/modules/opensearch/examples/getting_started/opensearch-security-config.yaml

Lines changed: 0 additions & 87 deletions
This file was deleted.

docs/modules/opensearch/pages/usage-guide/monitoring.adoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ To make the metrics accessible for all users, especially Prometheus, anonymous a
2929
----
3030
---
3131
apiVersion: v1
32-
kind: Secret
32+
kind: ConfigMap
3333
metadata:
34-
name: opensearch-security-config
35-
stringData:
34+
name: custom-opensearch-security-config
35+
data:
3636
config.yml: |
3737
---
3838
_meta:

docs/modules/opensearch/pages/usage-guide/security.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ spec:
144144

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

147+
Settings managed by the operator are hot-reloaded when changed, i.e. without pod restarts.
148+
147149
== TLS
148150

149151
TLS is also managed by the OpenSearch security plugin, therefore TLS is only available if the security plugin was not disabled.

docs/modules/opensearch/pages/usage-guide/upgrade.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
= SDP upgrade notes
22
:description: Instructions for upgrading the SDP versions.
33

4+
== Upgrade from SDP 26.3 to 26.7
5+
6+
=== Dedicated ConfigMap for security settings
7+
8+
The security settings defined in the cluster specification are now stored in a separate ConfigMap named `<cluster-name>-security-config`.
9+
If you used this name for your custom security configuration, then you must rename it.
10+
Otherwise the operator will override it.
11+
412
== Upgrade from SDP 25.11 to 26.3
513

614
When upgrading the OpenSearch operator from SDP 25.11 to 26.3, you may encounter several warnings and errors in the operator logs.

rust/operator-binary/src/controller/build.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ pub fn build(names: &ContextNames, cluster: ValidatedCluster) -> KubernetesResou
3333
listeners.push(role_group_builder.build_listener());
3434
}
3535

36-
if let Some(discovery_config_map) = role_builder.build_discovery_config_map() {
36+
if let Some(discovery_config_map) = role_builder.build_maybe_discovery_config_map() {
3737
config_maps.push(discovery_config_map);
3838
}
39+
if let Some(security_config_map) = role_builder.build_maybe_security_config_map() {
40+
config_maps.push(security_config_map);
41+
}
3942
services.push(role_builder.build_seed_nodes_service());
4043
listeners.push(role_builder.build_discovery_service_listener());
4144

@@ -90,7 +93,7 @@ mod tests {
9093
role_utils::GenericProductSpecificCommonConfig,
9194
types::{
9295
common::Port,
93-
kubernetes::{Hostname, ListenerClassName, NamespaceName},
96+
kubernetes::{Hostname, ListenerClassName, NamespaceName, SecretClassName},
9497
operator::{
9598
ClusterName, ControllerName, OperatorName, ProductName, ProductVersion,
9699
RoleGroupName,
@@ -134,7 +137,8 @@ mod tests {
134137
"my-opensearch",
135138
"my-opensearch-nodes-cluster-manager",
136139
"my-opensearch-nodes-coordinating",
137-
"my-opensearch-nodes-data"
140+
"my-opensearch-nodes-data",
141+
"my-opensearch-security-config"
138142
],
139143
extract_resource_names(&resources.config_maps)
140144
);
@@ -209,7 +213,11 @@ mod tests {
209213
),
210214
]
211215
.into(),
212-
ValidatedSecurity::Disabled,
216+
ValidatedSecurity::ManagedByApi {
217+
settings: v1alpha1::SecuritySettings::default(),
218+
tls_server_secret_class: None,
219+
tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"),
220+
},
213221
vec![],
214222
Some(ValidatedDiscoveryEndpoint {
215223
hostname: Hostname::from_str_unsafe("1.2.3.4"),

rust/operator-binary/src/controller/build/role_builder.rs

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Builder for role resources
22
3-
use std::str::FromStr;
3+
use std::{collections::BTreeMap, str::FromStr};
44

55
use stackable_operator::{
66
builder::meta::ObjectMetaBuilder,
@@ -23,8 +23,9 @@ use stackable_operator::{
2323
use crate::{
2424
controller::{
2525
ContextNames, HTTP_PORT, HTTP_PORT_NAME, TRANSPORT_PORT, TRANSPORT_PORT_NAME,
26-
ValidatedCluster, build::role_group_builder::RoleGroupBuilder,
26+
ValidatedCluster, ValidatedSecurity, build::role_group_builder::RoleGroupBuilder,
2727
},
28+
crd::v1alpha1,
2829
framework::{
2930
NameIsValidLabelValue,
3031
builder::{
@@ -166,7 +167,7 @@ impl<'a> RoleBuilder<'a> {
166167
/// The discovery endpoint is derived from the status of the discovery service Listener. If the
167168
/// status is not set yet, the reconciliation process will occur again once the Listener status
168169
/// is updated, leading to the eventual creation of the discovery ConfigMap.
169-
pub fn build_discovery_config_map(&self) -> Option<ConfigMap> {
170+
pub fn build_maybe_discovery_config_map(&self) -> Option<ConfigMap> {
170171
let discovery_endpoint = self.cluster.discovery_endpoint.as_ref()?;
171172

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

208+
/// Builds the [`ConfigMap`] containing the security configuration files that were defined by
209+
/// value.
210+
///
211+
/// Returns `None` if the security plugin is disabled or all configuration files are
212+
/// references.
213+
pub fn build_maybe_security_config_map(&self) -> Option<ConfigMap> {
214+
let metadata = self.common_metadata(security_config_map_name(&self.cluster.name));
215+
216+
let mut data = BTreeMap::new();
217+
218+
if let ValidatedSecurity::ManagedByApi { settings, .. }
219+
| ValidatedSecurity::ManagedByOperator { settings, .. } = &self.cluster.security
220+
{
221+
for file_type in settings {
222+
if let v1alpha1::SecuritySettingsFileTypeContent::Value(
223+
v1alpha1::SecuritySettingsFileTypeContentValue { value },
224+
) = &file_type.content
225+
{
226+
data.insert(file_type.filename.to_owned(), value.to_string());
227+
}
228+
}
229+
}
230+
231+
if data.is_empty() {
232+
None
233+
} else {
234+
Some(ConfigMap {
235+
metadata,
236+
data: Some(data),
237+
..ConfigMap::default()
238+
})
239+
}
240+
}
241+
207242
/// Builds a [`PodDisruptionBudget`] used by all role-groups
208243
pub fn build_pdb(&self) -> Option<PodDisruptionBudget> {
209244
let pdb_config = &self.cluster.role_config.common.pod_disruption_budget;
@@ -297,6 +332,20 @@ fn discovery_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
297332
ConfigMapName::from_str(cluster_name.as_ref()).expect("should be a valid ConfigMap name")
298333
}
299334

335+
pub fn security_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
336+
const SUFFIX: &str = "-security-config";
337+
338+
// compile-time checks
339+
const _: () = assert!(
340+
ClusterName::MAX_LENGTH + SUFFIX.len() <= ConfigMapName::MAX_LENGTH,
341+
"The string `<cluster_name>-security-config` must not exceed the limit of ConfigMap names."
342+
);
343+
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;
344+
345+
ConfigMapName::from_str(&format!("{}{SUFFIX}", cluster_name.as_ref()))
346+
.expect("should be a valid ConfigMap name")
347+
}
348+
300349
pub fn discovery_service_listener_name(cluster_name: &ClusterName) -> ListenerName {
301350
// compile-time checks
302351
const _: () = assert!(
@@ -640,12 +689,13 @@ mod tests {
640689
}
641690

642691
#[test]
643-
fn test_build_discovery_config_map() {
692+
fn test_build_maybe_discovery_config_map() {
644693
let context_names = context_names();
645694
let role_builder = role_builder(&context_names);
646695

647-
let discovery_config_map = serde_json::to_value(role_builder.build_discovery_config_map())
648-
.expect("should be serializable");
696+
let discovery_config_map =
697+
serde_json::to_value(role_builder.build_maybe_discovery_config_map())
698+
.expect("should be serializable");
649699

650700
assert_eq!(
651701
json!({
@@ -683,6 +733,56 @@ mod tests {
683733
);
684734
}
685735

736+
#[test]
737+
fn test_build_maybe_security_config_map() {
738+
let context_names = context_names();
739+
let role_builder = role_builder(&context_names);
740+
741+
let security_config_map =
742+
serde_json::to_value(role_builder.build_maybe_security_config_map())
743+
.expect("should be serializable");
744+
745+
assert_eq!(
746+
json!({
747+
"apiVersion": "v1",
748+
"kind": "ConfigMap",
749+
"metadata": {
750+
"labels": {
751+
"app.kubernetes.io/component": "nodes",
752+
"app.kubernetes.io/instance": "my-opensearch-cluster",
753+
"app.kubernetes.io/managed-by": "opensearch.stackable.tech_opensearchcluster",
754+
"app.kubernetes.io/name": "opensearch",
755+
"app.kubernetes.io/version": "3.4.0",
756+
"stackable.tech/vendor": "Stackable",
757+
},
758+
"name": "my-opensearch-cluster-security-config",
759+
"namespace": "default",
760+
"ownerReferences": [
761+
{
762+
"apiVersion": "opensearch.stackable.tech/v1alpha1",
763+
"controller": true,
764+
"kind": "OpenSearchCluster",
765+
"name": "my-opensearch-cluster",
766+
"uid": "0b1e30e6-326e-4c1a-868d-ad6598b49e8b",
767+
},
768+
],
769+
},
770+
"data": {
771+
"action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}",
772+
"allowlist.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"allowlist\"},\"config\":{\"enabled\":false}}",
773+
"audit.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"audit\"},\"config\":{\"enabled\":false}}",
774+
"config.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"config\"},\"config\":{\"dynamic\":{\"authc\":{},\"authz\":{},\"http\":{}}}}",
775+
"internal_users.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"internalusers\"}}",
776+
"nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}",
777+
"roles.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"roles\"}}",
778+
"roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}",
779+
"tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}",
780+
},
781+
}),
782+
security_config_map
783+
);
784+
}
785+
686786
#[test]
687787
fn test_build_pdb() {
688788
let context_names = context_names();

0 commit comments

Comments
 (0)