From e9e527ff14a79d9b60a2eba7390c1fadf2213e34 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 5 Feb 2026 17:18:03 +0100 Subject: [PATCH 01/53] test: Add security-config integration test --- .../kuttl/security-config/00-patch-ns.yaml | 15 ++ .../kuttl/security-config/01-rbac.yaml | 31 +++ .../kuttl/security-config/02-assert.yaml.j2 | 10 + ...or-aggregator-discovery-config-map.yaml.j2 | 9 + .../security-config/03-create-truststore.yaml | 9 + .../security-config/10-security-config.yaml | 133 ++++++++++ .../kuttl/security-config/11-assert.yaml.j2 | 28 ++ .../11-install-opensearch.yaml | 8 + .../11_install-opensearch.yaml.j2 | 245 ++++++++++++++++++ .../kuttl/security-config/20-assert.yaml | 11 + .../20-test-initial-security-config.yaml | 91 +++++++ .../21-change-security-config.yaml | 28 ++ .../kuttl/security-config/22-assert.yaml | 11 + .../22-test-updated-security-config.yaml | 96 +++++++ tests/test-definition.yaml | 6 + 15 files changed, 731 insertions(+) create mode 100644 tests/templates/kuttl/security-config/00-patch-ns.yaml create mode 100644 tests/templates/kuttl/security-config/01-rbac.yaml create mode 100644 tests/templates/kuttl/security-config/02-assert.yaml.j2 create mode 100644 tests/templates/kuttl/security-config/02-install-vector-aggregator-discovery-config-map.yaml.j2 create mode 100644 tests/templates/kuttl/security-config/03-create-truststore.yaml create mode 100644 tests/templates/kuttl/security-config/10-security-config.yaml create mode 100644 tests/templates/kuttl/security-config/11-assert.yaml.j2 create mode 100644 tests/templates/kuttl/security-config/11-install-opensearch.yaml create mode 100644 tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 create mode 100644 tests/templates/kuttl/security-config/20-assert.yaml create mode 100644 tests/templates/kuttl/security-config/20-test-initial-security-config.yaml create mode 100644 tests/templates/kuttl/security-config/21-change-security-config.yaml create mode 100644 tests/templates/kuttl/security-config/22-assert.yaml create mode 100644 tests/templates/kuttl/security-config/22-test-updated-security-config.yaml diff --git a/tests/templates/kuttl/security-config/00-patch-ns.yaml b/tests/templates/kuttl/security-config/00-patch-ns.yaml new file mode 100644 index 0000000..d4f91fa --- /dev/null +++ b/tests/templates/kuttl/security-config/00-patch-ns.yaml @@ -0,0 +1,15 @@ +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl patch namespace $NAMESPACE --patch=' + { + "metadata": { + "labels": { + "pod-security.kubernetes.io/enforce": "privileged" + } + } + }' + timeout: 120 diff --git a/tests/templates/kuttl/security-config/01-rbac.yaml b/tests/templates/kuttl/security-config/01-rbac.yaml new file mode 100644 index 0000000..64eced8 --- /dev/null +++ b/tests/templates/kuttl/security-config/01-rbac.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-service-account +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role-binding +subjects: + - kind: ServiceAccount + name: test-service-account +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: test-role diff --git a/tests/templates/kuttl/security-config/02-assert.yaml.j2 b/tests/templates/kuttl/security-config/02-assert.yaml.j2 new file mode 100644 index 0000000..50b1d4c --- /dev/null +++ b/tests/templates/kuttl/security-config/02-assert.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +{% endif %} diff --git a/tests/templates/kuttl/security-config/02-install-vector-aggregator-discovery-config-map.yaml.j2 b/tests/templates/kuttl/security-config/02-install-vector-aggregator-discovery-config-map.yaml.j2 new file mode 100644 index 0000000..2d6a0df --- /dev/null +++ b/tests/templates/kuttl/security-config/02-install-vector-aggregator-discovery-config-map.yaml.j2 @@ -0,0 +1,9 @@ +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +data: + ADDRESS: {{ lookup('env', 'VECTOR_AGGREGATOR') }} +{% endif %} diff --git a/tests/templates/kuttl/security-config/03-create-truststore.yaml b/tests/templates/kuttl/security-config/03-create-truststore.yaml new file mode 100644 index 0000000..2d55c6d --- /dev/null +++ b/tests/templates/kuttl/security-config/03-create-truststore.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-pem +spec: + secretClassName: tls + format: tls-pem + targetKind: ConfigMap diff --git a/tests/templates/kuttl/security-config/10-security-config.yaml b/tests/templates/kuttl/security-config/10-security-config.yaml new file mode 100644 index 0000000..14f5c81 --- /dev/null +++ b/tests/templates/kuttl/security-config/10-security-config.yaml @@ -0,0 +1,133 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: initial-security-config +stringData: + action_groups.yml: | + --- + _meta: + type: actiongroups + config_version: 2 + allowlist.yml: | + --- + _meta: + type: allowlist + config_version: 2 + + config: + enabled: false + audit.yml: | + --- + _meta: + type: audit + config_version: 2 + + config: + enabled: false + config.yml: | + --- + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internal_users.yml: | + --- + # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh + + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user + nodes_dn.yml: | + --- + _meta: + type: nodesdn + config_version: 2 + roles.yml: | + --- + _meta: + type: roles + config_version: 2 + + monitoring: + reserved: true + cluster_permissions: + - cluster:monitor/main + roles_mapping.yml: | + --- + _meta: + type: rolesmapping + config_version: 2 + + all_access: + reserved: false + backend_roles: + - admin + + kibana_server: + reserved: true + users: + - kibanaserver + + monitoring: + backend_roles: + - opendistro_security_anonymous_backendrole + tenants.yml: | + --- + _meta: + type: tenants + config_version: 2 +--- +apiVersion: v1 +kind: Secret +metadata: + name: managed-security-config +stringData: + config.yml: | + --- + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + http: + anonymous_auth_enabled: true diff --git a/tests/templates/kuttl/security-config/11-assert.yaml.j2 b/tests/templates/kuttl/security-config/11-assert.yaml.j2 new file mode 100644 index 0000000..63218dd --- /dev/null +++ b/tests/templates/kuttl/security-config/11-assert.yaml.j2 @@ -0,0 +1,28 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: opensearch-nodes-cluster-manager +status: + readyReplicas: 3 + replicas: 3 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: opensearch-nodes-data +status: + readyReplicas: 2 + replicas: 2 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: opensearch-nodes-security-config +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml b/tests/templates/kuttl/security-config/11-install-opensearch.yaml new file mode 100644 index 0000000..3a4bf21 --- /dev/null +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 600 +commands: + - script: > + envsubst '$NAMESPACE' < 11_install-opensearch.yaml | + kubectl apply --namespace=$NAMESPACE --filename=- diff --git a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 new file mode 100644 index 0000000..e1c2930 --- /dev/null +++ b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 @@ -0,0 +1,245 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% endif %} + productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" + pullPolicy: IfNotPresent + clusterConfig: +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleConfig: + discoveryServiceListenerClass: external-unstable + roleGroups: + cluster-manager: + config: + discoveryServiceExposed: true + nodeRoles: + - cluster_manager + resources: + storage: + data: + capacity: 100Mi + replicas: 3 + data: + config: + discoveryServiceExposed: false + nodeRoles: + - ingest + - data + - remote_cluster_client + resources: + storage: + data: + capacity: 2Gi + replicas: 2 + security-config: + config: + discoveryServiceExposed: false + nodeRoles: [] + resources: + storage: + data: + capacity: 100Mi + replicas: 1 + configOverrides: + opensearch.yml: + plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt + podOverrides: + spec: + initContainers: + - name: create-admin-certificate +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% else %} + image: oci.stackable.tech/sdp/opensearch:{{ test_scenario['values']['opensearch'].split(',')[0] }}-stackable{{ test_scenario['values']['release'] }} +{% endif %} + command: + - /bin/bash + - -euxo + - pipefail + - -c + args: + - | + openssl req \ + -x509 \ + -nodes \ + -subj=/CN=update-security-config.localhost \ + -out=/stackable/tls-admin/tls.crt \ + -keyout=/stackable/tls-admin/tls.key + + cat \ + /stackable/tls-server/ca.crt \ + /stackable/tls-admin/tls.crt > \ + /stackable/pemtrustedcas/ca.crt + volumeMounts: + - mountPath: /stackable/tls-server + name: tls-server + readOnly: true + - mountPath: /stackable/tls-admin + name: admin-certificate + readOnly: false + - mountPath: /stackable/pemtrustedcas + name: pemtrustedcas + readOnly: false + containers: + - name: opensearch + volumeMounts: + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas + name: pemtrustedcas + readOnly: true + - name: update-security-config +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% else %} + image: oci.stackable.tech/sdp/opensearch:{{ test_scenario['values']['opensearch'].split(',')[0] }}-stackable{{ test_scenario['values']['release'] }} +{% endif %} + command: + - /bin/bash + - -uo + - pipefail + - -c + args: + - | + function wait_seconds () { + seconds="$1" + + if test "$seconds" = 0 + then + echo "Wait until pod is restarted..." + else + echo "Wait for $seconds seconds..." + fi + + if test ! -e /stackable/log/_vector/shutdown + then + mkdir --parents /stackable/log/_vector + inotifywait \ + --quiet --quiet \ + --timeout $seconds \ + --event create \ + /stackable/log/_vector + fi + + if test -e /stackable/log/_vector/shutdown + then + echo "Shut down" + exit 0 + fi + } + + function update_config () { + filetype="$1" + filename="$2" + + file="{{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security/$filename" + + if test -e "$file" + then + echo "Update managed configuration type \"$filetype\"." + + until plugins/opensearch-security/tools/securityadmin.sh \ + --type "$filetype" \ + --file "$file" \ + --disable-host-name-verification \ + -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt \ + -cert /stackable/tls-admin/tls.crt \ + -key /stackable/tls-admin/tls.key + do + echo "Updating \"$filetype\" in the security index failed." + wait_seconds 10 + done + else + echo "Skip unmanaged configuration type \"$filetype\"." + fi + } + + update_config actiongroups action_groups.yml + update_config allowlist allowlist.yml + update_config audit audit.yml + update_config config config.yml + update_config internalusers internal_users.yml + update_config nodesdn nodes_dn.yml + update_config roles roles.yml + update_config rolesmapping roles_mapping.yml + update_config tenants tenants.yml + + echo "Wait for security configuration changes..." + # Wait until the pod is restarted due to a change of the Secret. + wait_seconds 0 + volumeMounts: + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security + name: managed-security-config + readOnly: true + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas + name: pemtrustedcas + readOnly: true + - mountPath: /stackable/tls-admin + name: admin-certificate + readOnly: true + - mountPath: /stackable/log + name: log + readOnly: false + volumes: + - name: admin-certificate + emptyDir: + sizeLimit: 1Mi + - name: pemtrustedcas + emptyDir: + sizeLimit: 1Mi + envOverrides: + # Only required for the official image + # The official image (built with https://github.com/opensearch-project/opensearch-build) + # installs a demo configuration if not disabled explicitly. + DISABLE_INSTALL_DEMO_CONFIG: "true" + OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} + configOverrides: + opensearch.yml: + # Disable memory mapping in this test; If memory mapping were activated, the kernel setting + # vm.max_map_count would have to be increased to 262144 on the node. + node.store.allow_mmap: "false" + # Disable the disk allocation decider in this test; Otherwise the test depends on the disk + # usage of the node and if the relative watermark set in + # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could + # 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.authcz.admin_dn: CN=update-security-config.localhost + plugins.security.restapi.roles_enabled: all_access + podOverrides: + spec: + containers: + - name: opensearch + volumeMounts: + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security + name: initial-security-config + readOnly: true + volumes: + - name: initial-security-config + secret: + secretName: initial-security-config + defaultMode: 0o660 + objectOverrides: + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: opensearch-nodes-security-config + namespace: $NAMESPACE + spec: + template: + spec: + volumes: + - name: managed-security-config + secret: + secretName: managed-security-config + defaultMode: 0o660 diff --git a/tests/templates/kuttl/security-config/20-assert.yaml b/tests/templates/kuttl/security-config/20-assert.yaml new file mode 100644 index 0000000..ce5e8a8 --- /dev/null +++ b/tests/templates/kuttl/security-config/20-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-initial-security-config +status: + succeeded: 1 diff --git a/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml b/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml new file mode 100644 index 0000000..87b9c8f --- /dev/null +++ b/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-initial-security-config +spec: + template: + spec: + containers: + - name: test-initial-security-config + image: oci.stackable.tech/sdp/testing-tools:0.3.0-stackable0.0.0-dev + command: + - /bin/bash + - -euxo + - pipefail + - -c + args: + - | + pip install opensearch-py==3.1.0 + python scripts/test.py + env: + # required for pip install + - name: HOME + value: /stackable + envFrom: + - configMapRef: + name: opensearch + volumeMounts: + - name: script + mountPath: /stackable/scripts + - name: tls + mountPath: /stackable/tls + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 128Mi + cpu: 400m + volumes: + - name: script + configMap: + name: test-initial-security-config + - name: tls + configMap: + name: truststore-pem + serviceAccountName: test-service-account + securityContext: + fsGroup: 1000 + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-initial-security-config +data: + test.py: | + import os + from opensearchpy import OpenSearch + + host = os.environ['OPENSEARCH_HOSTNAME'] + port = os.environ['OPENSEARCH_PORT'] + http_use_tls = os.environ['OPENSEARCH_PROTOCOL'] == 'https' + + connection_params = { + 'hosts': [{'host': host, 'port': port}], + 'http_compress': True, + 'use_ssl': http_use_tls, + 'verify_certs': True, + 'ca_certs': '/stackable/tls/ca.crt', + } + + admin_client = OpenSearch( + http_auth=('admin', 'AJVFsGJBbpT6mChn'), + **connection_params + ) + + security_configuration = admin_client.security.get_configuration() + assert security_configuration['config']['dynamic']['http']['anonymous_auth_enabled'] == True + + anonymous_client = OpenSearch( + **connection_params + ) + + assert anonymous_client.info()['cluster_name'] == 'opensearch' diff --git a/tests/templates/kuttl/security-config/21-change-security-config.yaml b/tests/templates/kuttl/security-config/21-change-security-config.yaml new file mode 100644 index 0000000..8c4220e --- /dev/null +++ b/tests/templates/kuttl/security-config/21-change-security-config.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: managed-security-config +stringData: + config.yml: | + --- + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + http: + anonymous_auth_enabled: false diff --git a/tests/templates/kuttl/security-config/22-assert.yaml b/tests/templates/kuttl/security-config/22-assert.yaml new file mode 100644 index 0000000..990c02e --- /dev/null +++ b/tests/templates/kuttl/security-config/22-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-updated-security-config +status: + succeeded: 1 diff --git a/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml b/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml new file mode 100644 index 0000000..f7db842 --- /dev/null +++ b/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-updated-security-config +spec: + template: + spec: + containers: + - name: test-updated-security-config + image: oci.stackable.tech/sdp/testing-tools:0.3.0-stackable0.0.0-dev + command: + - /bin/bash + - -euxo + - pipefail + - -c + args: + - | + pip install opensearch-py==3.1.0 + python scripts/test.py + env: + # required for pip install + - name: HOME + value: /stackable + envFrom: + - configMapRef: + name: opensearch + volumeMounts: + - name: script + mountPath: /stackable/scripts + - name: tls + mountPath: /stackable/tls + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 128Mi + cpu: 400m + volumes: + - name: script + configMap: + name: test-updated-security-config + - name: tls + configMap: + name: truststore-pem + serviceAccountName: test-service-account + securityContext: + fsGroup: 1000 + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-updated-security-config +data: + test.py: | + import os + from opensearchpy import OpenSearch + from opensearchpy.exceptions import AuthenticationException + + host = os.environ['OPENSEARCH_HOSTNAME'] + port = os.environ['OPENSEARCH_PORT'] + http_use_tls = os.environ['OPENSEARCH_PROTOCOL'] == 'https' + + connection_params = { + 'hosts': [{'host': host, 'port': port}], + 'http_compress': True, + 'use_ssl': http_use_tls, + 'verify_certs': True, + 'ca_certs': '/stackable/tls/ca.crt', + } + + admin_client = OpenSearch( + http_auth=('admin', 'AJVFsGJBbpT6mChn'), + **connection_params + ) + + security_configuration = admin_client.security.get_configuration() + assert security_configuration['config']['dynamic']['http']['anonymous_auth_enabled'] == False + + anonymous_client = OpenSearch( + **connection_params + ) + + try: + anonymous_client.info() + assert False, "Anonymous authentication should be disabled." + except AuthenticationException as e: + assert e.status_code == 401 diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 649b799..e5ca230 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -55,6 +55,11 @@ tests: - opensearch - release - s3-use-tls + - name: security-config + dimensions: + - opensearch + - opensearch_home + - release suites: - name: nightly patch: @@ -81,6 +86,7 @@ suites: - external-access - ldap - opensearch-dashboards + - security-config patch: - dimensions: - name: opensearch From 419447ccc59e73b93b12929d5e30dcf37cf7fc87 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 9 Feb 2026 17:04:34 +0100 Subject: [PATCH 02/53] feat: Add securityConfig to the CRD; Deploy the initial security configuration --- .../helm/opensearch-operator/crds/crds.yaml | 830 ++++++++++++++++++ rust/operator-binary/src/controller.rs | 4 + rust/operator-binary/src/controller/build.rs | 1 + .../src/controller/build/node_config.rs | 1 + .../src/controller/build/role_builder.rs | 1 + .../controller/build/role_group_builder.rs | 76 +- .../src/controller/validate.rs | 3 + rust/operator-binary/src/crd/mod.rs | 364 +++++++- .../security-config/10-security-config.yaml | 87 +- .../kuttl/security-config/11-assert.yaml.j2 | 26 + .../11_install-opensearch.yaml.j2 | 63 +- .../kuttl/security-config/21-assert.yaml | 27 + .../21-change-security-config.yaml | 39 +- 13 files changed, 1398 insertions(+), 124 deletions(-) create mode 100644 tests/templates/kuttl/security-config/21-assert.yaml diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index bbc9590..05e82a7 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -31,6 +31,80 @@ spec: clusterConfig: default: keystore: [] + securityConfig: + actionGroups: + content: + value: + _meta: + config_version: 2 + type: actiongroups + managedBy: API + allowList: + content: + value: + _meta: + config_version: 2 + type: allowlist + config: + enabled: false + managedBy: API + audit: + content: + value: + _meta: + config_version: 2 + type: audit + config: + enabled: false + managedBy: API + config: + content: + value: + _meta: + config_version: 2 + type: config + config: + dynamic: + authc: {} + authz: {} + http: {} + managedBy: API + enabled: true + internalUsers: + content: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API + nodesDn: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + roles: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API + rolesMapping: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + tenants: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API tls: internalSecretClass: tls serverSecretClass: tls @@ -70,6 +144,762 @@ spec: - secretKeyRef type: object type: array + securityConfig: + default: + actionGroups: + content: + value: + _meta: + config_version: 2 + type: actiongroups + managedBy: API + allowList: + content: + value: + _meta: + config_version: 2 + type: allowlist + config: + enabled: false + managedBy: API + audit: + content: + value: + _meta: + config_version: 2 + type: audit + config: + enabled: false + managedBy: API + config: + content: + value: + _meta: + config_version: 2 + type: config + config: + dynamic: + authc: {} + authz: {} + http: {} + managedBy: API + enabled: true + internalUsers: + content: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API + nodesDn: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + roles: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API + rolesMapping: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + tenants: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API + description: TODO Add description + properties: + actionGroups: + default: + content: + value: + _meta: + config_version: 2 + type: actiongroups + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + allowList: + default: + content: + value: + _meta: + config_version: 2 + type: allowlist + config: + enabled: false + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + audit: + default: + content: + value: + _meta: + config_version: 2 + type: audit + config: + enabled: false + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + config: + default: + content: + value: + _meta: + config_version: 2 + type: config + config: + dynamic: + authc: {} + authz: {} + http: {} + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + enabled: + default: true + type: boolean + internalUsers: + default: + content: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + nodesDn: + default: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + roles: + default: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + rolesMapping: + default: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + tenants: + default: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + type: object tls: default: internalSecretClass: tls diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 133755b..5377119 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -200,6 +200,7 @@ pub struct ValidatedCluster { pub role_config: v1alpha1::OpenSearchRoleConfig, pub role_group_configs: BTreeMap, pub tls_config: v1alpha1::OpenSearchTls, + pub security_config: v1alpha1::SecurityConfig, pub keystores: Vec, pub discovery_endpoint: Option, } @@ -215,6 +216,7 @@ impl ValidatedCluster { role_config: v1alpha1::OpenSearchRoleConfig, role_group_configs: BTreeMap, tls_config: v1alpha1::OpenSearchTls, + security_config: v1alpha1::SecurityConfig, keystores: Vec, discovery_endpoint: Option, ) -> Self { @@ -234,6 +236,7 @@ impl ValidatedCluster { role_config, role_group_configs, tls_config, + security_config, keystores, discovery_endpoint, } @@ -552,6 +555,7 @@ mod tests { ] .into(), v1alpha1::OpenSearchTls::default(), + v1alpha1::SecurityConfig::default(), vec![], None, ) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 9055050..bf7d2ed 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -210,6 +210,7 @@ mod tests { ] .into(), v1alpha1::OpenSearchTls::default(), + v1alpha1::SecurityConfig::default(), vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index de266ce..8c84322 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -602,6 +602,7 @@ mod tests { )] .into(), v1alpha1::OpenSearchTls::default(), + v1alpha1::SecurityConfig::default(), vec![], None, ); diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 2a174e8..f332944 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -428,6 +428,7 @@ mod tests { )] .into(), v1alpha1::OpenSearchTls::default(), + v1alpha1::SecurityConfig::default(), vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), 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 0a1c1bf..b67eeb2 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -32,6 +32,7 @@ use stackable_operator::{ shared::time::Duration, utils::COMMON_BASH_TRAP_FUNCTIONS, }; +use strum::IntoEnumIterator; use super::{ node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}, @@ -48,7 +49,7 @@ use crate::{ MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, }, }, - crd::v1alpha1, + crd::{SecurityConfigFileType, v1alpha1}, framework::{ builder::{ meta::ownerreference_from_resource, @@ -175,6 +176,12 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } + for file_type in SecurityConfigFileType::iter() { + if let Some(value) = self.cluster.security_config.value(file_type) { + data.insert(file_type.filename(), value.to_string()); + } + } + ConfigMap { metadata, data: Some(data), @@ -371,6 +378,59 @@ impl<'a> RoleGroupBuilder<'a> { )) }; + for file_type in SecurityConfigFileType::iter() { + if self.cluster.security_config.value(file_type).is_some() { + let volume = Volume { + name: format!("initial-security-config-{}", file_type.volume_name()), + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: file_type.filename(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: self.resource_names.role_group_config_map().to_string(), + ..Default::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = + self.cluster.security_config.config_map_key_ref(file_type) + { + let volume = Volume { + name: format!("initial-security-config-{}", file_type.volume_name()), + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: name.to_string(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::SecretKeyRef { name, key }) = + self.cluster.security_config.secret_key_ref(file_type) + { + let volume = Volume { + name: format!("initial-security-config-{}", file_type.volume_name()), + secret: Some(SecretVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + secret_name: Some(name.to_string()), + ..SecretVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } + } + if !self.cluster.keystores.is_empty() { volumes.push(Volume { name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), @@ -612,6 +672,19 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }); } + for file_type in SecurityConfigFileType::iter() { + volume_mounts.push(VolumeMount { + mount_path: format!( + "{opensearch_path_conf}/opensearch-security/{filename}", + filename = file_type.filename() + ), + name: format!("initial-security-config-{}", file_type.volume_name()), + read_only: Some(true), + sub_path: Some(file_type.filename()), + ..VolumeMount::default() + }); + } + if !self.cluster.keystores.is_empty() { volume_mounts.push(VolumeMount { mount_path: format!("{opensearch_path_conf}/{OPENSEARCH_KEYSTORE_FILE_NAME}"), @@ -979,6 +1052,7 @@ mod tests { )] .into(), v1alpha1::OpenSearchTls::default(), + v1alpha1::SecurityConfig::default(), vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index d161bfb..28fb8f6 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -161,6 +161,7 @@ pub fn validate( cluster.spec.nodes.role_config.clone(), role_group_configs, cluster.spec.cluster_config.tls.clone(), + cluster.spec.cluster_config.security_config.clone(), cluster.spec.cluster_config.keystore.clone(), validated_discovery_endpoint, )) @@ -601,6 +602,7 @@ mod tests { server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), internal_secret_class: SecretClassName::from_str_unsafe("tls") }, + v1alpha1::SecurityConfig::default(), vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { @@ -843,6 +845,7 @@ mod tests { key: SecretKey::from_str_unsafe("my-keystore-file"), }, }], + security_config: v1alpha1::SecurityConfig::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 1ddfa03..b89a7ce 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,6 +1,7 @@ use std::{slice, str::FromStr}; use serde::{Deserialize, Serialize}; +use serde_json::json; use stackable_operator::{ commons::{ affinity::{StackableAffinity, StackableAffinityFragment, affinity_between_role_pods}, @@ -23,6 +24,7 @@ use stackable_operator::{ schemars::{self, JsonSchema}, shared::time::Duration, status::condition::{ClusterCondition, HasStatusCondition}, + utils::crds::raw_object_schema, versioned::versioned, }; use strum::{Display, EnumIter}; @@ -34,8 +36,8 @@ use crate::{ role_utils::GenericProductSpecificCommonConfig, types::{ kubernetes::{ - ConfigMapName, ContainerName, ListenerClassName, SecretClassName, SecretKey, - SecretName, + ConfigMapKey, ConfigMapName, ContainerName, ListenerClassName, SecretClassName, + SecretKey, SecretName, }, operator::{ClusterName, ProductName, RoleName}, }, @@ -57,6 +59,7 @@ constant!(TLS_DEFAULT_SECRET_CLASS: SecretClassName = "tls"); ) )] pub mod versioned { + /// An 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](DOCS_BASE_URL_PLACEHOLDER/opensearch/). @@ -101,6 +104,10 @@ pub mod versioned { #[serde(default)] pub keystore: Vec, + /// TODO Add description + #[serde(default)] + pub security_config: SecurityConfig, + /// TLS configuration options for the server (REST API) and internal communication (transport). #[serde(default)] pub tls: OpenSearchTls, @@ -123,6 +130,98 @@ pub mod versioned { pub secret_key_ref: SecretKeyRef, } + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct SecurityConfig { + #[serde(default = "security_config_enabled_default")] + pub enabled: bool, + + #[serde(default = "security_config_file_type_actiongroups_default")] + pub action_groups: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_allowlist_default")] + pub allow_list: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_audit_default")] + pub audit: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_config_default")] + pub config: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_internalusers_default")] + pub internal_users: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_nodesdn_default")] + pub nodes_dn: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_roles_default")] + pub roles: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_rolesmapping_default")] + pub roles_mapping: SecurityConfigFileType, + + #[serde(default = "security_config_file_type_tenants_default")] + pub tenants: SecurityConfigFileType, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct SecurityConfigFileType { + /// No default, so that the user is aware! + pub managed_by: SecurityConfigFileTypeManagedBy, + pub content: SecurityConfigFileTypeContent, + } + + #[derive( + Clone, + Debug, + Deserialize, + Display, + EnumIter, + Eq, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, + )] + pub enum SecurityConfigFileTypeManagedBy { + #[serde(rename = "API")] + Api, + + #[serde(rename = "operator")] + Operator, + } + + #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub enum SecurityConfigFileTypeContent { + Value(SecurityConfigFileTypeContentValue), + ValueFrom(SecurityConfigFileTypeContentValueFrom), + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + pub struct SecurityConfigFileTypeContentValue { + #[serde(flatten)] + #[schemars(schema_with = "raw_object_schema")] + value: serde_json::Value, + } + + #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub enum SecurityConfigFileTypeContentValueFrom { + ConfigMapKeyRef(ConfigMapKeyRef), + SecretKeyRef(SecretKeyRef), + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + pub struct ConfigMapKeyRef { + /// Name of the ConfigMap + pub name: ConfigMapName, + /// Key in the ConfigMap that contains the value + pub key: ConfigMapKey, + } + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] pub struct SecretKeyRef { /// Name of the Secret @@ -391,6 +490,267 @@ impl v1alpha1::OpenSearchConfig { } } +impl Default for v1alpha1::SecurityConfig { + fn default() -> Self { + v1alpha1::SecurityConfig { + enabled: security_config_enabled_default(), + action_groups: security_config_file_type_actiongroups_default(), + allow_list: security_config_file_type_allowlist_default(), + audit: security_config_file_type_audit_default(), + config: security_config_file_type_config_default(), + internal_users: security_config_file_type_internalusers_default(), + nodes_dn: security_config_file_type_nodesdn_default(), + roles_mapping: security_config_file_type_rolesmapping_default(), + roles: security_config_file_type_roles_default(), + tenants: security_config_file_type_tenants_default(), + } + } +} + +#[derive(Clone, Copy, EnumIter)] +pub enum SecurityConfigFileType { + ActionGroups, + AllowList, + Audit, + Config, + InternalUsers, + NodesDn, + Roles, + RolesMapping, + Tenants, +} + +impl SecurityConfigFileType { + pub fn filename(&self) -> String { + match self { + SecurityConfigFileType::ActionGroups => "action_groups.yml".to_owned(), + SecurityConfigFileType::AllowList => "allow_list.yml".to_owned(), + SecurityConfigFileType::Audit => "audit.yml".to_owned(), + SecurityConfigFileType::Config => "config.yml".to_owned(), + SecurityConfigFileType::InternalUsers => "internal_users.yml".to_owned(), + SecurityConfigFileType::NodesDn => "nodes_dn.yml".to_owned(), + SecurityConfigFileType::Roles => "roles.yml".to_owned(), + SecurityConfigFileType::RolesMapping => "roles_mapping.yml".to_owned(), + SecurityConfigFileType::Tenants => "tenants.yml".to_owned(), + } + } + + pub fn volume_name(&self) -> String { + match self { + SecurityConfigFileType::ActionGroups => "actiongroups".to_owned(), + SecurityConfigFileType::AllowList => "allowlist".to_owned(), + SecurityConfigFileType::Audit => "audit".to_owned(), + SecurityConfigFileType::Config => "config".to_owned(), + SecurityConfigFileType::InternalUsers => "internalusers".to_owned(), + SecurityConfigFileType::NodesDn => "nodesdn".to_owned(), + SecurityConfigFileType::Roles => "roles".to_owned(), + SecurityConfigFileType::RolesMapping => "rolesmapping".to_owned(), + SecurityConfigFileType::Tenants => "tenants".to_owned(), + } + } + + fn default_content(&self) -> serde_json::Value { + match self { + SecurityConfigFileType::ActionGroups => json!({ + "_meta": { + "type": "actiongroups", + "config_version": 2 + } + }), + SecurityConfigFileType::AllowList => json!({ + "_meta": { + "type": "allowlist", + "config_version": 2 + }, + "config": { + "enabled": false + } + }), + SecurityConfigFileType::Audit => json!({ + "_meta": { + "type": "audit", + "config_version": 2 + }, + "config": { + "enabled": false + } + }), + SecurityConfigFileType::Config => json!({ + "_meta": { + "type": "config", + "config_version": 2 + }, + "config": { + "dynamic": { + "http": {}, + "authc": {}, + "authz": {} + } + } + }), + SecurityConfigFileType::InternalUsers => json!({ + "_meta": { + "type": "internalusers", + "config_version": 2 + } + }), + SecurityConfigFileType::NodesDn => json!({ + "_meta": { + "type": "nodesdn", + "config_version": 2 + } + }), + SecurityConfigFileType::Roles => json!({ + "_meta": { + "type": "roles", + "config_version": 2 + } + }), + SecurityConfigFileType::RolesMapping => json!({ + "_meta": { + "type": "rolesmapping", + "config_version": 2 + } + }), + SecurityConfigFileType::Tenants => json!({ + "_meta": { + "type": "tenants", + "config_version": 2 + } + }), + } + } +} + +impl v1alpha1::SecurityConfig { + fn security_config( + &self, + file_type: SecurityConfigFileType, + ) -> &v1alpha1::SecurityConfigFileType { + match file_type { + SecurityConfigFileType::ActionGroups => &self.action_groups, + SecurityConfigFileType::AllowList => &self.allow_list, + SecurityConfigFileType::Audit => &self.audit, + SecurityConfigFileType::Config => &self.config, + SecurityConfigFileType::InternalUsers => &self.internal_users, + SecurityConfigFileType::NodesDn => &self.nodes_dn, + SecurityConfigFileType::Roles => &self.roles, + SecurityConfigFileType::RolesMapping => &self.roles_mapping, + SecurityConfigFileType::Tenants => &self.tenants, + } + } + + pub fn value(&self, file_type: SecurityConfigFileType) -> Option { + if !self.enabled { + None + } else if let v1alpha1::SecurityConfigFileType { + content: + v1alpha1::SecurityConfigFileTypeContent::Value( + v1alpha1::SecurityConfigFileTypeContentValue { value }, + ), + .. + } = self.security_config(file_type) + { + Some(value.to_string()) + } else { + None + } + } + + pub fn config_map_key_ref( + &self, + file_type: SecurityConfigFileType, + ) -> Option<&v1alpha1::ConfigMapKeyRef> { + if !self.enabled { + None + } else if let v1alpha1::SecurityConfigFileType { + content: + v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + config_map_key_ref, + ), + ), + .. + } = self.security_config(file_type) + { + Some(config_map_key_ref) + } else { + None + } + } + + pub fn secret_key_ref( + &self, + file_type: SecurityConfigFileType, + ) -> Option<&v1alpha1::SecretKeyRef> { + if !self.enabled { + None + } else if let v1alpha1::SecurityConfigFileType { + content: + v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::SecretKeyRef(secret_key_ref), + ), + .. + } = self.security_config(file_type) + { + Some(secret_key_ref) + } else { + None + } + } +} + +fn security_config_enabled_default() -> bool { + true +} + +fn security_config_file_type_actiongroups_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::ActionGroups) +} + +fn security_config_file_type_allowlist_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::AllowList) +} + +fn security_config_file_type_audit_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::Audit) +} + +fn security_config_file_type_config_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::Config) +} + +fn security_config_file_type_internalusers_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::InternalUsers) +} + +fn security_config_file_type_nodesdn_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::NodesDn) +} + +fn security_config_file_type_roles_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::Roles) +} + +fn security_config_file_type_rolesmapping_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::RolesMapping) +} + +fn security_config_file_type_tenants_default() -> v1alpha1::SecurityConfigFileType { + crd_default(SecurityConfigFileType::Tenants) +} + +fn crd_default(file_type: SecurityConfigFileType) -> v1alpha1::SecurityConfigFileType { + v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, + content: v1alpha1::SecurityConfigFileTypeContent::Value( + v1alpha1::SecurityConfigFileTypeContentValue { + value: file_type.default_content(), + }, + ), + } +} + impl Default for v1alpha1::OpenSearchTls { fn default() -> Self { v1alpha1::OpenSearchTls { diff --git a/tests/templates/kuttl/security-config/10-security-config.yaml b/tests/templates/kuttl/security-config/10-security-config.yaml index 14f5c81..09ffaab 100644 --- a/tests/templates/kuttl/security-config/10-security-config.yaml +++ b/tests/templates/kuttl/security-config/10-security-config.yaml @@ -2,49 +2,8 @@ apiVersion: v1 kind: Secret metadata: - name: initial-security-config + name: security-config-internal-users stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} internal_users.yml: | --- # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh @@ -64,11 +23,12 @@ stringData: hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS reserved: true description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: security-config +data: roles.yml: | --- _meta: @@ -98,36 +58,3 @@ stringData: monitoring: backend_roles: - opendistro_security_anonymous_backendrole - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 ---- -apiVersion: v1 -kind: Secret -metadata: - name: managed-security-config -stringData: - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - http: - anonymous_auth_enabled: true diff --git a/tests/templates/kuttl/security-config/11-assert.yaml.j2 b/tests/templates/kuttl/security-config/11-assert.yaml.j2 index 63218dd..dacdb00 100644 --- a/tests/templates/kuttl/security-config/11-assert.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-assert.yaml.j2 @@ -26,3 +26,29 @@ metadata: status: readyReplicas: 1 replicas: 1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch-nodes-cluster-manager +data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate + via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":true}}}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch-nodes-data +data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate + via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":true}}}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' diff --git a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 index e1c2930..81fe8a0 100644 --- a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 @@ -11,6 +11,51 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent clusterConfig: + securityConfig: + config: + managedBy: operator + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + http: + anonymous_auth_enabled: true + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: security-config-internal-users + key: internal_users.yml + roles: + managedBy: API + content: + valueFrom: + configMapKeyRef: + name: security-config + key: roles.yml + rolesMapping: + managedBy: API + content: + valueFrom: + configMapKeyRef: + name: security-config + key: roles_mapping.yml {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} @@ -178,9 +223,10 @@ spec: # Wait until the pod is restarted due to a change of the Secret. wait_seconds 0 volumeMounts: - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security - name: managed-security-config + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security/config.yml + name: initial-security-config-config readOnly: true + subPath: config.yml - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas name: pemtrustedcas readOnly: true @@ -216,19 +262,6 @@ spec: plugins.security.allow_default_init_securityindex: "true" plugins.security.authcz.admin_dn: CN=update-security-config.localhost plugins.security.restapi.roles_enabled: all_access - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - name: initial-security-config - readOnly: true - volumes: - - name: initial-security-config - secret: - secretName: initial-security-config - defaultMode: 0o660 objectOverrides: - apiVersion: apps/v1 kind: StatefulSet diff --git a/tests/templates/kuttl/security-config/21-assert.yaml b/tests/templates/kuttl/security-config/21-assert.yaml new file mode 100644 index 0000000..3176668 --- /dev/null +++ b/tests/templates/kuttl/security-config/21-assert.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch-nodes-cluster-manager +data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate + via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":false}}}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch-nodes-data +data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate + via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":false}}}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' + diff --git a/tests/templates/kuttl/security-config/21-change-security-config.yaml b/tests/templates/kuttl/security-config/21-change-security-config.yaml index 8c4220e..9377214 100644 --- a/tests/templates/kuttl/security-config/21-change-security-config.yaml +++ b/tests/templates/kuttl/security-config/21-change-security-config.yaml @@ -1,28 +1,15 @@ --- -apiVersion: v1 -kind: Secret +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster metadata: - name: managed-security-config -stringData: - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - http: - anonymous_auth_enabled: false + name: opensearch +spec: + clusterConfig: + securityConfig: + config: + content: + value: + config: + dynamic: + http: + anonymous_auth_enabled: false From d12a691c47e5aa7827d915f64842f1867fcd8a2a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 10 Feb 2026 15:18:20 +0100 Subject: [PATCH 03/53] Deploy security config files only if completely managed by the API --- .../src/controller/build/node_config.rs | 10 ++ .../controller/build/role_group_builder.rs | 149 ++++++++++-------- rust/operator-binary/src/crd/mod.rs | 8 +- .../security-config/10-security-config.yaml | 2 +- .../11_install-opensearch.yaml.j2 | 143 ++++++++++++++++- 5 files changed, 242 insertions(+), 70 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 8c84322..fdb291e 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -72,6 +72,12 @@ const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; /// Type: string const CONFIG_OPTION_PATH_LOGS: &str = "path.logs"; +/// If this is set to true OpenSearch Security will automatically initialize the configuration index +/// with the files in the config directory if the index does not exist. +/// Type: boolean +const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = + "plugins.security.allow_default_init_securityindex"; + /// Specifies a list of distinguished names (DNs) that denote the other nodes in the cluster. /// Type: (comma-separated) list of strings const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; @@ -199,6 +205,10 @@ impl NodeConfig { CONFIG_OPTION_DISCOVERY_TYPE.to_owned(), json!(self.discovery_type()), ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), + json!(self.cluster.security_config.is_only_managed_by_api()), + ); config.insert // Accept certificates generated by the secret-operator ( 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 b67eeb2..249dfbb 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -176,6 +176,7 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } + // TODO Deploy only for the security-config role group for file_type in SecurityConfigFileType::iter() { if let Some(value) = self.cluster.security_config.value(file_type) { data.insert(file_type.filename(), value.to_string()); @@ -378,57 +379,8 @@ impl<'a> RoleGroupBuilder<'a> { )) }; - for file_type in SecurityConfigFileType::iter() { - if self.cluster.security_config.value(file_type).is_some() { - let volume = Volume { - name: format!("initial-security-config-{}", file_type.volume_name()), - config_map: Some(ConfigMapVolumeSource { - items: Some(vec![KeyToPath { - key: file_type.filename(), - mode: Some(0o660), - path: file_type.filename(), - }]), - name: self.resource_names.role_group_config_map().to_string(), - ..Default::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = - self.cluster.security_config.config_map_key_ref(file_type) - { - let volume = Volume { - name: format!("initial-security-config-{}", file_type.volume_name()), - config_map: Some(ConfigMapVolumeSource { - items: Some(vec![KeyToPath { - key: key.to_string(), - mode: Some(0o660), - path: file_type.filename(), - }]), - name: name.to_string(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::SecretKeyRef { name, key }) = - self.cluster.security_config.secret_key_ref(file_type) - { - let volume = Volume { - name: format!("initial-security-config-{}", file_type.volume_name()), - secret: Some(SecretVolumeSource { - items: Some(vec![KeyToPath { - key: key.to_string(), - mode: Some(0o660), - path: file_type.filename(), - }]), - secret_name: Some(name.to_string()), - ..SecretVolumeSource::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } + if self.cluster.security_config.is_only_managed_by_api() { + volumes.extend(self.security_config_volumes()); } if !self.cluster.keystores.is_empty() { @@ -672,17 +624,8 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }); } - for file_type in SecurityConfigFileType::iter() { - volume_mounts.push(VolumeMount { - mount_path: format!( - "{opensearch_path_conf}/opensearch-security/{filename}", - filename = file_type.filename() - ), - name: format!("initial-security-config-{}", file_type.volume_name()), - read_only: Some(true), - sub_path: Some(file_type.filename()), - ..VolumeMount::default() - }); + if self.cluster.security_config.is_only_managed_by_api() { + volume_mounts.extend(self.security_config_volume_mounts()); } if !self.cluster.keystores.is_empty() { @@ -913,6 +856,88 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ) .build() } + + fn security_config_volumes(&self) -> Vec { + let mut volumes = vec![]; + + for file_type in SecurityConfigFileType::iter() { + let volume_name = format!("security-config-file-{}", file_type.volume_name()); + if self.cluster.security_config.value(file_type).is_some() { + let volume = Volume { + name: volume_name, + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: file_type.filename(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: self.resource_names.role_group_config_map().to_string(), + ..Default::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = + self.cluster.security_config.config_map_key_ref(file_type) + { + let volume = Volume { + name: volume_name, + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: name.to_string(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::SecretKeyRef { name, key }) = + self.cluster.security_config.secret_key_ref(file_type) + { + let volume = Volume { + name: volume_name, + secret: Some(SecretVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + secret_name: Some(name.to_string()), + ..SecretVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } + } + + volumes + } + + fn security_config_volume_mounts(&self) -> Vec { + let mut volume_mounts = vec![]; + + let opensearch_path_conf = self.node_config.opensearch_path_conf(); + + for file_type in SecurityConfigFileType::iter() { + let volume_name = format!("security-config-file-{}", file_type.volume_name()); + volume_mounts.push(VolumeMount { + mount_path: format!( + "{opensearch_path_conf}/opensearch-security/{filename}", + filename = file_type.filename() + ), + name: volume_name, + read_only: Some(true), + sub_path: Some(file_type.filename()), + ..VolumeMount::default() + }); + } + + volume_mounts + } } #[cfg(test)] diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b89a7ce..c6a52c3 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -27,7 +27,7 @@ use stackable_operator::{ utils::crds::raw_object_schema, versioned::versioned, }; -use strum::{Display, EnumIter}; +use strum::{Display, EnumIter, IntoEnumIterator}; use crate::{ attributed_string_type, constant, @@ -640,6 +640,12 @@ impl v1alpha1::SecurityConfig { } } + pub fn is_only_managed_by_api(&self) -> bool { + SecurityConfigFileType::iter() + .map(|file_type| self.security_config(file_type)) + .all(|config| config.managed_by == v1alpha1::SecurityConfigFileTypeManagedBy::Api) + } + pub fn value(&self, file_type: SecurityConfigFileType) -> Option { if !self.enabled { None diff --git a/tests/templates/kuttl/security-config/10-security-config.yaml b/tests/templates/kuttl/security-config/10-security-config.yaml index 09ffaab..d37283f 100644 --- a/tests/templates/kuttl/security-config/10-security-config.yaml +++ b/tests/templates/kuttl/security-config/10-security-config.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: - name: security-config-internal-users + name: security-config-file-internal-users stringData: internal_users.yml: | --- diff --git a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 index 81fe8a0..049fbb6 100644 --- a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 @@ -183,11 +183,50 @@ spec: fi } + function initialize_security_index() { + echo "Initialize the security index." + + until plugins/opensearch-security/tools/securityadmin.sh \ + --configdir {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security \ + --disable-host-name-verification \ + -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt \ + -cert /stackable/tls-admin/tls.crt \ + -key /stackable/tls-admin/tls.key + do + echo "Initializing the security index failed." + wait_seconds 10 + done + } + + function check_security_index() { + echo "Check the status of the security index." + + STATUS_CODE=$(curl \ + --insecure \ + --cert /stackable/tls-admin/tls.crt \ + --key /stackable/tls-admin/tls.key \ + --silent \ + --output /dev/null \ + --write-out "%{http_code}" \ + https://localhost:9200/.opendistro_security) + if test "$STATUS_CODE" = "200" + then + echo "The security index is already initialized." + elif test "$STATUS_CODE" = "404" + then + initialize_security_index + else + echo "Checking the security index failed." + wait_seconds 10 + check_security_index + fi + } + function update_config () { filetype="$1" filename="$2" - file="{{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security/$filename" + file="{{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/$filename" if test -e "$file" then @@ -209,6 +248,8 @@ spec: fi } + check_security_index + update_config actiongroups action_groups.yml update_config allowlist allowlist.yml update_config audit audit.yml @@ -223,10 +264,42 @@ spec: # Wait until the pod is restarted due to a change of the Secret. wait_seconds 0 volumeMounts: - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/new-opensearch-security/config.yml - name: initial-security-config-config + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml + name: security-config-file-actiongroups + readOnly: true + subPath: action_groups.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml + name: security-config-file-allowlist + readOnly: true + subPath: allow_list.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml + name: security-config-file-audit + readOnly: true + subPath: audit.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/config.yml + name: security-config-file-config readOnly: true subPath: config.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/internal_users.yml + name: security-config-file-internalusers + readOnly: true + subPath: internal_users.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/nodes_dn.yml + name: security-config-file-nodesdn + readOnly: true + subPath: nodes_dn.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles.yml + name: security-config-file-roles + readOnly: true + subPath: roles.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles_mapping.yml + name: security-config-file-rolesmapping + readOnly: true + subPath: roles_mapping.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/tenants.yml + name: security-config-file-tenants + readOnly: true + subPath: tenants.yml - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas name: pemtrustedcas readOnly: true @@ -259,7 +332,6 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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.authcz.admin_dn: CN=update-security-config.localhost plugins.security.restapi.roles_enabled: all_access objectOverrides: @@ -272,7 +344,66 @@ spec: template: spec: volumes: - - name: managed-security-config + - name: security-config-file-actiongroups + configMap: + defaultMode: 0o660 + items: + - key: action_groups.yml + path: action_groups.yml + name: opensearch-nodes-security-config + - name: security-config-file-allowlist + configMap: + defaultMode: 0o660 + items: + - key: allow_list.yml + path: allow_list.yml + name: opensearch-nodes-security-config + - name: security-config-file-audit + configMap: + defaultMode: 0o660 + items: + - key: audit.yml + path: audit.yml + name: opensearch-nodes-security-config + - name: security-config-file-config + configMap: + defaultMode: 0o660 + items: + - key: config.yml + path: config.yml + name: opensearch-nodes-security-config + - name: security-config-file-internalusers secret: - secretName: managed-security-config defaultMode: 0o660 + items: + - key: internal_users.yml + path: internal_users.yml + secretName: security-config-file-internal-users + - name: security-config-file-nodesdn + configMap: + defaultMode: 0o660 + items: + - key: nodes_dn.yml + path: nodes_dn.yml + name: opensearch-nodes-security-config + - name: security-config-file-roles + configMap: + defaultMode: 0o660 + items: + - key: roles.yml + path: roles.yml + name: security-config + - name: security-config-file-rolesmapping + configMap: + defaultMode: 0o660 + items: + - key: roles_mapping.yml + path: roles_mapping.yml + name: security-config + - name: security-config-file-tenants + configMap: + defaultMode: 0o660 + items: + - key: tenants.yml + path: tenants.yml + name: opensearch-nodes-security-config From a0382d01bcebb2206d9b2f0380e563c39f869194 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 10 Feb 2026 16:01:17 +0100 Subject: [PATCH 04/53] test(smoke): Use securityConfig --- .../{11-assert.yaml.j2 => 10-assert.yaml.j2} | 248 +++++++++++++++++- ....yaml.j2 => 10-install-opensearch.yaml.j2} | 63 ++++- .../smoke/10-opensearch-security-config.yaml | 96 ------- 3 files changed, 283 insertions(+), 124 deletions(-) rename tests/templates/kuttl/smoke/{11-assert.yaml.j2 => 10-assert.yaml.j2} (77%) rename tests/templates/kuttl/smoke/{11-install-opensearch.yaml.j2 => 10-install-opensearch.yaml.j2} (63%) delete mode 100644 tests/templates/kuttl/smoke/10-opensearch-security-config.yaml diff --git a/tests/templates/kuttl/smoke/11-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 similarity index 77% rename from tests/templates/kuttl/smoke/11-assert.yaml.j2 rename to tests/templates/kuttl/smoke/10-assert.yaml.j2 index 65617e9..b9174b3 100644 --- a/tests/templates/kuttl/smoke/11-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -190,9 +190,42 @@ spec: - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server name: tls-server {% endif %} - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - name: security-config + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml + name: security-config-file-actiongroups readOnly: true + subPath: action_groups.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml + name: security-config-file-allowlist + readOnly: true + subPath: allow_list.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml + name: security-config-file-audit + readOnly: true + subPath: audit.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/config.yml + name: security-config-file-config + readOnly: true + subPath: config.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/internal_users.yml + name: security-config-file-internalusers + readOnly: true + subPath: internal_users.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/nodes_dn.yml + name: security-config-file-nodesdn + readOnly: true + subPath: nodes_dn.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles.yml + name: security-config-file-roles + readOnly: true + subPath: roles.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles_mapping.yml + name: security-config-file-rolesmapping + readOnly: true + subPath: roles_mapping.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/tenants.yml + name: security-config-file-tenants + readOnly: true + subPath: tenants.yml {% if lookup('env', 'VECTOR_AGGREGATOR') %} - args: - |- @@ -309,10 +342,78 @@ spec: volumeMode: Filesystem name: tls-server {% endif %} - - name: security-config - secret: - defaultMode: 0o660 - secretName: opensearch-security-config + - configMap: + defaultMode: 420 + items: + - key: action_groups.yml + mode: 432 + path: action_groups.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-actiongroups + - configMap: + defaultMode: 420 + items: + - key: allow_list.yml + mode: 432 + path: allow_list.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-allowlist + - configMap: + defaultMode: 420 + items: + - key: audit.yml + mode: 432 + path: audit.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-audit + - configMap: + defaultMode: 420 + items: + - key: config.yml + mode: 432 + path: config.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-config + - configMap: + defaultMode: 420 + items: + - key: internal_users.yml + mode: 432 + path: internal_users.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-internalusers + - configMap: + defaultMode: 420 + items: + - key: nodes_dn.yml + mode: 432 + path: nodes_dn.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-nodesdn + - configMap: + defaultMode: 420 + items: + - key: roles.yml + mode: 432 + path: roles.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-roles + - configMap: + defaultMode: 420 + items: + - key: roles_mapping.yml + mode: 432 + path: roles_mapping.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-rolesmapping + - configMap: + defaultMode: 420 + items: + - key: tenants.yml + mode: 432 + path: tenants.yml + name: opensearch-nodes-cluster-manager + name: security-config-file-tenants volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim @@ -555,9 +656,42 @@ spec: - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server name: tls-server {% endif %} - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - name: security-config + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml + name: security-config-file-actiongroups readOnly: true + subPath: action_groups.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml + name: security-config-file-allowlist + readOnly: true + subPath: allow_list.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml + name: security-config-file-audit + readOnly: true + subPath: audit.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/config.yml + name: security-config-file-config + readOnly: true + subPath: config.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/internal_users.yml + name: security-config-file-internalusers + readOnly: true + subPath: internal_users.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/nodes_dn.yml + name: security-config-file-nodesdn + readOnly: true + subPath: nodes_dn.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles.yml + name: security-config-file-roles + readOnly: true + subPath: roles.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles_mapping.yml + name: security-config-file-rolesmapping + readOnly: true + subPath: roles_mapping.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/tenants.yml + name: security-config-file-tenants + readOnly: true + subPath: tenants.yml {% if lookup('env', 'VECTOR_AGGREGATOR') %} - args: - |- @@ -674,10 +808,78 @@ spec: volumeMode: Filesystem name: tls-server {% endif %} - - name: security-config - secret: - defaultMode: 0o660 - secretName: opensearch-security-config + - configMap: + defaultMode: 420 + items: + - key: action_groups.yml + mode: 432 + path: action_groups.yml + name: opensearch-nodes-data + name: security-config-file-actiongroups + - configMap: + defaultMode: 420 + items: + - key: allow_list.yml + mode: 432 + path: allow_list.yml + name: opensearch-nodes-data + name: security-config-file-allowlist + - configMap: + defaultMode: 420 + items: + - key: audit.yml + mode: 432 + path: audit.yml + name: opensearch-nodes-data + name: security-config-file-audit + - configMap: + defaultMode: 420 + items: + - key: config.yml + mode: 432 + path: config.yml + name: opensearch-nodes-data + name: security-config-file-config + - configMap: + defaultMode: 420 + items: + - key: internal_users.yml + mode: 432 + path: internal_users.yml + name: opensearch-nodes-data + name: security-config-file-internalusers + - configMap: + defaultMode: 420 + items: + - key: nodes_dn.yml + mode: 432 + path: nodes_dn.yml + name: opensearch-nodes-data + name: security-config-file-nodesdn + - configMap: + defaultMode: 420 + items: + - key: roles.yml + mode: 432 + path: roles.yml + name: opensearch-nodes-data + name: security-config-file-roles + - configMap: + defaultMode: 420 + items: + - key: roles_mapping.yml + mode: 432 + path: roles_mapping.yml + name: opensearch-nodes-data + name: security-config-file-rolesmapping + - configMap: + defaultMode: 420 + items: + - key: tenants.yml + mode: 432 + path: tenants.yml + name: opensearch-nodes-data + name: security-config-file-tenants volumeClaimTemplates: - apiVersion: v1 kind: PersistentVolumeClaim @@ -734,6 +936,12 @@ metadata: kind: OpenSearchCluster name: opensearch data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{}}}}' + internal_users.yml: '{"_meta":{"config_version":2,"type":"internalusers"},"admin":{"backend_roles":["admin"],"description":"OpenSearch admin user","hash":"$2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e","reserved":true}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' opensearch.yml: |- cluster.name: "opensearch" cluster.routing.allocation.disk.threshold_enabled: "false" @@ -742,7 +950,7 @@ data: node.attr.role-group: "cluster-manager" node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" - plugins.security.allow_default_init_securityindex: "true" + plugins.security.allow_default_init_securityindex: true plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true @@ -756,6 +964,9 @@ data: plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/tls.crt" plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/tls.key" plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/ca.crt" + roles.yml: '{"_meta":{"config_version":2,"type":"roles"}}' + roles_mapping.yml: '{"_meta":{"config_version":2,"type":"rolesmapping"},"all_access":{"backend_roles":["admin"],"reserved":false}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' --- apiVersion: v1 kind: ConfigMap @@ -775,6 +986,12 @@ metadata: kind: OpenSearchCluster name: opensearch data: + action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' + allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{}}}}' + internal_users.yml: '{"_meta":{"config_version":2,"type":"internalusers"},"admin":{"backend_roles":["admin"],"description":"OpenSearch admin user","hash":"$2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e","reserved":true}}' + nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' opensearch.yml: |- cluster.name: "opensearch" cluster.routing.allocation.disk.threshold_enabled: "false" @@ -783,7 +1000,7 @@ data: node.attr.role-group: "data" node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" - plugins.security.allow_default_init_securityindex: "true" + plugins.security.allow_default_init_securityindex: true plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true @@ -797,6 +1014,9 @@ data: plugins.security.ssl.transport.pemcert_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/tls.crt" plugins.security.ssl.transport.pemkey_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/tls.key" plugins.security.ssl.transport.pemtrustedcas_filepath: "{{ test_scenario['values']['opensearch_home'] }}/config/tls/internal/ca.crt" + roles.yml: '{"_meta":{"config_version":2,"type":"roles"}}' + roles_mapping.yml: '{"_meta":{"config_version":2,"type":"rolesmapping"},"all_access":{"backend_roles":["admin"],"reserved":false}}' + tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' --- apiVersion: v1 kind: Service diff --git a/tests/templates/kuttl/smoke/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 similarity index 63% rename from tests/templates/kuttl/smoke/11-install-opensearch.yaml.j2 rename to tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 index d61f6cf..f7348ca 100644 --- a/tests/templates/kuttl/smoke/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 @@ -11,6 +11,55 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent clusterConfig: + securityConfig: + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 + + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + value: + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 + + all_access: + reserved: false + backend_roles: + - admin {% if test_scenario['values']['server-use-tls'] == 'false' %} tls: serverSecretClass: null @@ -63,17 +112,3 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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" - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 diff --git a/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml b/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml deleted file mode 100644 index 27c6f34..0000000 --- a/tests/templates/kuttl/smoke/10-opensearch-security-config.yaml +++ /dev/null @@ -1,96 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - internal_users.yml: | - --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 From 0b8ab8a98f1bdfcdacb3ca7a68d0d8a3df5715db Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 11 Feb 2026 11:05:29 +0100 Subject: [PATCH 05/53] Deploy security config files only to the managing role group --- .../helm/opensearch-operator/crds/crds.yaml | 7 ++ rust/operator-binary/src/controller.rs | 12 ++- .../controller/build/role_group_builder.rs | 19 +++-- .../src/controller/preprocess.rs | 71 ++++++++++++++++++ rust/operator-binary/src/crd/mod.rs | 10 ++- .../security-config/10-security-config.yaml | 12 --- .../{11-assert.yaml.j2 => 11-assert.yaml} | 15 +--- .../11-install-opensearch.yaml | 8 -- ....yaml.j2 => 11-install-opensearch.yaml.j2} | 75 +------------------ .../kuttl/security-config/21-assert.yaml | 18 +---- 10 files changed, 116 insertions(+), 131 deletions(-) create mode 100644 rust/operator-binary/src/controller/preprocess.rs rename tests/templates/kuttl/security-config/{11-assert.yaml.j2 => 11-assert.yaml} (58%) delete mode 100644 tests/templates/kuttl/security-config/11-install-opensearch.yaml rename tests/templates/kuttl/security-config/{11_install-opensearch.yaml.j2 => 11-install-opensearch.yaml.j2} (83%) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 05e82a7..82db88b 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -77,6 +77,7 @@ spec: config_version: 2 type: internalusers managedBy: API + managingRoleGroup: security-config nodesDn: content: value: @@ -191,6 +192,7 @@ spec: config_version: 2 type: internalusers managedBy: API + managingRoleGroup: security-config nodesDn: content: value: @@ -603,6 +605,11 @@ spec: - content - managedBy type: object + managingRoleGroup: + default: security-config + maxLength: 16 + minLength: 1 + type: string nodesDn: default: content: diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 5377119..2a4fb62 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -31,6 +31,7 @@ use update_status::update_status; use validate::validate; use crate::{ + controller::preprocess::preprocess, crd::{NodeRoles, v1alpha1}, framework::{ HasName, HasUid, NameIsValidLabelValue, @@ -50,6 +51,7 @@ use crate::{ mod apply; mod build; mod dereference; +mod preprocess; mod update_status; mod validate; @@ -120,6 +122,9 @@ pub enum Error { #[snafu(display("failed to dereference resources"))] Dereference { source: dereference::Error }, + #[snafu(display("failed to preprocess cluster"))] + Preprocess { source: preprocess::Error }, + #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, @@ -378,9 +383,12 @@ pub async fn reconcile( .await .context(DereferenceSnafu)?; + // preprocess (no client required) + let preprocessed_cluster = preprocess(cluster.clone()).context(PreprocessSnafu)?; + // validate (no client required) - let validated_cluster = - validate(&context.names, cluster, &dereferenced_objects).context(ValidateClusterSnafu)?; + let validated_cluster = validate(&context.names, &preprocessed_cluster, &dereferenced_objects) + .context(ValidateClusterSnafu)?; // build (no client required; infallible) let prepared_resources = build(&context.names, validated_cluster.clone()); 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 249dfbb..1b54ff8 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -144,6 +144,12 @@ impl<'a> RoleGroupBuilder<'a> { } } + fn manages_security_config(&self) -> bool { + self.cluster.security_config.enabled + && (self.cluster.security_config.is_only_managed_by_api() + || self.cluster.security_config.managing_role_group == self.role_group_name) + } + /// Builds the [`ConfigMap`] containing the configuration files of the role-group /// [`StatefulSet`] pub fn build_config_map(&self) -> ConfigMap { @@ -176,10 +182,11 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - // TODO Deploy only for the security-config role group - for file_type in SecurityConfigFileType::iter() { - if let Some(value) = self.cluster.security_config.value(file_type) { - data.insert(file_type.filename(), value.to_string()); + if self.manages_security_config() { + for file_type in SecurityConfigFileType::iter() { + if let Some(value) = self.cluster.security_config.value(file_type) { + data.insert(file_type.filename(), value.to_string()); + } } } @@ -379,7 +386,7 @@ impl<'a> RoleGroupBuilder<'a> { )) }; - if self.cluster.security_config.is_only_managed_by_api() { + if self.manages_security_config() { volumes.extend(self.security_config_volumes()); } @@ -624,7 +631,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }); } - if self.cluster.security_config.is_only_managed_by_api() { + if self.manages_security_config() { volume_mounts.extend(self.security_config_volume_mounts()); } diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs new file mode 100644 index 0000000..36f9ef6 --- /dev/null +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -0,0 +1,71 @@ +use snafu::Snafu; +use stackable_operator::{ + commons::resources::{PvcConfigFragment, ResourcesFragment}, + k8s_openapi::apimachinery::pkg::api::resource::Quantity, + role_utils::{CommonConfiguration, RoleGroup}, +}; +use strum::{EnumDiscriminants, IntoStaticStr}; +use tracing::info; + +use crate::{ + crd::{NodeRoles, v1alpha1}, + framework::role_utils::GenericProductSpecificCommonConfig, +}; + +#[derive(Snafu, Debug, EnumDiscriminants)] +#[strum_discriminants(derive(IntoStaticStr))] +pub enum Error { + #[snafu(display("failed to get the cluster name"))] + GetClusterName { + source: crate::framework::controller_utils::Error, + }, +} + +type Result = std::result::Result; + +pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result { + let security_config = &cluster.spec.cluster_config.security_config; + if !security_config.is_only_managed_by_api() + && !cluster + .spec + .nodes + .role_groups + .contains_key(&security_config.managing_role_group.to_string()) + { + info!( + "The security configuration is managed by the role group \"{role_group}\". \ + This role group was not specified explicitly and will be created.", + role_group = security_config.managing_role_group + ); + + let role_group = + RoleGroup:: { + config: CommonConfiguration { + config: v1alpha1::OpenSearchConfigFragment { + discovery_service_exposed: Some(false), + node_roles: Some(NodeRoles(vec![])), + resources: ResourcesFragment { + storage: v1alpha1::StorageConfigFragment { + data: PvcConfigFragment { + capacity: Some(Quantity("100Mi".to_owned())), + ..PvcConfigFragment::default() + }, + }, + ..ResourcesFragment::default() + }, + ..v1alpha1::OpenSearchConfigFragment::default() + }, + ..CommonConfiguration::default() + }, + replicas: Some(1), + }; + + cluster + .spec + .nodes + .role_groups + .insert(security_config.managing_role_group.to_string(), role_group); + } + + Ok(cluster) +} diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index c6a52c3..99a1acc 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -39,7 +39,7 @@ use crate::{ ConfigMapKey, ConfigMapName, ContainerName, ListenerClassName, SecretClassName, SecretKey, SecretName, }, - operator::{ClusterName, ProductName, RoleName}, + operator::{ClusterName, ProductName, RoleGroupName, RoleName}, }, }, }; @@ -136,6 +136,9 @@ pub mod versioned { #[serde(default = "security_config_enabled_default")] pub enabled: bool, + #[serde(default = "security_config_managing_role_group")] + pub managing_role_group: RoleGroupName, + #[serde(default = "security_config_file_type_actiongroups_default")] pub action_groups: SecurityConfigFileType, @@ -494,6 +497,7 @@ impl Default for v1alpha1::SecurityConfig { fn default() -> Self { v1alpha1::SecurityConfig { enabled: security_config_enabled_default(), + managing_role_group: security_config_managing_role_group(), action_groups: security_config_file_type_actiongroups_default(), allow_list: security_config_file_type_allowlist_default(), audit: security_config_file_type_audit_default(), @@ -710,6 +714,10 @@ fn security_config_enabled_default() -> bool { true } +fn security_config_managing_role_group() -> RoleGroupName { + RoleGroupName::from_str("security-config").expect("should be a valid role group name") +} + fn security_config_file_type_actiongroups_default() -> v1alpha1::SecurityConfigFileType { crd_default(SecurityConfigFileType::ActionGroups) } diff --git a/tests/templates/kuttl/security-config/10-security-config.yaml b/tests/templates/kuttl/security-config/10-security-config.yaml index d37283f..a411ee8 100644 --- a/tests/templates/kuttl/security-config/10-security-config.yaml +++ b/tests/templates/kuttl/security-config/10-security-config.yaml @@ -6,8 +6,6 @@ metadata: stringData: internal_users.yml: | --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - _meta: type: internalusers config_version: 2 @@ -18,11 +16,6 @@ stringData: backend_roles: - admin description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user --- apiVersion: v1 kind: ConfigMap @@ -50,11 +43,6 @@ data: backend_roles: - admin - kibana_server: - reserved: true - users: - - kibanaserver - monitoring: backend_roles: - opendistro_security_anonymous_backendrole diff --git a/tests/templates/kuttl/security-config/11-assert.yaml.j2 b/tests/templates/kuttl/security-config/11-assert.yaml similarity index 58% rename from tests/templates/kuttl/security-config/11-assert.yaml.j2 rename to tests/templates/kuttl/security-config/11-assert.yaml index dacdb00..4f0fc5a 100644 --- a/tests/templates/kuttl/security-config/11-assert.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-assert.yaml @@ -30,20 +30,7 @@ status: apiVersion: v1 kind: ConfigMap metadata: - name: opensearch-nodes-cluster-manager -data: - action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate - via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":true}}}}' - nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' - tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: opensearch-nodes-data + name: opensearch-nodes-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml b/tests/templates/kuttl/security-config/11-install-opensearch.yaml deleted file mode 100644 index 3a4bf21..0000000 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -timeout: 600 -commands: - - script: > - envsubst '$NAMESPACE' < 11_install-opensearch.yaml | - kubectl apply --namespace=$NAMESPACE --filename=- diff --git a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 similarity index 83% rename from tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 rename to tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 049fbb6..37a81fe 100644 --- a/tests/templates/kuttl/security-config/11_install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -40,7 +40,7 @@ spec: content: valueFrom: secretKeyRef: - name: security-config-internal-users + name: security-config-file-internal-users key: internal_users.yml roles: managedBy: API @@ -334,76 +334,3 @@ spec: cluster.routing.allocation.disk.threshold_enabled: "false" plugins.security.authcz.admin_dn: CN=update-security-config.localhost plugins.security.restapi.roles_enabled: all_access - objectOverrides: - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: opensearch-nodes-security-config - namespace: $NAMESPACE - spec: - template: - spec: - volumes: - - name: security-config-file-actiongroups - configMap: - defaultMode: 0o660 - items: - - key: action_groups.yml - path: action_groups.yml - name: opensearch-nodes-security-config - - name: security-config-file-allowlist - configMap: - defaultMode: 0o660 - items: - - key: allow_list.yml - path: allow_list.yml - name: opensearch-nodes-security-config - - name: security-config-file-audit - configMap: - defaultMode: 0o660 - items: - - key: audit.yml - path: audit.yml - name: opensearch-nodes-security-config - - name: security-config-file-config - configMap: - defaultMode: 0o660 - items: - - key: config.yml - path: config.yml - name: opensearch-nodes-security-config - - name: security-config-file-internalusers - secret: - defaultMode: 0o660 - items: - - key: internal_users.yml - path: internal_users.yml - secretName: security-config-file-internal-users - - name: security-config-file-nodesdn - configMap: - defaultMode: 0o660 - items: - - key: nodes_dn.yml - path: nodes_dn.yml - name: opensearch-nodes-security-config - - name: security-config-file-roles - configMap: - defaultMode: 0o660 - items: - - key: roles.yml - path: roles.yml - name: security-config - - name: security-config-file-rolesmapping - configMap: - defaultMode: 0o660 - items: - - key: roles_mapping.yml - path: roles_mapping.yml - name: security-config - - name: security-config-file-tenants - configMap: - defaultMode: 0o660 - items: - - key: tenants.yml - path: tenants.yml - name: opensearch-nodes-security-config diff --git a/tests/templates/kuttl/security-config/21-assert.yaml b/tests/templates/kuttl/security-config/21-assert.yaml index 3176668..3779548 100644 --- a/tests/templates/kuttl/security-config/21-assert.yaml +++ b/tests/templates/kuttl/security-config/21-assert.yaml @@ -1,21 +1,12 @@ --- -apiVersion: v1 -kind: ConfigMap -metadata: - name: opensearch-nodes-cluster-manager -data: - action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate - via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":false}}}}' - nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' - tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 120 --- apiVersion: v1 kind: ConfigMap metadata: - name: opensearch-nodes-data + name: opensearch-nodes-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' @@ -24,4 +15,3 @@ data: via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":false}}}}' nodes_dn.yml: '{"_meta":{"config_version":2,"type":"nodesdn"}}' tenants.yml: '{"_meta":{"config_version":2,"type":"tenants"}}' - From dce4c408e0494bfb16cfc42e3de4cb876ce207dd Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 11 Feb 2026 13:28:57 +0100 Subject: [PATCH 06/53] Create admin certificate in init container --- .../controller/build/role_group_builder.rs | 133 +++++++++++++++++- .../11-install-opensearch.yaml.j2 | 80 +++-------- 2 files changed, 144 insertions(+), 69 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 1b54ff8..088a294 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -65,8 +65,8 @@ use crate::{ role_group_utils::ResourceNames, types::{ kubernetes::{ - ListenerName, PersistentVolumeClaimName, SecretClassName, ServiceAccountName, - ServiceName, VolumeName, + ContainerName, ListenerName, PersistentVolumeClaimName, SecretClassName, + ServiceAccountName, ServiceName, VolumeName, }, operator::RoleGroupName, }, @@ -87,7 +87,11 @@ constant!(DISCOVERY_SERVICE_LISTENER_VOLUME_NAME: PersistentVolumeClaimName = "d const DISCOVERY_SERVICE_LISTENER_VOLUME_DIR: &str = "/stackable/listeners/discovery-service"; constant!(TLS_SERVER_VOLUME_NAME: VolumeName = "tls-server"); +constant!(TLS_SERVER_CA_VOLUME_NAME: VolumeName = "tls-server-ca"); constant!(TLS_INTERNAL_VOLUME_NAME: VolumeName = "tls-internal"); +const TLS_SERVER_CA_VOLUME_SIZE: &str = "1Mi"; +constant!(TLS_ADMIN_CERT_VOLUME_NAME: VolumeName = "tls-admin-cert"); +const TLS_ADMIN_CERT_VOLUME_SIZE: &str = "1Mi"; constant!(LOG_VOLUME_NAME: VolumeName = "log"); const LOG_VOLUME_DIR: &str = "/stackable/log"; @@ -144,12 +148,26 @@ impl<'a> RoleGroupBuilder<'a> { } } - fn manages_security_config(&self) -> bool { + fn needs_security_config(&self) -> bool { self.cluster.security_config.enabled && (self.cluster.security_config.is_only_managed_by_api() || self.cluster.security_config.managing_role_group == self.role_group_name) } + fn manages_security_config(&self) -> bool { + self.cluster.security_config.enabled + && !self.cluster.security_config.is_only_managed_by_api() + && self.cluster.security_config.managing_role_group == self.role_group_name + } + + fn server_ca_volume(&self) -> VolumeName { + if self.manages_security_config() { + TLS_SERVER_CA_VOLUME_NAME.to_owned() + } else { + TLS_SERVER_VOLUME_NAME.to_owned() + } + } + /// Builds the [`ConfigMap`] containing the configuration files of the role-group /// [`StatefulSet`] pub fn build_config_map(&self) -> ConfigMap { @@ -182,7 +200,7 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if self.manages_security_config() { + if self.needs_security_config() { for file_type in SecurityConfigFileType::iter() { if let Some(value) = self.cluster.security_config.value(file_type) { data.insert(file_type.filename(), value.to_string()); @@ -309,6 +327,11 @@ impl<'a> RoleGroupBuilder<'a> { if let Some(keystore_init_container) = self.build_maybe_keystore_init_container() { init_containers.push(keystore_init_container); } + if let Some(admin_certificate_init_container) = + self.build_maybe_admin_certificate_init_container() + { + init_containers.push(admin_certificate_init_container); + } let log_config_volume_config_map = if let ValidatedContainerLogConfigChoice::Custom(config_map_name) = @@ -386,10 +409,33 @@ impl<'a> RoleGroupBuilder<'a> { )) }; - if self.manages_security_config() { + if self.server_ca_volume() != TLS_SERVER_VOLUME_NAME.to_owned() { + // An init container is responsible for creating the file ca.crt. + volumes.push(Volume { + name: self.server_ca_volume().to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }); + } + + if self.needs_security_config() { volumes.extend(self.security_config_volumes()); } + if self.manages_security_config() { + volumes.push(Volume { + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }); + } + if !self.cluster.keystores.is_empty() { volumes.push(Volume { name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), @@ -548,6 +594,66 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ) } + /// Builds the container for the [`PodTemplateSpec`] + fn build_maybe_admin_certificate_init_container(&self) -> Option { + if !self.manages_security_config() { + return None; + } + + let volume_mounts = vec![ + VolumeMount { + mount_path: "/stackable/tls-server/ca.crt".to_owned(), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("ca.crt".to_owned()), + read_only: Some(true), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: "/stackable/tls-admin-cert".to_owned(), + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + read_only: Some(false), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: "/stackable/tls-server-ca".to_owned(), + name: TLS_SERVER_CA_VOLUME_NAME.to_string(), + read_only: Some(false), + ..VolumeMount::default() + }, + ]; + + let container = new_container_builder( + &ContainerName::from_str("create-admin-certificate") + .expect("should be a valid container name"), + ) + .image_from_product_image(&self.cluster.image) + .command(vec![ + "/bin/bash".to_string(), + "-euxo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![format!( + "openssl req \ + -x509 \ + -nodes \ + -subj=/CN=update-security-config.localhost \ + -out=/stackable/tls-admin-cert/tls.crt \ + -keyout=/stackable/tls-admin-cert/tls.key + +cat \ + /stackable/tls-server/ca.crt \ + /stackable/tls-admin-cert/tls.crt > \ + /stackable/tls-server-ca/ca.crt", + )]) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources(self.role_group_config.config.resources.clone().into()) + .build(); + + Some(container) + } + /// Builds the container for the [`PodTemplateSpec`] fn build_opensearch_container(&self) -> Container { // Probe values taken from the official Helm chart @@ -625,13 +731,26 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO if self.cluster.tls_config.server_secret_class.is_some() { volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server"), + mount_path: format!("{opensearch_path_conf}/tls/server/tls.crt"), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.crt".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/tls.key"), name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.key".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), + name: self.server_ca_volume().to_string(), + sub_path: Some("ca.crt".to_owned()), ..VolumeMount::default() }); } - if self.manages_security_config() { + if self.needs_security_config() { volume_mounts.extend(self.security_config_volume_mounts()); } diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 37a81fe..684c2f2 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -97,52 +97,9 @@ spec: data: capacity: 100Mi replicas: 1 - configOverrides: - opensearch.yml: - plugins.security.ssl.http.pemtrustedcas_filepath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt podOverrides: spec: - initContainers: - - name: create-admin-certificate -{% if test_scenario['values']['opensearch'].find(",") > 0 %} - image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" -{% else %} - image: oci.stackable.tech/sdp/opensearch:{{ test_scenario['values']['opensearch'].split(',')[0] }}-stackable{{ test_scenario['values']['release'] }} -{% endif %} - command: - - /bin/bash - - -euxo - - pipefail - - -c - args: - - | - openssl req \ - -x509 \ - -nodes \ - -subj=/CN=update-security-config.localhost \ - -out=/stackable/tls-admin/tls.crt \ - -keyout=/stackable/tls-admin/tls.key - - cat \ - /stackable/tls-server/ca.crt \ - /stackable/tls-admin/tls.crt > \ - /stackable/pemtrustedcas/ca.crt - volumeMounts: - - mountPath: /stackable/tls-server - name: tls-server - readOnly: true - - mountPath: /stackable/tls-admin - name: admin-certificate - readOnly: false - - mountPath: /stackable/pemtrustedcas - name: pemtrustedcas - readOnly: false containers: - - name: opensearch - volumeMounts: - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas - name: pemtrustedcas - readOnly: true - name: update-security-config {% if test_scenario['values']['opensearch'].find(",") > 0 %} image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" @@ -189,9 +146,9 @@ spec: until plugins/opensearch-security/tools/securityadmin.sh \ --configdir {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security \ --disable-host-name-verification \ - -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt \ - -cert /stackable/tls-admin/tls.crt \ - -key /stackable/tls-admin/tls.key + -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt \ + -cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ + -key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key do echo "Initializing the security index failed." wait_seconds 10 @@ -203,8 +160,8 @@ spec: STATUS_CODE=$(curl \ --insecure \ - --cert /stackable/tls-admin/tls.crt \ - --key /stackable/tls-admin/tls.key \ + --cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ + --key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key \ --silent \ --output /dev/null \ --write-out "%{http_code}" \ @@ -236,9 +193,9 @@ spec: --type "$filetype" \ --file "$file" \ --disable-host-name-verification \ - -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas/ca.crt \ - -cert /stackable/tls-admin/tls.crt \ - -key /stackable/tls-admin/tls.key + -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt \ + -cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ + -key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key do echo "Updating \"$filetype\" in the security index failed." wait_seconds 10 @@ -300,22 +257,21 @@ spec: name: security-config-file-tenants readOnly: true subPath: tenants.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/pemtrustedcas - name: pemtrustedcas + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt + name: tls-server-ca + readOnly: true + subPath: ca.crt + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt + name: tls-admin-cert readOnly: true - - mountPath: /stackable/tls-admin - name: admin-certificate + subPath: tls.crt + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key + name: tls-admin-cert readOnly: true + subPath: tls.key - mountPath: /stackable/log name: log readOnly: false - volumes: - - name: admin-certificate - emptyDir: - sizeLimit: 1Mi - - name: pemtrustedcas - emptyDir: - sizeLimit: 1Mi envOverrides: # Only required for the official image # The official image (built with https://github.com/opensearch-project/opensearch-build) From f6387a9e4f85b664dd6355912eb75f296080359b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 11 Feb 2026 15:58:09 +0100 Subject: [PATCH 07/53] Add update-security-config container --- .../controller/build/role_group_builder.rs | 107 ++++++++-- .../build/update-security-config.sh | 108 ++++++++++ rust/operator-binary/src/crd/mod.rs | 2 +- .../11-install-opensearch.yaml.j2 | 184 ------------------ 4 files changed, 205 insertions(+), 196 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/update-security-config.sh 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 088a294..d481696 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -54,7 +54,7 @@ use crate::{ builder::{ meta::ownerreference_from_resource, pod::{ - container::new_container_builder, + container::{EnvVarName, EnvVarSet, new_container_builder}, volume::{ListenerReference, listener_operator_volume_source_builder_build_pvc}, }, }, @@ -148,10 +148,9 @@ impl<'a> RoleGroupBuilder<'a> { } } - fn needs_security_config(&self) -> bool { + fn initializes_security_config(&self) -> bool { self.cluster.security_config.enabled - && (self.cluster.security_config.is_only_managed_by_api() - || self.cluster.security_config.managing_role_group == self.role_group_name) + && self.cluster.security_config.is_only_managed_by_api() } fn manages_security_config(&self) -> bool { @@ -200,7 +199,7 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if self.needs_security_config() { + if self.initializes_security_config() || self.manages_security_config() { for file_type in SecurityConfigFileType::iter() { if let Some(value) = self.cluster.security_config.value(file_type) { data.insert(file_type.filename(), value.to_string()); @@ -322,6 +321,7 @@ impl<'a> RoleGroupBuilder<'a> { vector_config_file_extra_env_vars(), ) }); + let security_config_container = self.build_maybe_security_config_container(); let mut init_containers = vec![]; if let Some(keystore_init_container) = self.build_maybe_keystore_init_container() { @@ -421,7 +421,7 @@ impl<'a> RoleGroupBuilder<'a> { }); } - if self.needs_security_config() { + if self.initializes_security_config() || self.manages_security_config() { volumes.extend(self.security_config_volumes()); } @@ -481,10 +481,14 @@ impl<'a> RoleGroupBuilder<'a> { .pod_anti_affinity .clone(), }), - containers: [Some(opensearch_container), vector_container] - .into_iter() - .flatten() - .collect(), + containers: [ + Some(opensearch_container), + vector_container, + security_config_container, + ] + .into_iter() + .flatten() + .collect(), init_containers: Some(init_containers), node_selector: self .role_group_config @@ -654,6 +658,87 @@ cat \ Some(container) } + /// Builds the container for the [`PodTemplateSpec`] + fn build_maybe_security_config_container(&self) -> Option { + if !self.manages_security_config() { + return None; + } + + let opensearch_path_conf = self.node_config.opensearch_path_conf(); + + let mut volume_mounts = vec![ + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/tls.crt"), + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("tls.crt".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/tls.key"), + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("tls.key".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/ca.crt"), + name: TLS_SERVER_CA_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("ca.crt".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: LOG_VOLUME_DIR.to_owned(), + name: LOG_VOLUME_NAME.to_string(), + ..VolumeMount::default() + }, + ]; + volume_mounts.extend(self.security_config_volume_mounts()); + + let mut env_vars = EnvVarSet::new().with_value( + &EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"), + opensearch_path_conf, + ); + + for file_type in SecurityConfigFileType::iter() { + let managed_by_operator = self + .cluster + .security_config + .security_config(file_type) + .managed_by + == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; + + env_vars = env_vars.with_value( + &EnvVarName::from_str_unsafe(&format!( + "MANAGE_{}", + file_type.volume_name().to_uppercase() + )), + managed_by_operator.to_string(), + ); + } + + let container = new_container_builder( + &ContainerName::from_str("update-security-config") + .expect("should be a valid container name"), + ) + .image_from_product_image(&self.cluster.image) + .command(vec![ + "/bin/bash".to_string(), + "-uo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![include_str!("update-security-config.sh").to_owned()]) + .add_env_vars(env_vars.into()) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources(self.role_group_config.config.resources.clone().into()) + .build(); + + Some(container) + } + /// Builds the container for the [`PodTemplateSpec`] fn build_opensearch_container(&self) -> Container { // Probe values taken from the official Helm chart @@ -750,7 +835,7 @@ cat \ }); } - if self.needs_security_config() { + if self.initializes_security_config() { volume_mounts.extend(self.security_config_volume_mounts()); } diff --git a/rust/operator-binary/src/controller/build/update-security-config.sh b/rust/operator-binary/src/controller/build/update-security-config.sh new file mode 100644 index 0000000..3ad6cfc --- /dev/null +++ b/rust/operator-binary/src/controller/build/update-security-config.sh @@ -0,0 +1,108 @@ +function wait_seconds () { + seconds="$1" + + if test "$seconds" = 0 + then + echo "Wait until pod is restarted..." + else + echo "Wait for $seconds seconds..." + fi + + if test ! -e /stackable/log/_vector/shutdown + then + mkdir --parents /stackable/log/_vector + inotifywait \ + --quiet --quiet \ + --timeout $seconds \ + --event create \ + /stackable/log/_vector + fi + + if test -e /stackable/log/_vector/shutdown + then + echo "Shut down" + exit 0 + fi +} + +function initialize_security_index() { + echo "Initialize the security index." + + until plugins/opensearch-security/tools/securityadmin.sh \ + --configdir "$OPENSEARCH_PATH_CONF/opensearch-security" \ + --disable-host-name-verification \ + -cacert "$OPENSEARCH_PATH_CONF/tls/ca.crt" \ + -cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ + -key "$OPENSEARCH_PATH_CONF/tls/tls.key" + do + echo "Initializing the security index failed." + wait_seconds 10 + done +} + +function check_security_index() { + echo "Check the status of the security index." + + STATUS_CODE=$(curl \ + --insecure \ + --cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ + --key "$OPENSEARCH_PATH_CONF/tls/tls.key" \ + --silent \ + --output /dev/null \ + --write-out "%{http_code}" \ + https://localhost:9200/.opendistro_security) + if test "$STATUS_CODE" = "200" + then + echo "The security index is already initialized." + elif test "$STATUS_CODE" = "404" + then + initialize_security_index + else + echo "Checking the security index failed." + wait_seconds 10 + check_security_index + fi +} + +function update_config () { + filetype="$1" + filename="$2" + + file="$OPENSEARCH_PATH_CONF/opensearch-security/$filename" + + envvar="MANAGE_${filetype^^}" + if test "${!envvar}" = "true" + then + echo "Update managed configuration type \"$filetype\"." + + until plugins/opensearch-security/tools/securityadmin.sh \ + --type "$filetype" \ + --file "$file" \ + --disable-host-name-verification \ + -cacert "$OPENSEARCH_PATH_CONF/tls/ca.crt" \ + -cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ + -key "$OPENSEARCH_PATH_CONF/tls/tls.key" + do + echo "Updating \"$filetype\" in the security index failed." + wait_seconds 10 + done + else + echo "Skip unmanaged configuration type \"$filetype\"." + fi +} + +check_security_index + +update_config actiongroups action_groups.yml +update_config allowlist allowlist.yml +update_config audit audit.yml +update_config config config.yml +update_config internalusers internal_users.yml +update_config nodesdn nodes_dn.yml +update_config roles roles.yml +update_config rolesmapping roles_mapping.yml +update_config tenants tenants.yml + +echo "Wait for security configuration changes..." +# Wait until the pod is restarted due to a change of the Secret. +wait_seconds 0 diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 99a1acc..918b653 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -627,7 +627,7 @@ impl SecurityConfigFileType { } impl v1alpha1::SecurityConfig { - fn security_config( + pub fn security_config( &self, file_type: SecurityConfigFileType, ) -> &v1alpha1::SecurityConfigFileType { diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 684c2f2..b85de17 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -88,190 +88,6 @@ spec: data: capacity: 2Gi replicas: 2 - security-config: - config: - discoveryServiceExposed: false - nodeRoles: [] - resources: - storage: - data: - capacity: 100Mi - replicas: 1 - podOverrides: - spec: - containers: - - name: update-security-config -{% if test_scenario['values']['opensearch'].find(",") > 0 %} - image: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" -{% else %} - image: oci.stackable.tech/sdp/opensearch:{{ test_scenario['values']['opensearch'].split(',')[0] }}-stackable{{ test_scenario['values']['release'] }} -{% endif %} - command: - - /bin/bash - - -uo - - pipefail - - -c - args: - - | - function wait_seconds () { - seconds="$1" - - if test "$seconds" = 0 - then - echo "Wait until pod is restarted..." - else - echo "Wait for $seconds seconds..." - fi - - if test ! -e /stackable/log/_vector/shutdown - then - mkdir --parents /stackable/log/_vector - inotifywait \ - --quiet --quiet \ - --timeout $seconds \ - --event create \ - /stackable/log/_vector - fi - - if test -e /stackable/log/_vector/shutdown - then - echo "Shut down" - exit 0 - fi - } - - function initialize_security_index() { - echo "Initialize the security index." - - until plugins/opensearch-security/tools/securityadmin.sh \ - --configdir {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security \ - --disable-host-name-verification \ - -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt \ - -cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ - -key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - do - echo "Initializing the security index failed." - wait_seconds 10 - done - } - - function check_security_index() { - echo "Check the status of the security index." - - STATUS_CODE=$(curl \ - --insecure \ - --cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ - --key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key \ - --silent \ - --output /dev/null \ - --write-out "%{http_code}" \ - https://localhost:9200/.opendistro_security) - if test "$STATUS_CODE" = "200" - then - echo "The security index is already initialized." - elif test "$STATUS_CODE" = "404" - then - initialize_security_index - else - echo "Checking the security index failed." - wait_seconds 10 - check_security_index - fi - } - - function update_config () { - filetype="$1" - filename="$2" - - file="{{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/$filename" - - if test -e "$file" - then - echo "Update managed configuration type \"$filetype\"." - - until plugins/opensearch-security/tools/securityadmin.sh \ - --type "$filetype" \ - --file "$file" \ - --disable-host-name-verification \ - -cacert {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt \ - -cert {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt \ - -key {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - do - echo "Updating \"$filetype\" in the security index failed." - wait_seconds 10 - done - else - echo "Skip unmanaged configuration type \"$filetype\"." - fi - } - - check_security_index - - update_config actiongroups action_groups.yml - update_config allowlist allowlist.yml - update_config audit audit.yml - update_config config config.yml - update_config internalusers internal_users.yml - update_config nodesdn nodes_dn.yml - update_config roles roles.yml - update_config rolesmapping roles_mapping.yml - update_config tenants tenants.yml - - echo "Wait for security configuration changes..." - # Wait until the pod is restarted due to a change of the Secret. - wait_seconds 0 - volumeMounts: - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml - name: security-config-file-actiongroups - readOnly: true - subPath: action_groups.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml - name: security-config-file-allowlist - readOnly: true - subPath: allow_list.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml - name: security-config-file-audit - readOnly: true - subPath: audit.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/config.yml - name: security-config-file-config - readOnly: true - subPath: config.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/internal_users.yml - name: security-config-file-internalusers - readOnly: true - subPath: internal_users.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/nodes_dn.yml - name: security-config-file-nodesdn - readOnly: true - subPath: nodes_dn.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles.yml - name: security-config-file-roles - readOnly: true - subPath: roles.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/roles_mapping.yml - name: security-config-file-rolesmapping - readOnly: true - subPath: roles_mapping.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/tenants.yml - name: security-config-file-tenants - readOnly: true - subPath: tenants.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/ca.crt - name: tls-server-ca - readOnly: true - subPath: ca.crt - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.crt - name: tls-admin-cert - readOnly: true - subPath: tls.crt - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/tls.key - name: tls-admin-cert - readOnly: true - subPath: tls.key - - mountPath: /stackable/log - name: log - readOnly: false envOverrides: # Only required for the official image # The official image (built with https://github.com/opensearch-project/opensearch-build) From fefcfdbc880e1be7e97498b410d4b303f000261c Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 11 Feb 2026 17:17:49 +0100 Subject: [PATCH 08/53] Configure DN of the admin certificate --- .../build/create-admin-certificate.sh | 11 ++++++++ .../src/controller/build/node_config.rs | 28 +++++++++++++++++++ .../controller/build/role_group_builder.rs | 17 +++-------- .../11-install-opensearch.yaml.j2 | 2 +- 4 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/create-admin-certificate.sh diff --git a/rust/operator-binary/src/controller/build/create-admin-certificate.sh b/rust/operator-binary/src/controller/build/create-admin-certificate.sh new file mode 100644 index 0000000..f71afd9 --- /dev/null +++ b/rust/operator-binary/src/controller/build/create-admin-certificate.sh @@ -0,0 +1,11 @@ +openssl req \ + -x509 \ + -nodes \ + -subj=/$ADMIN_DN \ + -out=/stackable/tls-admin-cert/tls.crt \ + -keyout=/stackable/tls-admin-cert/tls.key + +cat \ + /stackable/tls-server/ca.crt \ + /stackable/tls-admin-cert/tls.crt > \ + /stackable/tls-server-ca/ca.crt diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index fdb291e..1760d7a 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -78,6 +78,10 @@ const CONFIG_OPTION_PATH_LOGS: &str = "path.logs"; const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = "plugins.security.allow_default_init_securityindex"; +/// Defines the DNs of certificates to which admin privileges should be assigned. +/// Type: (comma-separated) list of strings +const CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN: &str = "plugins.security.authcz.admin_dn"; + /// Specifies a list of distinguished names (DNs) that denote the other nodes in the cluster. /// Type: (comma-separated) list of strings const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; @@ -209,6 +213,14 @@ impl NodeConfig { CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), json!(self.cluster.security_config.is_only_managed_by_api()), ); + if self.cluster.security_config.enabled + && !self.cluster.security_config.is_only_managed_by_api() + { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), + json!(self.admin_dn()), + ); + } config.insert // Accept certificates generated by the secret-operator ( @@ -230,6 +242,22 @@ impl NodeConfig { config } + pub fn admin_dn(&self) -> Option { + if self.cluster.security_config.enabled + && !self.cluster.security_config.is_only_managed_by_api() + { + Some(format!( + "CN={container}.{pod}.{namespace}.{cluster_domain_name}", + container = "update-security-config", + pod = self.cluster.security_config.managing_role_group, + namespace = self.cluster.namespace, + cluster_domain_name = self.cluster_domain_name + )) + } else { + None + } + } + pub fn tls_config(&self) -> serde_json::Map { let mut config = serde_json::Map::new(); let opensearch_path_conf = self.opensearch_path_conf(); 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 d481696..10d6c6c 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -604,6 +604,8 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO return None; } + let admin_dn = self.node_config.admin_dn().expect(""); + let volume_mounts = vec![ VolumeMount { mount_path: "/stackable/tls-server/ca.crt".to_owned(), @@ -637,19 +639,8 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO "pipefail".to_string(), "-c".to_string(), ]) - .args(vec![format!( - "openssl req \ - -x509 \ - -nodes \ - -subj=/CN=update-security-config.localhost \ - -out=/stackable/tls-admin-cert/tls.crt \ - -keyout=/stackable/tls-admin-cert/tls.key - -cat \ - /stackable/tls-server/ca.crt \ - /stackable/tls-admin-cert/tls.crt > \ - /stackable/tls-server-ca/ca.crt", - )]) + .args(vec![include_str!("create-admin-certificate.sh").to_owned()]) + .add_env_var("ADMIN_DN", admin_dn) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") .resources(self.role_group_config.config.resources.clone().into()) diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index b85de17..5e5f4dd 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -104,5 +104,5 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # not be created even if enough disk space would be available. cluster.routing.allocation.disk.threshold_enabled: "false" - plugins.security.authcz.admin_dn: CN=update-security-config.localhost + # Allows the test jobs to access the security REST API. plugins.security.restapi.roles_enabled: all_access From 5aa8f8b8cbd0b0e942b3d00339b980fea55bab02 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 12 Feb 2026 12:36:38 +0100 Subject: [PATCH 09/53] Allow only one pod to manage the security configuration --- .../build/create-admin-certificate.sh | 69 ++++++++-- .../controller/build/role_group_builder.rs | 60 +++++++-- .../build/update-security-config.sh | 125 ++++++++++++------ 3 files changed, 191 insertions(+), 63 deletions(-) diff --git a/rust/operator-binary/src/controller/build/create-admin-certificate.sh b/rust/operator-binary/src/controller/build/create-admin-certificate.sh index f71afd9..3bc2ea5 100644 --- a/rust/operator-binary/src/controller/build/create-admin-certificate.sh +++ b/rust/operator-binary/src/controller/build/create-admin-certificate.sh @@ -1,11 +1,58 @@ -openssl req \ - -x509 \ - -nodes \ - -subj=/$ADMIN_DN \ - -out=/stackable/tls-admin-cert/tls.crt \ - -keyout=/stackable/tls-admin-cert/tls.key - -cat \ - /stackable/tls-server/ca.crt \ - /stackable/tls-admin-cert/tls.crt > \ - /stackable/tls-server-ca/ca.crt +function log () { + level="$1" + message="$2" + + timestamp="$(date --utc +"%FT%T.%3NZ")" + echo "$timestamp [$level] $message" +} + +function info () { + message="$@" + + log INFO "$message" +} + +function check_pod () { + POD_INDEX="${POD_NAME##*-}" + + if test "$POD_INDEX" = "0" + then + info "This pod is responsible for managing the security" \ + "configuration." + else + MANAGING_POD="${POD_NAME%-*}-0" + info "This pod is not responsible for managing the security" \ + "configuration, as such an admin certificate is not " \ + "required. The security configuration is managed by the " \ + "pod \"$MANAGING_POD\"." + + cp \ + /stackable/tls-server/ca.crt \ + /stackable/tls-server-ca/ca.crt + exit 0 + fi +} + +function create_admin_certificate () { + info "Create admin certificate with \"$ADMIN_DN\"" + + openssl req \ + -x509 \ + -nodes \ + -subj=/$ADMIN_DN \ + -out=/stackable/tls-admin-cert/tls.crt \ + -keyout=/stackable/tls-admin-cert/tls.key +} + +function concatenate_certificates () { + info "Add admin certificate to the trusted CAs" + + cat \ + /stackable/tls-server/ca.crt \ + /stackable/tls-admin-cert/tls.crt > \ + /stackable/tls-server-ca/ca.crt +} + +check_pod +create_admin_certificate +concatenate_certificates 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 10d6c6c..941ba58 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -5,8 +5,12 @@ use std::{collections::BTreeMap, str::FromStr}; use stackable_operator::{ builder::{ meta::ObjectMetaBuilder, - pod::volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + pod::{ + container::FieldPathEnvVar, + volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, + }, }, + commons::resources::{CpuLimits, MemoryLimits, Resources}, constants::RESTART_CONTROLLER_ENABLED_LABEL, crd::listener::{self}, k8s_openapi::{ @@ -606,6 +610,13 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO let admin_dn = self.node_config.admin_dn().expect(""); + let env_vars = EnvVarSet::new() + .with_value(&EnvVarName::from_str_unsafe("ADMIN_DN"), admin_dn) + .with_field_path( + &EnvVarName::from_str_unsafe("POD_NAME"), + FieldPathEnvVar::Name, + ); + let volume_mounts = vec![ VolumeMount { mount_path: "/stackable/tls-server/ca.crt".to_owned(), @@ -635,15 +646,28 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .image_from_product_image(&self.cluster.image) .command(vec![ "/bin/bash".to_string(), - "-euxo".to_string(), + "-euo".to_string(), "pipefail".to_string(), "-c".to_string(), ]) .args(vec![include_str!("create-admin-certificate.sh").to_owned()]) - .add_env_var("ADMIN_DN", admin_dn) + .add_env_vars(env_vars.into()) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") - .resources(self.role_group_config.config.resources.clone().into()) + .resources( + Resources::<()> { + memory: MemoryLimits { + limit: Some(Quantity("128Mi".to_owned())), + ..MemoryLimits::default() + }, + cpu: CpuLimits { + min: Some(Quantity("100m".to_owned())), + max: Some(Quantity("400m".to_owned())), + }, + ..Resources::default() + } + .into(), + ) .build(); Some(container) @@ -687,10 +711,15 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ]; volume_mounts.extend(self.security_config_volume_mounts()); - let mut env_vars = EnvVarSet::new().with_value( - &EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"), - opensearch_path_conf, - ); + let mut env_vars = EnvVarSet::new() + .with_value( + &EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"), + opensearch_path_conf, + ) + .with_field_path( + &EnvVarName::from_str_unsafe("POD_NAME"), + FieldPathEnvVar::Name, + ); for file_type in SecurityConfigFileType::iter() { let managed_by_operator = self @@ -724,7 +753,20 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .add_env_vars(env_vars.into()) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") - .resources(self.role_group_config.config.resources.clone().into()) + .resources( + Resources::<()> { + memory: MemoryLimits { + limit: Some(Quantity("512Mi".to_owned())), + ..MemoryLimits::default() + }, + cpu: CpuLimits { + min: Some(Quantity("100m".to_owned())), + max: Some(Quantity("400m".to_owned())), + }, + ..Resources::default() + } + .into(), + ) .build(); Some(container) diff --git a/rust/operator-binary/src/controller/build/update-security-config.sh b/rust/operator-binary/src/controller/build/update-security-config.sh index 3ad6cfc..4859ab0 100644 --- a/rust/operator-binary/src/controller/build/update-security-config.sh +++ b/rust/operator-binary/src/controller/build/update-security-config.sh @@ -1,11 +1,31 @@ +function log () { + level="$1" + message="$2" + + timestamp="$(date --utc +"%FT%T.%3NZ")" + echo "$timestamp [$level] $message" +} + +function info () { + message="$@" + + log INFO "$message" +} + +function warn () { + message="$1" + + log WARN "$message" +} + function wait_seconds () { seconds="$1" if test "$seconds" = 0 then - echo "Wait until pod is restarted..." + info "Wait until pod is restarted..." else - echo "Wait for $seconds seconds..." + info "Wait for $seconds seconds..." fi if test ! -e /stackable/log/_vector/shutdown @@ -20,13 +40,30 @@ function wait_seconds () { if test -e /stackable/log/_vector/shutdown then - echo "Shut down" + info "Shut down" exit 0 fi } +function check_pod () { + POD_INDEX="${POD_NAME##*-}" + + if test "$POD_INDEX" = "0" + then + info "This pod is responsible for managing the security" \ + "configuration." + else + MANAGING_POD="${POD_NAME%-*}-0" + info "This pod is not responsible for managing the security" \ + "configuration. The security configuration is managed by" \ + "the pod \"$MANAGING_POD\"." + + wait_seconds 0 + fi +} + function initialize_security_index() { - echo "Initialize the security index." + info "Initialize the security index." until plugins/opensearch-security/tools/securityadmin.sh \ --configdir "$OPENSEARCH_PATH_CONF/opensearch-security" \ @@ -35,35 +72,11 @@ function initialize_security_index() { -cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ -key "$OPENSEARCH_PATH_CONF/tls/tls.key" do - echo "Initializing the security index failed." + warn "Initializing the security index failed." wait_seconds 10 done } -function check_security_index() { - echo "Check the status of the security index." - - STATUS_CODE=$(curl \ - --insecure \ - --cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ - --key "$OPENSEARCH_PATH_CONF/tls/tls.key" \ - --silent \ - --output /dev/null \ - --write-out "%{http_code}" \ - https://localhost:9200/.opendistro_security) - if test "$STATUS_CODE" = "200" - then - echo "The security index is already initialized." - elif test "$STATUS_CODE" = "404" - then - initialize_security_index - else - echo "Checking the security index failed." - wait_seconds 10 - check_security_index - fi -} - function update_config () { filetype="$1" filename="$2" @@ -73,7 +86,7 @@ function update_config () { envvar="MANAGE_${filetype^^}" if test "${!envvar}" = "true" then - echo "Update managed configuration type \"$filetype\"." + info "Update managed configuration type \"$filetype\"." until plugins/opensearch-security/tools/securityadmin.sh \ --type "$filetype" \ @@ -83,26 +96,52 @@ function update_config () { -cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ -key "$OPENSEARCH_PATH_CONF/tls/tls.key" do - echo "Updating \"$filetype\" in the security index failed." + warn "Updating \"$filetype\" in the security index failed." wait_seconds 10 done else - echo "Skip unmanaged configuration type \"$filetype\"." + info "Skip unmanaged configuration type \"$filetype\"." + fi +} + +function update_security_index() { + info "Check the status of the security index." + + STATUS_CODE=$(curl \ + --insecure \ + --cert "$OPENSEARCH_PATH_CONF/tls/tls.crt" \ + --key "$OPENSEARCH_PATH_CONF/tls/tls.key" \ + --silent \ + --output /dev/null \ + --write-out "%{http_code}" \ + https://localhost:9200/.opendistro_security) + if test "$STATUS_CODE" = "200" + then + info "The security index is already initialized." + + update_config actiongroups action_groups.yml + update_config allowlist allowlist.yml + update_config audit audit.yml + update_config config config.yml + update_config internalusers internal_users.yml + update_config nodesdn nodes_dn.yml + update_config roles roles.yml + update_config rolesmapping roles_mapping.yml + update_config tenants tenants.yml + elif test "$STATUS_CODE" = "404" + then + initialize_security_index + else + warn "Checking the security index failed." + wait_seconds 10 + check_security_index fi } -check_security_index +check_pod -update_config actiongroups action_groups.yml -update_config allowlist allowlist.yml -update_config audit audit.yml -update_config config config.yml -update_config internalusers internal_users.yml -update_config nodesdn nodes_dn.yml -update_config roles roles.yml -update_config rolesmapping roles_mapping.yml -update_config tenants tenants.yml +update_security_index -echo "Wait for security configuration changes..." +info "Wait for security configuration changes..." # Wait until the pod is restarted due to a change of the Secret. wait_seconds 0 From d9e4787a236bb74786081a98c24f549999348122 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 12 Feb 2026 15:34:23 +0100 Subject: [PATCH 10/53] Validate the security configuration; Fix all unit tests --- .../src/controller/build/node_config.rs | 1 + .../controller/build/role_group_builder.rs | 201 +++++++++++++++++- .../src/controller/validate.rs | 95 ++++++++- rust/operator-binary/src/main.rs | 3 + .../11-install-opensearch.yaml.j2 | 2 + 5 files changed, 289 insertions(+), 13 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 1760d7a..8fb0368 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -669,6 +669,7 @@ mod tests { "network.host: \"0.0.0.0\"\n", "node.attr.role-group: \"data\"\n", "path.logs: \"/stackable/log/opensearch\"\n", + "plugins.security.allow_default_init_securityindex: true\n", "plugins.security.nodes_dn: [\"CN=generated certificate for pod\"]\n", "plugins.security.ssl.http.enabled: true\n", "plugins.security.ssl.http.pemcert_filepath: \"/stackable/opensearch/config/tls/server/tls.crt\"\n", 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 941ba58..15e09e5 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -1399,8 +1399,17 @@ mod tests { ] }, "data": { - "log4j2.properties": null, - "opensearch.yml": null, + "action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}", + "allow_list.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\"}}", + "log4j2.properties": null, + "nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}", + "opensearch.yml": null, + "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\"}}", "vector.yaml": null } }), @@ -1633,13 +1642,79 @@ mod tests { }, { "mountPath": "/stackable/opensearch/config/tls/server", + "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", "name": "tls-server", + "subPath": "tls.crt" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/tls.key", + "name": "tls-server", + "subPath": "tls.key" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/ca.crt", + "name": "tls-server", + "subPath": "ca.crt" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/action_groups.yml", + "name": "security-config-file-actiongroups", + "readOnly": true, + "subPath": "action_groups.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", + "name": "security-config-file-allowlist", + "readOnly": true, + "subPath": "allow_list.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", + "name": "security-config-file-audit", + "readOnly": true, + "subPath": "audit.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/config.yml", + "name": "security-config-file-config", + "readOnly": true, + "subPath": "config.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/internal_users.yml", + "name": "security-config-file-internalusers", + "readOnly": true, + "subPath": "internal_users.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/nodes_dn.yml", + "name": "security-config-file-nodesdn", + "readOnly": true, + "subPath": "nodes_dn.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles.yml", + "name": "security-config-file-roles", + "readOnly": true, + "subPath": "roles.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles_mapping.yml", + "name": "security-config-file-rolesmapping", + "readOnly": true, + "subPath": "roles_mapping.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/tenants.yml", + "name": "security-config-file-tenants", + "readOnly": true, + "subPath": "tenants.yml" }, { "mountPath": "/stackable/opensearch/config/opensearch.keystore", "name": "keystore", "readOnly": true, - "subPath": "opensearch.keystore", + "subPath": "opensearch.keystore" } ] }, @@ -1855,7 +1930,125 @@ mod tests { } }, "name": "tls-server" - }, + }, + { + "configMap": { + "items": [ + { + "key": "action_groups.yml", + "mode": 0o660, + "path": "action_groups.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-actiongroups" + }, + { + "configMap": { + "items": [ + { + "key": "allow_list.yml", + "mode": 0o660, + "path": "allow_list.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-allowlist" + }, + { + "configMap": { + "items": [ + { + "key": "audit.yml", + "mode": 0o660, + "path": "audit.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-audit" + }, + { + "configMap": { + "items": [ + { + "key": "config.yml", + "mode": 0o660, + "path": "config.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-config" + }, + { + "configMap": { + "items": [ + { + "key": "internal_users.yml", + "mode": 0o660, + "path": "internal_users.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-internalusers" + }, + { + "configMap": { + "items": [ + { + "key": "nodes_dn.yml", + "mode": 0o660, + "path": "nodes_dn.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-nodesdn" + }, + { + "configMap": { + "items": [ + { + "key": "roles.yml", + "mode": 0o660, + "path": "roles.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-roles" + }, + { + "configMap": { + "items": [ + { + "key": "roles_mapping.yml", + "mode": 0o660, + "path": "roles_mapping.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-rolesmapping" + }, + { + "configMap": { + "items": [ + { + "key": "tenants.yml", + "mode": 0o660, + "path": "tenants.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-tenants" + }, + { "emptyDir": { "sizeLimit": "1Mi" diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 28fb8f6..49648db 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeMap, str::FromStr}; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu, ensure}; use stackable_operator::{ crd::listener, kube::ResourceExt, product_logging::spec::Logging, role_utils::RoleGroup, shared::time::Duration, @@ -34,6 +34,18 @@ use crate::{ #[derive(Snafu, Debug, EnumDiscriminants)] #[strum_discriminants(derive(IntoStaticStr))] pub enum Error { + #[snafu(display( + "The role group that is defined to manage the security configuration, is not specified." + ))] + CheckSecurityConfigManagingRoleGroup { + security_config_managing_role_group: RoleGroupName, + }, + + #[snafu(display( + "A TLS server SecretClass must be set if the security configuration is managed by the operator." + ))] + CheckSecurityConfigTlsSettings {}, + #[snafu(display("failed to get the cluster name"))] GetClusterName { source: crate::framework::controller_utils::Error, @@ -87,6 +99,12 @@ pub enum Error { source: stackable_operator::commons::product_image_selection::Error, }, + #[snafu(display("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64)))] + TerminationGracePeriodTooLong { + source: std::num::TryFromIntError, + duration: Duration, + }, + #[snafu(display("failed to validate the logging configuration"))] ValidateLoggingConfig { source: crate::framework::product_logging::framework::Error, @@ -96,12 +114,6 @@ pub enum Error { ValidateOpenSearchConfig { source: stackable_operator::config::fragment::ValidationError, }, - - #[snafu(display("termination grace period is too long (got {duration}, maximum allowed is {max})", max = Duration::from_secs(i64::MAX as u64)))] - TerminationGracePeriodTooLong { - source: std::num::TryFromIntError, - duration: Duration, - }, } type Result = std::result::Result; @@ -146,6 +158,8 @@ pub fn validate( role_group_configs.insert(role_group_name, validated_role_group_config); } + validate_security_config(&cluster.spec)?; + let validated_discovery_endpoint = validate_discovery_endpoint( dereferenced_objects .maybe_discovery_service_listener @@ -264,6 +278,31 @@ fn validate_logging_configuration( }) } +fn validate_security_config(spec: &v1alpha1::OpenSearchClusterSpec) -> Result<()> { + if spec.cluster_config.security_config.enabled { + let security_config_managing_role_group = spec + .cluster_config + .security_config + .managing_role_group + .clone(); + ensure!( + spec.nodes + .role_groups + .contains_key(&security_config_managing_role_group.to_string()), + CheckSecurityConfigManagingRoleGroupSnafu { + security_config_managing_role_group + } + ); + + ensure!( + spec.cluster_config.security_config.is_only_managed_by_api() + || spec.cluster_config.tls.server_secret_class.is_some(), + CheckSecurityConfigTlsSettingsSnafu {} + ); + } + Ok(()) +} + /// Return the validated discovery endpoint if a Listener is given with a status containing the /// endpoint fn validate_discovery_endpoint( @@ -602,7 +641,11 @@ mod tests { server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), internal_secret_class: SecretClassName::from_str_unsafe("tls") }, - v1alpha1::SecurityConfig::default(), + v1alpha1::SecurityConfig { + enabled: true, + managing_role_group: RoleGroupName::from_str_unsafe("default"), + ..v1alpha1::SecurityConfig::default() + }, vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { @@ -802,6 +845,36 @@ mod tests { ); } + #[test] + fn test_validate_err_check_security_config_managing_role_group() { + test_validate_err( + |cluster, _| { + cluster + .spec + .cluster_config + .security_config + .managing_role_group = RoleGroupName::from_str_unsafe("non-existent"); + }, + ErrorDiscriminants::CheckSecurityConfigManagingRoleGroup, + ); + } + + #[test] + fn test_validate_err_check_security_config_tls_settings() { + test_validate_err( + |cluster, _| { + cluster + .spec + .cluster_config + .security_config + .config + .managed_by = v1alpha1::SecurityConfigFileTypeManagedBy::Operator; + cluster.spec.cluster_config.tls.server_secret_class = None; + }, + ErrorDiscriminants::CheckSecurityConfigTlsSettings, + ); + } + fn test_validate_err( change_test_objects: fn(&mut v1alpha1::OpenSearchCluster, &mut DereferencedObjects) -> (), expected_err: ErrorDiscriminants, @@ -845,7 +918,11 @@ mod tests { key: SecretKey::from_str_unsafe("my-keystore-file"), }, }], - security_config: v1alpha1::SecurityConfig::default(), + security_config: v1alpha1::SecurityConfig { + enabled: true, + managing_role_group: RoleGroupName::from_str_unsafe("default"), + ..v1alpha1::SecurityConfig::default() + }, vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( "vector-aggregator", )), diff --git a/rust/operator-binary/src/main.rs b/rust/operator-binary/src/main.rs index 82bffdc..0a30ca5 100644 --- a/rust/operator-binary/src/main.rs +++ b/rust/operator-binary/src/main.rs @@ -1,3 +1,6 @@ +// Increase the recursion limit because some unit tests use large JSON structures. +#![recursion_limit = "256"] + use std::{str::FromStr, sync::Arc}; use clap::Parser as _; diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 5e5f4dd..16766c7 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -56,6 +56,8 @@ spec: configMapKeyRef: name: security-config key: roles_mapping.yml + tls: + serverSecretClass: null {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} From 5d5cf8a45f9ae7dc990d2fe1306e6ab9d2921d64 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 13 Feb 2026 09:52:28 +0100 Subject: [PATCH 11/53] Allow to disable the security plugin --- .../src/controller/build/node_config.rs | 8 ++ .../src/controller/build/role_builder.rs | 4 +- .../kuttl/ldap/30-test-opensearch.yaml | 2 +- .../11-install-opensearch.yaml.j2 | 2 - .../kuttl/security-disabled/00-patch-ns.yaml | 15 +++ .../kuttl/security-disabled/01-rbac.yaml | 31 ++++++ .../kuttl/security-disabled/02-assert.yaml.j2 | 10 ++ ...or-aggregator-discovery-config-map.yaml.j2 | 9 ++ .../03-create-truststore.yaml | 9 ++ .../kuttl/security-disabled/10-assert.yaml | 12 +++ .../10-install-opensearch.yaml.j2 | 48 +++++++++ .../kuttl/security-disabled/20-assert.yaml | 11 ++ .../security-disabled/20-test-opensearch.yaml | 100 ++++++++++++++++++ tests/test-definition.yaml | 6 ++ 14 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 tests/templates/kuttl/security-disabled/00-patch-ns.yaml create mode 100644 tests/templates/kuttl/security-disabled/01-rbac.yaml create mode 100644 tests/templates/kuttl/security-disabled/02-assert.yaml.j2 create mode 100644 tests/templates/kuttl/security-disabled/02-install-vector-aggregator-discovery-config-map.yaml.j2 create mode 100644 tests/templates/kuttl/security-disabled/03-create-truststore.yaml create mode 100644 tests/templates/kuttl/security-disabled/10-assert.yaml create mode 100644 tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 create mode 100644 tests/templates/kuttl/security-disabled/20-assert.yaml create mode 100644 tests/templates/kuttl/security-disabled/20-test-opensearch.yaml diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 8fb0368..90bcc6b 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -82,6 +82,10 @@ const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = /// Type: (comma-separated) list of strings const CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN: &str = "plugins.security.authcz.admin_dn"; +/// Disables the security plugin +/// Type: boolean +const CONFIG_OPTION_PLUGINS_SECURITY_DISABLED: &str = "plugins.security.disabled"; + /// Specifies a list of distinguished names (DNs) that denote the other nodes in the cluster. /// Type: (comma-separated) list of strings const CONFIG_OPTION_PLUGINS_SECURITY_NODES_DN: &str = "plugins.security.nodes_dn"; @@ -221,6 +225,10 @@ impl NodeConfig { json!(self.admin_dn()), ); } + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_DISABLED.to_owned(), + json!(!self.cluster.security_config.enabled), + ); config.insert // Accept certificates generated by the secret-operator ( diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index f332944..87c2773 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -171,7 +171,9 @@ impl<'a> RoleBuilder<'a> { let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name)); - let protocol = if self.cluster.tls_config.server_secret_class.is_some() { + let protocol = if self.cluster.security_config.enabled + && self.cluster.tls_config.server_secret_class.is_some() + { "https" } else { "http" diff --git a/tests/templates/kuttl/ldap/30-test-opensearch.yaml b/tests/templates/kuttl/ldap/30-test-opensearch.yaml index ee17e17..c14b926 100644 --- a/tests/templates/kuttl/ldap/30-test-opensearch.yaml +++ b/tests/templates/kuttl/ldap/30-test-opensearch.yaml @@ -79,7 +79,7 @@ data: http_use_tls = os.environ['OPENSEARCH_PROTOCOL'] == 'https' client = OpenSearch( - hosts = [{'host': host, 'port': port}], + hosts=[{'host': host, 'port': port}], http_auth=('integrationtest', 'integrationtest'), http_compress=True, use_ssl=http_use_tls, diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 16766c7..5e5f4dd 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -56,8 +56,6 @@ spec: configMapKeyRef: name: security-config key: roles_mapping.yml - tls: - serverSecretClass: null {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/security-disabled/00-patch-ns.yaml b/tests/templates/kuttl/security-disabled/00-patch-ns.yaml new file mode 100644 index 0000000..d4f91fa --- /dev/null +++ b/tests/templates/kuttl/security-disabled/00-patch-ns.yaml @@ -0,0 +1,15 @@ +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl patch namespace $NAMESPACE --patch=' + { + "metadata": { + "labels": { + "pod-security.kubernetes.io/enforce": "privileged" + } + } + }' + timeout: 120 diff --git a/tests/templates/kuttl/security-disabled/01-rbac.yaml b/tests/templates/kuttl/security-disabled/01-rbac.yaml new file mode 100644 index 0000000..64eced8 --- /dev/null +++ b/tests/templates/kuttl/security-disabled/01-rbac.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: test-service-account +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - privileged + verbs: + - use +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: test-role-binding +subjects: + - kind: ServiceAccount + name: test-service-account +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: test-role diff --git a/tests/templates/kuttl/security-disabled/02-assert.yaml.j2 b/tests/templates/kuttl/security-disabled/02-assert.yaml.j2 new file mode 100644 index 0000000..50b1d4c --- /dev/null +++ b/tests/templates/kuttl/security-disabled/02-assert.yaml.j2 @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +{% endif %} diff --git a/tests/templates/kuttl/security-disabled/02-install-vector-aggregator-discovery-config-map.yaml.j2 b/tests/templates/kuttl/security-disabled/02-install-vector-aggregator-discovery-config-map.yaml.j2 new file mode 100644 index 0000000..2d6a0df --- /dev/null +++ b/tests/templates/kuttl/security-disabled/02-install-vector-aggregator-discovery-config-map.yaml.j2 @@ -0,0 +1,9 @@ +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +data: + ADDRESS: {{ lookup('env', 'VECTOR_AGGREGATOR') }} +{% endif %} diff --git a/tests/templates/kuttl/security-disabled/03-create-truststore.yaml b/tests/templates/kuttl/security-disabled/03-create-truststore.yaml new file mode 100644 index 0000000..2d55c6d --- /dev/null +++ b/tests/templates/kuttl/security-disabled/03-create-truststore.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: secrets.stackable.tech/v1alpha1 +kind: TrustStore +metadata: + name: truststore-pem +spec: + secretClassName: tls + format: tls-pem + targetKind: ConfigMap diff --git a/tests/templates/kuttl/security-disabled/10-assert.yaml b/tests/templates/kuttl/security-disabled/10-assert.yaml new file mode 100644 index 0000000..65425bd --- /dev/null +++ b/tests/templates/kuttl/security-disabled/10-assert.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: opensearch-nodes-default +status: + readyReplicas: 3 + replicas: 3 diff --git a/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 new file mode 100644 index 0000000..23f89aa --- /dev/null +++ b/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 @@ -0,0 +1,48 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% endif %} + productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" + pullPolicy: IfNotPresent + clusterConfig: + securityConfig: + enabled: false +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleConfig: + discoveryServiceListenerClass: external-unstable + roleGroups: + default: + config: + resources: + storage: + data: + capacity: 100Mi + replicas: 3 + envOverrides: + # Only required for the official image + # The official image (built with https://github.com/opensearch-project/opensearch-build) + # installs a demo configuration if not disabled explicitly. + DISABLE_INSTALL_DEMO_CONFIG: "true" + OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} + configOverrides: + opensearch.yml: + # Disable memory mapping in this test; If memory mapping were activated, the kernel setting + # vm.max_map_count would have to be increased to 262144 on the node. + node.store.allow_mmap: "false" + # Disable the disk allocation decider in this test; Otherwise the test depends on the disk + # usage of the node and if the relative watermark set in + # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could + # not be created even if enough disk space would be available. + cluster.routing.allocation.disk.threshold_enabled: "false" diff --git a/tests/templates/kuttl/security-disabled/20-assert.yaml b/tests/templates/kuttl/security-disabled/20-assert.yaml new file mode 100644 index 0000000..4a066bf --- /dev/null +++ b/tests/templates/kuttl/security-disabled/20-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +timeout: 300 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +status: + succeeded: 1 diff --git a/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml b/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml new file mode 100644 index 0000000..00497b3 --- /dev/null +++ b/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test-opensearch +spec: + template: + spec: + containers: + - name: test-opensearch + image: oci.stackable.tech/sdp/testing-tools:0.3.0-stackable0.0.0-dev + command: + - /bin/bash + - -euxo + - pipefail + - -c + args: + - | + pip install opensearch-py==3.1.0 + python scripts/test.py + env: + # required for pip install + - name: HOME + value: /stackable + envFrom: + - configMapRef: + name: opensearch + volumeMounts: + - name: script + mountPath: /stackable/scripts + - name: tls + mountPath: /stackable/tls + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 128Mi + cpu: 400m + volumes: + - name: script + configMap: + name: test-opensearch + - name: tls + configMap: + name: truststore-pem + serviceAccountName: test-service-account + securityContext: + fsGroup: 1000 + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-opensearch +data: + test.py: | + import os + from opensearchpy import OpenSearch + + host = os.environ['OPENSEARCH_HOSTNAME'] + port = os.environ['OPENSEARCH_PORT'] + http_use_tls = os.environ['OPENSEARCH_PROTOCOL'] == 'https' + + client = OpenSearch( + hosts=[{'host': host, 'port': port}], + http_compress=True, + use_ssl=http_use_tls, + verify_certs=True, + ca_certs='/stackable/tls/ca.crt' + ) + + # Create an index + index_name = 'test-index' + + response = client.indices.create(index=index_name) + + print(f'Creating index; {response=}') + + # Add a document to the index + response = client.index( + index = index_name, + body = { + 'name': 'Stackable' + }, + id = 1, + ) + + print(f'Adding document; {response=}') + + # Delete the index. + response = client.indices.delete(index=index_name) + + print(f'Deleting index; {response=}') diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index e5ca230..662da82 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -60,6 +60,11 @@ tests: - opensearch - opensearch_home - release + - name: security-disabled + dimensions: + - opensearch + - opensearch_home + - release suites: - name: nightly patch: @@ -87,6 +92,7 @@ suites: - ldap - opensearch-dashboards - security-config + - security-disabled patch: - dimensions: - name: opensearch From 96745ee187bfb64d3911cd020d604e25f3c627ad Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 13 Feb 2026 15:19:56 +0100 Subject: [PATCH 12/53] Use a structure for the validated security configuration --- .../helm/opensearch-operator/crds/crds.yaml | 1486 +++++++++-------- rust/operator-binary/src/controller.rs | 25 +- rust/operator-binary/src/controller/build.rs | 3 +- .../src/controller/build/node_config.rs | 132 +- .../src/controller/build/role_builder.rs | 20 +- .../controller/build/role_group_builder.rs | 236 +-- .../src/controller/preprocess.rs | 11 +- .../src/controller/validate.rs | 130 +- rust/operator-binary/src/crd/mod.rs | 35 +- .../11-install-opensearch.yaml.j2 | 89 +- .../21-change-security-config.yaml | 15 +- .../kuttl/security-disabled/10-assert.yaml | 7 + .../10-install-opensearch.yaml.j2 | 2 +- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 26 +- .../kuttl/smoke/10-install-opensearch.yaml.j2 | 91 +- 15 files changed, 1251 insertions(+), 1057 deletions(-) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 82db88b..0a8cd55 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -31,122 +31,8 @@ spec: clusterConfig: default: keystore: [] - securityConfig: - actionGroups: - content: - value: - _meta: - config_version: 2 - type: actiongroups - managedBy: API - allowList: - content: - value: - _meta: - config_version: 2 - type: allowlist - config: - enabled: false - managedBy: API - audit: - content: - value: - _meta: - config_version: 2 - type: audit - config: - enabled: false - managedBy: API + security: config: - content: - value: - _meta: - config_version: 2 - type: config - config: - dynamic: - authc: {} - authz: {} - http: {} - managedBy: API - enabled: true - internalUsers: - content: - value: - _meta: - config_version: 2 - type: internalusers - managedBy: API - managingRoleGroup: security-config - nodesDn: - content: - value: - _meta: - config_version: 2 - type: nodesdn - managedBy: API - roles: - content: - value: - _meta: - config_version: 2 - type: roles - managedBy: API - rolesMapping: - content: - value: - _meta: - config_version: 2 - type: rolesmapping - managedBy: API - tenants: - content: - value: - _meta: - config_version: 2 - type: tenants - managedBy: API - tls: - internalSecretClass: tls - serverSecretClass: tls - description: Configuration that applies to all roles and role groups - properties: - keystore: - default: [] - description: Entries to add to the OpenSearch keystore. - items: - properties: - key: - description: Key in the OpenSearch keystore - minLength: 1 - pattern: ^[A-Za-z0-9_\-.]+$ - type: string - secretKeyRef: - description: Reference to the Secret containing the value which will be stored in the OpenSearch keystore - properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - required: - - key - - secretKeyRef - type: object - type: array - securityConfig: - default: actionGroups: content: value: @@ -184,7 +70,6 @@ spec: authz: {} http: {} managedBy: API - enabled: true internalUsers: content: value: @@ -192,7 +77,6 @@ spec: config_version: 2 type: internalusers managedBy: API - managingRoleGroup: security-config nodesDn: content: value: @@ -221,84 +105,58 @@ spec: config_version: 2 type: tenants managedBy: API - description: TODO Add description - properties: - actionGroups: - default: + enabled: true + managingRoleGroup: security-config + tls: + internalSecretClass: tls + serverSecretClass: tls + description: Configuration that applies to all roles and role groups + properties: + keystore: + default: [] + description: Entries to add to the OpenSearch keystore. + items: + properties: + key: + description: Key in the OpenSearch keystore + minLength: 1 + pattern: ^[A-Za-z0-9_\-.]+$ + type: string + secretKeyRef: + description: Reference to the Secret containing the value which will be stored in the OpenSearch keystore + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + required: + - key + - secretKeyRef + type: object + type: array + security: + default: + config: + actionGroups: content: value: _meta: config_version: 2 type: actiongroups managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom - properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: - oneOf: - - required: - - configMapKeyRef - - required: - - secretKeyRef - properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - secretKeyRef: - properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - type: object - type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - allowList: - default: + allowList: content: value: _meta: @@ -307,74 +165,7 @@ spec: config: enabled: false managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom - properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: - oneOf: - - required: - - configMapKeyRef - - required: - - secretKeyRef - properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - secretKeyRef: - properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - type: object - type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - audit: - default: + audit: content: value: _meta: @@ -383,74 +174,7 @@ spec: config: enabled: false managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom - properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: - oneOf: - - required: - - configMapKeyRef - - required: - - secretKeyRef - properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - secretKeyRef: - properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name - type: object - type: object - type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - config: - default: + config: content: value: _meta: @@ -462,450 +186,804 @@ spec: authz: {} http: {} managedBy: API - properties: + internalUsers: content: - oneOf: - - required: - - value - - required: - - valueFrom - properties: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API + nodesDn: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + roles: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API + rolesMapping: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + tenants: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API + enabled: true + managingRoleGroup: security-config + description: TODO Add description + properties: + config: + default: + actionGroups: + content: value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + _meta: + config_version: 2 + type: actiongroups + managedBy: API + allowList: + content: + value: + _meta: + config_version: 2 + type: allowlist + config: + enabled: false + managedBy: API + audit: + content: + value: + _meta: + config_version: 2 + type: audit + config: + enabled: false + managedBy: API + config: + content: + value: + _meta: + config_version: 2 + type: config + config: + dynamic: + authc: {} + authz: {} + http: {} + managedBy: API + internalUsers: + content: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API + nodesDn: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + roles: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API + rolesMapping: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + tenants: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API + properties: + actionGroups: + default: + content: + value: + _meta: + config_version: 2 + type: actiongroups + managedBy: API + properties: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + value: type: object - secretKeyRef: + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - enabled: - default: true - type: boolean - internalUsers: - default: - content: - value: - _meta: - config_version: 2 - type: internalusers - managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom + allowList: + default: + content: + value: + _meta: + config_version: 2 + type: allowlist + config: + enabled: false + managedBy: API properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + value: type: object - secretKeyRef: + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - managingRoleGroup: - default: security-config - maxLength: 16 - minLength: 1 - type: string - nodesDn: - default: - content: - value: - _meta: - config_version: 2 - type: nodesdn - managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom + audit: + default: + content: + value: + _meta: + config_version: 2 + type: audit + config: + enabled: false + managedBy: API properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + value: type: object - secretKeyRef: + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - roles: - default: - content: - value: - _meta: - config_version: 2 - type: roles - managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom + config: + default: + content: + value: + _meta: + config_version: 2 + type: config + config: + dynamic: + authc: {} + authz: {} + http: {} + managedBy: API properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: - properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + value: type: object - secretKeyRef: + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - rolesMapping: - default: - content: - value: - _meta: - config_version: 2 - type: rolesmapping - managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom + internalUsers: + default: + content: + value: + _meta: + config_version: 2 + type: internalusers + managedBy: API properties: - value: - type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + nodesDn: + default: + content: + value: + _meta: + config_version: 2 + type: nodesdn + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: type: object - secretKeyRef: + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy - type: object - tenants: - default: - content: - value: - _meta: - config_version: 2 - type: tenants - managedBy: API - properties: - content: - oneOf: - - required: - - value - - required: - - valueFrom + roles: + default: + content: + value: + _meta: + config_version: 2 + type: roles + managedBy: API properties: - value: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef + properties: + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + type: object type: object - x-kubernetes-preserve-unknown-fields: true - valueFrom: + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + rolesMapping: + default: + content: + value: + _meta: + config_version: 2 + type: rolesmapping + managedBy: API + properties: + content: oneOf: - required: - - configMapKeyRef + - value - required: - - secretKeyRef + - valueFrom properties: - configMapKeyRef: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the ConfigMap that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the ConfigMap - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object - secretKeyRef: + type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy + type: object + tenants: + default: + content: + value: + _meta: + config_version: 2 + type: tenants + managedBy: API + properties: + content: + oneOf: + - required: + - value + - required: + - valueFrom + properties: + value: + type: object + x-kubernetes-preserve-unknown-fields: true + valueFrom: + oneOf: + - required: + - configMapKeyRef + - required: + - secretKeyRef properties: - key: - description: Key in the Secret that contains the value - maxLength: 253 - minLength: 1 - pattern: ^[-._a-zA-Z0-9]+$ - type: string - name: - description: Name of the Secret - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ - type: string - required: - - key - - name + configMapKeyRef: + properties: + key: + description: Key in the ConfigMap that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the ConfigMap + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object + secretKeyRef: + properties: + key: + description: Key in the Secret that contains the value + maxLength: 253 + minLength: 1 + pattern: ^[-._a-zA-Z0-9]+$ + type: string + name: + description: Name of the Secret + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - key + - name + type: object type: object type: object + managedBy: + description: No default, so that the user is aware! + enum: + - API + - operator + type: string + required: + - content + - managedBy type: object - managedBy: - description: No default, so that the user is aware! - enum: - - API - - operator - type: string - required: - - content - - managedBy type: object + enabled: + default: true + type: boolean + managingRoleGroup: + default: security-config + maxLength: 16 + minLength: 1 + type: string type: object tls: default: diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 2a4fb62..972b038 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -180,6 +180,13 @@ impl ValidatedLogging { } } +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedSecurity { + pub managing_role_group: Option, + pub config: v1alpha1::SecurityConfig, + pub tls: v1alpha1::OpenSearchTls, +} + #[derive(Clone, Debug, PartialEq)] pub struct ValidatedDiscoveryEndpoint { pub hostname: Hostname, @@ -204,8 +211,7 @@ pub struct ValidatedCluster { pub uid: Uid, pub role_config: v1alpha1::OpenSearchRoleConfig, pub role_group_configs: BTreeMap, - pub tls_config: v1alpha1::OpenSearchTls, - pub security_config: v1alpha1::SecurityConfig, + pub security: Option, pub keystores: Vec, pub discovery_endpoint: Option, } @@ -220,8 +226,7 @@ impl ValidatedCluster { uid: impl Into, role_config: v1alpha1::OpenSearchRoleConfig, role_group_configs: BTreeMap, - tls_config: v1alpha1::OpenSearchTls, - security_config: v1alpha1::SecurityConfig, + security: Option, keystores: Vec, discovery_endpoint: Option, ) -> Self { @@ -240,8 +245,7 @@ impl ValidatedCluster { uid, role_config, role_group_configs, - tls_config, - security_config, + security, keystores, discovery_endpoint, } @@ -439,7 +443,7 @@ mod tests { use super::{Context, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedLogging}; use crate::{ - controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig}, + controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig, ValidatedSecurity}, crd::{NodeRoles, v1alpha1}, framework::{ builder::pod::container::EnvVarSet, @@ -562,8 +566,11 @@ mod tests { ), ] .into(), - v1alpha1::OpenSearchTls::default(), - v1alpha1::SecurityConfig::default(), + Some(ValidatedSecurity { + managing_role_group: None, + config: v1alpha1::SecurityConfig::default(), + tls: v1alpha1::OpenSearchTls::default(), + }), vec![], None, ) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index bf7d2ed..72826c6 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -209,8 +209,7 @@ mod tests { ), ] .into(), - v1alpha1::OpenSearchTls::default(), - v1alpha1::SecurityConfig::default(), + None, vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 90bcc6b..0222121 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -213,21 +213,9 @@ impl NodeConfig { CONFIG_OPTION_DISCOVERY_TYPE.to_owned(), json!(self.discovery_type()), ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), - json!(self.cluster.security_config.is_only_managed_by_api()), - ); - if self.cluster.security_config.enabled - && !self.cluster.security_config.is_only_managed_by_api() - { - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), - json!(self.admin_dn()), - ); - } config.insert( CONFIG_OPTION_PLUGINS_SECURITY_DISABLED.to_owned(), - json!(!self.cluster.security_config.enabled), + json!(self.cluster.security.is_none()), ); config.insert // Accept certificates generated by the secret-operator @@ -247,75 +235,93 @@ impl NodeConfig { )), ); + if let Some(security) = &self.cluster.security { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), + json!(security.config.is_only_managed_by_api()), + ); + if !security.config.is_only_managed_by_api() { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), + json!(self.admin_dn()), + ); + } + } + config } pub fn admin_dn(&self) -> Option { - if self.cluster.security_config.enabled - && !self.cluster.security_config.is_only_managed_by_api() - { - Some(format!( - "CN={container}.{pod}.{namespace}.{cluster_domain_name}", - container = "update-security-config", - pod = self.cluster.security_config.managing_role_group, - namespace = self.cluster.namespace, - cluster_domain_name = self.cluster_domain_name - )) - } else { - None - } + let security = self.cluster.security.as_ref()?; + + security + .managing_role_group + .as_ref() + .map(|managing_role_group| { + format!( + "CN={container}.{pod}-0.{namespace}.{cluster_domain_name}", + container = "update-security-config", + pod = managing_role_group, + namespace = self.cluster.namespace, + cluster_domain_name = self.cluster_domain_name + ) + }) } 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( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), - json!(true), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/internal/tls.crt")), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/internal/tls.key")), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/internal/ca.crt")), - ); + if let Some(security) = &self.cluster.security { + let opensearch_path_conf = self.opensearch_path_conf(); - // TLS config for HTTP port (REST API) (optional). - if self.cluster.tls_config.server_secret_class.is_some() { + // TLS config for TRANSPORT port which is always enabled. config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), json!(true), ); config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/tls.crt")), + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/internal/tls.crt")), ); config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/tls.key")), + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/internal/tls.key")), ); config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/ca.crt")), - ); - } else { - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), - json!(false), + CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/internal/ca.crt")), ); + + // TLS config for HTTP port (REST API) (optional). + if security.tls.server_secret_class.is_some() { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!(true), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/tls.crt")), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/tls.key")), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/ca.crt")), + ); + } else { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!(false), + ); + } } config } + // TODO Obsolete or should configOverrides be the truth? /// Returns `true` if TLS is enabled on the HTTP port pub fn tls_on_http_port_enabled(&self) -> bool { self.opensearch_config() @@ -551,7 +557,7 @@ mod tests { use super::*; use crate::{ - controller::{ValidatedLogging, ValidatedOpenSearchConfig}, + controller::{ValidatedLogging, ValidatedOpenSearchConfig, ValidatedSecurity}, crd::{NodeRoles, v1alpha1}, framework::{ product_logging::framework::ValidatedContainerLogConfigChoice, @@ -647,8 +653,11 @@ mod tests { role_group_config.clone(), )] .into(), - v1alpha1::OpenSearchTls::default(), - v1alpha1::SecurityConfig::default(), + Some(ValidatedSecurity { + managing_role_group: None, + config: v1alpha1::SecurityConfig::default(), + tls: v1alpha1::OpenSearchTls::default(), + }), vec![], None, ); @@ -678,6 +687,7 @@ mod tests { "node.attr.role-group: \"data\"\n", "path.logs: \"/stackable/log/opensearch\"\n", "plugins.security.allow_default_init_securityindex: true\n", + "plugins.security.disabled: false\n", "plugins.security.nodes_dn: [\"CN=generated certificate for pod\"]\n", "plugins.security.ssl.http.enabled: true\n", "plugins.security.ssl.http.pemcert_filepath: \"/stackable/opensearch/config/tls/server/tls.crt\"\n", diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 87c2773..be5eaa8 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -171,9 +171,14 @@ impl<'a> RoleBuilder<'a> { let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name)); - let protocol = if self.cluster.security_config.enabled - && self.cluster.tls_config.server_secret_class.is_some() - { + let tls_server_secret_class_defined = self + .cluster + .security + .as_ref() + .and_then(|security| security.tls.server_secret_class.as_ref()) + .is_some(); + + let protocol = if tls_server_secret_class_defined { "https" } else { "http" @@ -338,7 +343,7 @@ mod tests { controller::{ ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedDiscoveryEndpoint, ValidatedLogging, - ValidatedOpenSearchConfig, + ValidatedOpenSearchConfig, ValidatedSecurity, build::role_builder::{ discovery_config_map_name, discovery_service_listener_name, seed_nodes_service_name, }, @@ -429,8 +434,11 @@ mod tests { role_group_config.clone(), )] .into(), - v1alpha1::OpenSearchTls::default(), - v1alpha1::SecurityConfig::default(), + Some(ValidatedSecurity { + managing_role_group: None, + config: v1alpha1::SecurityConfig::default(), + tls: v1alpha1::OpenSearchTls::default(), + }), vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), 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 15e09e5..121ba3e 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -48,7 +48,7 @@ use crate::{ constant, controller::{ ContextNames, HTTP_PORT, HTTP_PORT_NAME, OpenSearchRoleGroupConfig, TRANSPORT_PORT, - TRANSPORT_PORT_NAME, ValidatedCluster, + TRANSPORT_PORT_NAME, ValidatedCluster, ValidatedSecurity, build::product_logging::config::{ MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, }, @@ -69,8 +69,8 @@ use crate::{ role_group_utils::ResourceNames, types::{ kubernetes::{ - ContainerName, ListenerName, PersistentVolumeClaimName, SecretClassName, - ServiceAccountName, ServiceName, VolumeName, + ConfigMapName, ContainerName, ListenerName, PersistentVolumeClaimName, + SecretClassName, ServiceAccountName, ServiceName, VolumeName, }, operator::RoleGroupName, }, @@ -152,19 +152,16 @@ impl<'a> RoleGroupBuilder<'a> { } } - fn initializes_security_config(&self) -> bool { - self.cluster.security_config.enabled - && self.cluster.security_config.is_only_managed_by_api() + fn initializes_security_config(security: &ValidatedSecurity) -> bool { + security.managing_role_group.is_none() } - fn manages_security_config(&self) -> bool { - self.cluster.security_config.enabled - && !self.cluster.security_config.is_only_managed_by_api() - && self.cluster.security_config.managing_role_group == self.role_group_name + fn manages_security_config(&self, security: &ValidatedSecurity) -> bool { + security.managing_role_group.as_ref() == Some(&self.role_group_name) } - fn server_ca_volume(&self) -> VolumeName { - if self.manages_security_config() { + fn server_ca_volume(&self, security: &ValidatedSecurity) -> VolumeName { + if self.manages_security_config(security) { TLS_SERVER_CA_VOLUME_NAME.to_owned() } else { TLS_SERVER_VOLUME_NAME.to_owned() @@ -203,10 +200,14 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if self.initializes_security_config() || self.manages_security_config() { - for file_type in SecurityConfigFileType::iter() { - if let Some(value) = self.cluster.security_config.value(file_type) { - data.insert(file_type.filename(), value.to_string()); + if let Some(security) = &self.cluster.security { + if security.managing_role_group.is_none() + || security.managing_role_group.as_ref() == Some(&self.role_group_name) + { + for file_type in SecurityConfigFileType::iter() { + if let Some(value) = security.config.value(file_type) { + data.insert(file_type.filename(), value.to_string()); + } } } } @@ -346,25 +347,6 @@ impl<'a> RoleGroupBuilder<'a> { self.resource_names.role_group_config_map() }; - let mut internal_tls_volume_service_scopes = vec![]; - if self - .role_group_config - .config - .node_roles - .contains(&v1alpha1::NodeRole::ClusterManager) - { - internal_tls_volume_service_scopes - .push(self.node_config.seed_nodes_service_name.clone()); - } - let internal_tls_volume = self.build_tls_volume( - &TLS_INTERNAL_VOLUME_NAME, - &self.cluster.tls_config.internal_secret_class, - internal_tls_volume_service_scopes, - SecretFormat::TlsPem, - &self.role_group_config.config.requested_secret_lifetime, - vec![ROLE_GROUP_LISTENER_VOLUME_NAME.clone()], - ); - let mut volumes = vec![ Volume { name: CONFIG_VOLUME_NAME.to_string(), @@ -394,50 +376,76 @@ impl<'a> RoleGroupBuilder<'a> { }), ..Volume::default() }, - internal_tls_volume, ]; - if let Some(tls_http_secret_class_name) = &self.cluster.tls_config.server_secret_class { - let mut listener_scopes = vec![ROLE_GROUP_LISTENER_VOLUME_NAME.to_owned()]; - if self.role_group_config.config.discovery_service_exposed { - listener_scopes.push(DISCOVERY_SERVICE_LISTENER_VOLUME_NAME.to_owned()); + if let Some(security) = &self.cluster.security { + let mut internal_tls_volume_service_scopes = vec![]; + if self + .role_group_config + .config + .node_roles + .contains(&v1alpha1::NodeRole::ClusterManager) + { + internal_tls_volume_service_scopes + .push(self.node_config.seed_nodes_service_name.clone()); } - - volumes.push(self.build_tls_volume( - &TLS_SERVER_VOLUME_NAME, - tls_http_secret_class_name, - vec![], + let internal_tls_volume = self.build_tls_volume( + &TLS_INTERNAL_VOLUME_NAME, + &security.tls.internal_secret_class, + internal_tls_volume_service_scopes, SecretFormat::TlsPem, &self.role_group_config.config.requested_secret_lifetime, - listener_scopes, - )) - }; + vec![ROLE_GROUP_LISTENER_VOLUME_NAME.clone()], + ); + volumes.push(internal_tls_volume); - if self.server_ca_volume() != TLS_SERVER_VOLUME_NAME.to_owned() { - // An init container is responsible for creating the file ca.crt. - volumes.push(Volume { - name: self.server_ca_volume().to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }); - } + if let Some(tls_http_secret_class_name) = &security.tls.server_secret_class { + let mut listener_scopes = vec![ROLE_GROUP_LISTENER_VOLUME_NAME.to_owned()]; + if self.role_group_config.config.discovery_service_exposed { + listener_scopes.push(DISCOVERY_SERVICE_LISTENER_VOLUME_NAME.to_owned()); + } - if self.initializes_security_config() || self.manages_security_config() { - volumes.extend(self.security_config_volumes()); - } + volumes.push(self.build_tls_volume( + &TLS_SERVER_VOLUME_NAME, + tls_http_secret_class_name, + vec![], + SecretFormat::TlsPem, + &self.role_group_config.config.requested_secret_lifetime, + listener_scopes, + )) + }; - if self.manages_security_config() { - volumes.push(Volume { - name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }); + if self.server_ca_volume(security) != TLS_SERVER_VOLUME_NAME.to_owned() { + // An init container is responsible for creating the file ca.crt. + volumes.push(Volume { + name: self.server_ca_volume(security).to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }); + } + + if security.managing_role_group.is_none() + || security.managing_role_group.as_ref() == Some(&self.role_group_name) + { + volumes.extend(RoleGroupBuilder::security_config_volumes( + &security.config, + self.resource_names.role_group_config_map(), + )); + } + + if security.managing_role_group.as_ref() == Some(&self.role_group_name) { + volumes.push(Volume { + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }); + } } if !self.cluster.keystores.is_empty() { @@ -604,7 +612,9 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO /// Builds the container for the [`PodTemplateSpec`] fn build_maybe_admin_certificate_init_container(&self) -> Option { - if !self.manages_security_config() { + let security = self.cluster.security.as_ref()?; + + if security.managing_role_group.as_ref() != Some(&self.role_group_name) { return None; } @@ -675,7 +685,9 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO /// Builds the container for the [`PodTemplateSpec`] fn build_maybe_security_config_container(&self) -> Option { - if !self.manages_security_config() { + let security = self.cluster.security.as_ref()?; + + if security.managing_role_group.as_ref() != Some(&self.role_group_name) { return None; } @@ -722,11 +734,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ); for file_type in SecurityConfigFileType::iter() { - let managed_by_operator = self - .cluster - .security_config - .security_config(file_type) - .managed_by + let managed_by_operator = security.config.security_config(file_type).managed_by == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; env_vars = env_vars.with_value( @@ -832,11 +840,6 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO name: LOG_VOLUME_NAME.to_string(), ..VolumeMount::default() }, - VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/internal"), - name: TLS_INTERNAL_VOLUME_NAME.to_string(), - ..VolumeMount::default() - }, ]; if self.role_group_config.config.discovery_service_exposed { @@ -847,29 +850,37 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }); } - if self.cluster.tls_config.server_secret_class.is_some() { + if let Some(security) = &self.cluster.security { volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/tls.crt"), - name: TLS_SERVER_VOLUME_NAME.to_string(), - sub_path: Some("tls.crt".to_owned()), - ..VolumeMount::default() - }); - volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/tls.key"), - name: TLS_SERVER_VOLUME_NAME.to_string(), - sub_path: Some("tls.key".to_owned()), - ..VolumeMount::default() - }); - volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), - name: self.server_ca_volume().to_string(), - sub_path: Some("ca.crt".to_owned()), + mount_path: format!("{opensearch_path_conf}/tls/internal"), + name: TLS_INTERNAL_VOLUME_NAME.to_string(), ..VolumeMount::default() }); - } - if self.initializes_security_config() { - volume_mounts.extend(self.security_config_volume_mounts()); + if security.tls.server_secret_class.is_some() { + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/tls.crt"), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.crt".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/tls.key"), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.key".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), + name: self.server_ca_volume(security).to_string(), + sub_path: Some("ca.crt".to_owned()), + ..VolumeMount::default() + }); + } + + if security.managing_role_group.is_none() { + volume_mounts.extend(self.security_config_volume_mounts()); + } } if !self.cluster.keystores.is_empty() { @@ -1101,12 +1112,15 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .build() } - fn security_config_volumes(&self) -> Vec { + fn security_config_volumes( + security_config: &v1alpha1::SecurityConfig, + role_group_config_map_name: ConfigMapName, + ) -> Vec { let mut volumes = vec![]; for file_type in SecurityConfigFileType::iter() { let volume_name = format!("security-config-file-{}", file_type.volume_name()); - if self.cluster.security_config.value(file_type).is_some() { + if security_config.value(file_type).is_some() { let volume = Volume { name: volume_name, config_map: Some(ConfigMapVolumeSource { @@ -1115,14 +1129,14 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO mode: Some(0o660), path: file_type.filename(), }]), - name: self.resource_names.role_group_config_map().to_string(), + name: role_group_config_map_name.to_string(), ..Default::default() }), ..Volume::default() }; volumes.push(volume); } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = - self.cluster.security_config.config_map_key_ref(file_type) + security_config.config_map_key_ref(file_type) { let volume = Volume { name: volume_name, @@ -1139,7 +1153,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }; volumes.push(volume); } else if let Some(v1alpha1::SecretKeyRef { name, key }) = - self.cluster.security_config.secret_key_ref(file_type) + security_config.secret_key_ref(file_type) { let volume = Volume { name: volume_name, @@ -1214,6 +1228,7 @@ mod tests { controller::{ ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, + ValidatedSecurity, build::role_group_builder::{ DISCOVERY_SERVICE_LISTENER_VOLUME_NAME, OPENSEARCH_KEYSTORE_VOLUME_NAME, TLS_INTERNAL_VOLUME_NAME, TLS_SERVER_VOLUME_NAME, @@ -1320,8 +1335,11 @@ mod tests { role_group_config.clone(), )] .into(), - v1alpha1::OpenSearchTls::default(), - v1alpha1::SecurityConfig::default(), + Some(ValidatedSecurity { + managing_role_group: None, + config: v1alpha1::SecurityConfig::default(), + tls: v1alpha1::OpenSearchTls::default(), + }), vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs index 36f9ef6..c603f37 100644 --- a/rust/operator-binary/src/controller/preprocess.rs +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -24,18 +24,19 @@ pub enum Error { type Result = std::result::Result; pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result { - let security_config = &cluster.spec.cluster_config.security_config; - if !security_config.is_only_managed_by_api() + let security = &cluster.spec.cluster_config.security; + if security.enabled + && !security.config.is_only_managed_by_api() && !cluster .spec .nodes .role_groups - .contains_key(&security_config.managing_role_group.to_string()) + .contains_key(&security.managing_role_group.to_string()) { info!( "The security configuration is managed by the role group \"{role_group}\". \ This role group was not specified explicitly and will be created.", - role_group = security_config.managing_role_group + role_group = security.managing_role_group ); let role_group = @@ -64,7 +65,7 @@ pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result Result<()> { - if spec.cluster_config.security_config.enabled { - let security_config_managing_role_group = spec - .cluster_config - .security_config - .managing_role_group - .clone(); - ensure!( - spec.nodes - .role_groups - .contains_key(&security_config_managing_role_group.to_string()), - CheckSecurityConfigManagingRoleGroupSnafu { - security_config_managing_role_group - } - ); +fn validate_security_config( + spec: &v1alpha1::OpenSearchClusterSpec, +) -> Result> { + let security = if spec.cluster_config.security.enabled { + let managing_role_group = if !spec.cluster_config.security.config.is_only_managed_by_api() { + let managing_role_group = spec.cluster_config.security.managing_role_group.clone(); - ensure!( - spec.cluster_config.security_config.is_only_managed_by_api() - || spec.cluster_config.tls.server_secret_class.is_some(), - CheckSecurityConfigTlsSettingsSnafu {} - ); - } - Ok(()) + ensure!( + spec.nodes + .role_groups + .contains_key(&managing_role_group.to_string()), + CheckSecurityConfigManagingRoleGroupSnafu { + security_config_managing_role_group: managing_role_group + } + ); + + // The role group requires server TLS to communicate with the cluster. + ensure!( + spec.cluster_config.tls.server_secret_class.is_some(), + CheckSecurityConfigTlsSettingsSnafu {} + ); + + Some(managing_role_group) + } else { + None + }; + + Some(ValidatedSecurity { + managing_role_group, + config: spec.cluster_config.security.config.clone(), + tls: spec.cluster_config.tls.clone(), + }) + } else { + None + }; + + Ok(security) } /// Return the validated discovery endpoint if a Listener is given with a status containing the @@ -406,7 +421,7 @@ mod tests { built_info, controller::{ ContextNames, DereferencedObjects, ValidatedCluster, ValidatedDiscoveryEndpoint, - ValidatedLogging, ValidatedOpenSearchConfig, + ValidatedLogging, ValidatedOpenSearchConfig, ValidatedSecurity, }, crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, framework::{ @@ -418,8 +433,8 @@ mod tests { types::{ common::Port, kubernetes::{ - ConfigMapName, Hostname, ListenerClassName, NamespaceName, SecretClassName, - SecretKey, SecretName, + ConfigMapKey, ConfigMapName, Hostname, ListenerClassName, NamespaceName, + SecretClassName, SecretKey, SecretName, }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, @@ -637,15 +652,27 @@ mod tests { } )] .into(), - v1alpha1::OpenSearchTls { - server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), - internal_secret_class: SecretClassName::from_str_unsafe("tls") - }, - v1alpha1::SecurityConfig { - enabled: true, - managing_role_group: RoleGroupName::from_str_unsafe("default"), - ..v1alpha1::SecurityConfig::default() - }, + Some(ValidatedSecurity { + managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), + config: v1alpha1::SecurityConfig { + config: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { + name: ConfigMapName::from_str_unsafe("security-config"), + key: ConfigMapKey::from_str_unsafe("config.yml") + } + ) + ) + }, + ..v1alpha1::SecurityConfig::default() + }, + tls: v1alpha1::OpenSearchTls { + server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + internal_secret_class: SecretClassName::from_str_unsafe("tls") + }, + }), vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { @@ -849,11 +876,8 @@ mod tests { fn test_validate_err_check_security_config_managing_role_group() { test_validate_err( |cluster, _| { - cluster - .spec - .cluster_config - .security_config - .managing_role_group = RoleGroupName::from_str_unsafe("non-existent"); + cluster.spec.cluster_config.security.managing_role_group = + RoleGroupName::from_str_unsafe("non-existent"); }, ErrorDiscriminants::CheckSecurityConfigManagingRoleGroup, ); @@ -866,7 +890,8 @@ mod tests { cluster .spec .cluster_config - .security_config + .security + .config .config .managed_by = v1alpha1::SecurityConfigFileTypeManagedBy::Operator; cluster.spec.cluster_config.tls.server_secret_class = None; @@ -918,10 +943,23 @@ mod tests { key: SecretKey::from_str_unsafe("my-keystore-file"), }, }], - security_config: v1alpha1::SecurityConfig { + security: v1alpha1::Security { enabled: true, managing_role_group: RoleGroupName::from_str_unsafe("default"), - ..v1alpha1::SecurityConfig::default() + config: v1alpha1::SecurityConfig { + config: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { + name: ConfigMapName::from_str_unsafe("security-config"), + key: ConfigMapKey::from_str_unsafe("config.yml") + } + ) + ), + }, + ..v1alpha1::SecurityConfig::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 918b653..5d494b2 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -106,7 +106,7 @@ pub mod versioned { /// TODO Add description #[serde(default)] - pub security_config: SecurityConfig, + pub security: Security, /// TLS configuration options for the server (REST API) and internal communication (transport). #[serde(default)] @@ -132,13 +132,20 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub struct SecurityConfig { + pub struct Security { #[serde(default = "security_config_enabled_default")] pub enabled: bool, #[serde(default = "security_config_managing_role_group")] pub managing_role_group: RoleGroupName, + #[serde(default)] + pub config: SecurityConfig, + } + + #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct SecurityConfig { #[serde(default = "security_config_file_type_actiongroups_default")] pub action_groups: SecurityConfigFileType, @@ -493,11 +500,19 @@ impl v1alpha1::OpenSearchConfig { } } -impl Default for v1alpha1::SecurityConfig { +impl Default for v1alpha1::Security { fn default() -> Self { - v1alpha1::SecurityConfig { + Self { enabled: security_config_enabled_default(), managing_role_group: security_config_managing_role_group(), + config: v1alpha1::SecurityConfig::default(), + } + } +} + +impl Default for v1alpha1::SecurityConfig { + fn default() -> Self { + Self { action_groups: security_config_file_type_actiongroups_default(), allow_list: security_config_file_type_allowlist_default(), audit: security_config_file_type_audit_default(), @@ -651,9 +666,7 @@ impl v1alpha1::SecurityConfig { } pub fn value(&self, file_type: SecurityConfigFileType) -> Option { - if !self.enabled { - None - } else if let v1alpha1::SecurityConfigFileType { + if let v1alpha1::SecurityConfigFileType { content: v1alpha1::SecurityConfigFileTypeContent::Value( v1alpha1::SecurityConfigFileTypeContentValue { value }, @@ -671,9 +684,7 @@ impl v1alpha1::SecurityConfig { &self, file_type: SecurityConfigFileType, ) -> Option<&v1alpha1::ConfigMapKeyRef> { - if !self.enabled { - None - } else if let v1alpha1::SecurityConfigFileType { + if let v1alpha1::SecurityConfigFileType { content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( @@ -693,9 +704,7 @@ impl v1alpha1::SecurityConfig { &self, file_type: SecurityConfigFileType, ) -> Option<&v1alpha1::SecretKeyRef> { - if !self.enabled { - None - } else if let v1alpha1::SecurityConfigFileType { + if let v1alpha1::SecurityConfigFileType { content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( v1alpha1::SecurityConfigFileTypeContentValueFrom::SecretKeyRef(secret_key_ref), diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index 5e5f4dd..c6216c0 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -11,51 +11,52 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent clusterConfig: - securityConfig: + security: config: - managedBy: operator - content: - value: - _meta: - type: config - config_version: 2 - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - http: - anonymous_auth_enabled: true - internalUsers: - managedBy: API - content: - valueFrom: - secretKeyRef: - name: security-config-file-internal-users - key: internal_users.yml - roles: - managedBy: API - content: - valueFrom: - configMapKeyRef: - name: security-config - key: roles.yml - rolesMapping: - managedBy: API - content: - valueFrom: - configMapKeyRef: - name: security-config - key: roles_mapping.yml + config: + managedBy: operator + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + http: + anonymous_auth_enabled: true + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: security-config-file-internal-users + key: internal_users.yml + roles: + managedBy: API + content: + valueFrom: + configMapKeyRef: + name: security-config + key: roles.yml + rolesMapping: + managedBy: API + content: + valueFrom: + configMapKeyRef: + name: security-config + key: roles_mapping.yml {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} diff --git a/tests/templates/kuttl/security-config/21-change-security-config.yaml b/tests/templates/kuttl/security-config/21-change-security-config.yaml index 9377214..b5b56f8 100644 --- a/tests/templates/kuttl/security-config/21-change-security-config.yaml +++ b/tests/templates/kuttl/security-config/21-change-security-config.yaml @@ -5,11 +5,12 @@ metadata: name: opensearch spec: clusterConfig: - securityConfig: + security: config: - content: - value: - config: - dynamic: - http: - anonymous_auth_enabled: false + config: + content: + value: + config: + dynamic: + http: + anonymous_auth_enabled: false diff --git a/tests/templates/kuttl/security-disabled/10-assert.yaml b/tests/templates/kuttl/security-disabled/10-assert.yaml index 65425bd..d315253 100644 --- a/tests/templates/kuttl/security-disabled/10-assert.yaml +++ b/tests/templates/kuttl/security-disabled/10-assert.yaml @@ -10,3 +10,10 @@ metadata: status: readyReplicas: 3 replicas: 3 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch +data: + OPENSEARCH_PROTOCOL: http diff --git a/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 index 23f89aa..72655e0 100644 --- a/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-disabled/10-install-opensearch.yaml.j2 @@ -11,7 +11,7 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent clusterConfig: - securityConfig: + security: enabled: false {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index b9174b3..afa0b12 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -182,13 +182,20 @@ spec: name: listener - mountPath: /stackable/log name: log - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/internal - name: tls-internal - mountPath: /stackable/listeners/discovery-service name: discovery-service-listener {% if test_scenario['values']['server-use-tls'] == 'true' %} - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/internal + name: tls-internal + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.crt + name: tls-server + subPath: tls.crt + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.key name: tls-server + subPath: tls.key + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/ca.crt + name: tls-server + subPath: ca.crt {% endif %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml name: security-config-file-actiongroups @@ -650,11 +657,18 @@ spec: name: listener - mountPath: /stackable/log name: log +{% if test_scenario['values']['server-use-tls'] == 'true' %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/internal name: tls-internal -{% if test_scenario['values']['server-use-tls'] == 'true' %} - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.crt + name: tls-server + subPath: tls.crt + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.key + name: tls-server + subPath: tls.key + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/ca.crt name: tls-server + subPath: ca.crt {% endif %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/action_groups.yml name: security-config-file-actiongroups @@ -951,6 +965,7 @@ data: node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" plugins.security.allow_default_init_securityindex: true + plugins.security.disabled: false plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true @@ -1001,6 +1016,7 @@ data: node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" plugins.security.allow_default_init_securityindex: true + plugins.security.disabled: false plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 index f7348ca..b70f971 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 @@ -11,55 +11,56 @@ spec: productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent clusterConfig: - securityConfig: + security: config: - managedBy: API - content: - value: - _meta: - type: config - config_version: 2 + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - internalUsers: - managedBy: API - content: - value: - _meta: - type: internalusers - config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + value: + _meta: + type: internalusers + config_version: 2 - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - rolesMapping: - managedBy: API - content: - value: - _meta: - type: rolesmapping - config_version: 2 + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 - all_access: - reserved: false - backend_roles: - - admin + all_access: + reserved: false + backend_roles: + - admin {% if test_scenario['values']['server-use-tls'] == 'false' %} tls: serverSecretClass: null From b2c611da00e5d444a19961eef662bcbca80949f8 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 13 Feb 2026 17:43:01 +0100 Subject: [PATCH 13/53] Declare security init containers --- .../controller/build/role_group_builder.rs | 126 +++++++++--------- rust/operator-binary/src/crd/mod.rs | 8 ++ 2 files changed, 71 insertions(+), 63 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 121ba3e..75b2ca6 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -152,16 +152,23 @@ impl<'a> RoleGroupBuilder<'a> { } } - fn initializes_security_config(security: &ValidatedSecurity) -> bool { - security.managing_role_group.is_none() - } - - fn manages_security_config(&self, security: &ValidatedSecurity) -> bool { - security.managing_role_group.as_ref() == Some(&self.role_group_name) + /// Returns the container of this role group that manages the security or None if security is + /// not managed by this role group. + fn security_managing_container( + &self, + security: &ValidatedSecurity, + ) -> Option { + match &security.managing_role_group { + None => Some(v1alpha1::Container::OpenSearch), + Some(managing_role_group) if managing_role_group == &self.role_group_name => { + Some(v1alpha1::Container::UpdateSecurityConfig) + } + _ => None, + } } fn server_ca_volume(&self, security: &ValidatedSecurity) -> VolumeName { - if self.manages_security_config(security) { + if security.managing_role_group.as_ref() == Some(&self.role_group_name) { TLS_SERVER_CA_VOLUME_NAME.to_owned() } else { TLS_SERVER_VOLUME_NAME.to_owned() @@ -200,14 +207,12 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if let Some(security) = &self.cluster.security { - if security.managing_role_group.is_none() - || security.managing_role_group.as_ref() == Some(&self.role_group_name) - { - for file_type in SecurityConfigFileType::iter() { - if let Some(value) = security.config.value(file_type) { - data.insert(file_type.filename(), value.to_string()); - } + if let Some(security) = &self.cluster.security + && self.security_managing_container(security).is_some() + { + for file_type in SecurityConfigFileType::iter() { + if let Some(value) = security.config.value(file_type) { + data.insert(file_type.filename(), value.to_string()); } } } @@ -326,7 +331,15 @@ impl<'a> RoleGroupBuilder<'a> { vector_config_file_extra_env_vars(), ) }); - let security_config_container = self.build_maybe_security_config_container(); + + let security_config_container = if let Some(security) = &self.cluster.security + && self.security_managing_container(security) + == Some(v1alpha1::Container::UpdateSecurityConfig) + { + Some(self.build_security_config_container(security)) + } else { + None + }; let mut init_containers = vec![]; if let Some(keystore_init_container) = self.build_maybe_keystore_init_container() { @@ -427,9 +440,7 @@ impl<'a> RoleGroupBuilder<'a> { }); } - if security.managing_role_group.is_none() - || security.managing_role_group.as_ref() == Some(&self.role_group_name) - { + if self.security_managing_container(security).is_some() { volumes.extend(RoleGroupBuilder::security_config_volumes( &security.config, self.resource_names.role_group_config_map(), @@ -684,13 +695,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO } /// Builds the container for the [`PodTemplateSpec`] - fn build_maybe_security_config_container(&self) -> Option { - let security = self.cluster.security.as_ref()?; - - if security.managing_role_group.as_ref() != Some(&self.role_group_name) { - return None; - } - + fn build_security_config_container(&self, security: &ValidatedSecurity) -> Container { let opensearch_path_conf = self.node_config.opensearch_path_conf(); let mut volume_mounts = vec![ @@ -746,38 +751,33 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ); } - let container = new_container_builder( - &ContainerName::from_str("update-security-config") - .expect("should be a valid container name"), - ) - .image_from_product_image(&self.cluster.image) - .command(vec![ - "/bin/bash".to_string(), - "-uo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - ]) - .args(vec![include_str!("update-security-config.sh").to_owned()]) - .add_env_vars(env_vars.into()) - .add_volume_mounts(volume_mounts) - .expect("The mount paths are statically defined and there should be no duplicates.") - .resources( - Resources::<()> { - memory: MemoryLimits { - limit: Some(Quantity("512Mi".to_owned())), - ..MemoryLimits::default() - }, - cpu: CpuLimits { - min: Some(Quantity("100m".to_owned())), - max: Some(Quantity("400m".to_owned())), - }, - ..Resources::default() - } - .into(), - ) - .build(); - - Some(container) + new_container_builder(&v1alpha1::Container::UpdateSecurityConfig.to_container_name()) + .image_from_product_image(&self.cluster.image) + .command(vec![ + "/bin/bash".to_string(), + "-uo".to_string(), + "pipefail".to_string(), + "-c".to_string(), + ]) + .args(vec![include_str!("update-security-config.sh").to_owned()]) + .add_env_vars(env_vars.into()) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources( + Resources::<()> { + memory: MemoryLimits { + limit: Some(Quantity("512Mi".to_owned())), + ..MemoryLimits::default() + }, + cpu: CpuLimits { + min: Some(Quantity("100m".to_owned())), + max: Some(Quantity("400m".to_owned())), + }, + ..Resources::default() + } + .into(), + ) + .build() } /// Builds the container for the [`PodTemplateSpec`] @@ -878,7 +878,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO }); } - if security.managing_role_group.is_none() { + if self.security_managing_container(security) == Some(v1alpha1::Container::OpenSearch) { volume_mounts.extend(self.security_config_volume_mounts()); } } @@ -1650,14 +1650,14 @@ mod tests { "mountPath": "/stackable/log", "name": "log" }, - { - "mountPath": "/stackable/opensearch/config/tls/internal", - "name": "tls-internal" - }, { "mountPath": "/stackable/listeners/discovery-service", "name": "discovery-service-listener" }, + { + "mountPath": "/stackable/opensearch/config/tls/internal", + "name": "tls-internal" + }, { "mountPath": "/stackable/opensearch/config/tls/server", "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 5d494b2..b4f5929 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -375,6 +375,12 @@ pub mod versioned { #[serde(rename = "vector")] Vector, + #[serde(rename = "create-admin-certificate")] + CreateAdminCertificate, + + #[serde(rename = "update-security-config")] + UpdateSecurityConfig, + #[serde(rename = "init-keystore")] InitKeystore, } @@ -828,6 +834,8 @@ impl v1alpha1::Container { ContainerName::from_str(match self { v1alpha1::Container::OpenSearch => "opensearch", v1alpha1::Container::Vector => "vector", + v1alpha1::Container::CreateAdminCertificate => "create-admin-certificate", + v1alpha1::Container::UpdateSecurityConfig => "update-security-config", v1alpha1::Container::InitKeystore => "init-keystore", }) .expect("should be a valid container name") From 88cb2592c48cdd0e405c3e8ce1c0f77d3cf87218 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 18 Feb 2026 17:08:10 +0100 Subject: [PATCH 14/53] test(backup-restore): Use securityConfig --- .../21-install-opensearch-1.yaml.j2 | 143 ++++++------------ .../51-install-opensearch-2.yaml.j2 | 143 ++++++------------ 2 files changed, 98 insertions(+), 188 deletions(-) diff --git a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 index f72b709..f5ce703 100644 --- a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 @@ -12,14 +12,55 @@ spec: pullPolicy: IfNotPresent clusterConfig: keystore: - - key: s3.client.default.access_key - secretKeyRef: - name: s3-credentials - key: ACCESS_KEY - - key: s3.client.default.secret_key - secretKeyRef: - name: s3-credentials - key: SECRET_KEY + - key: s3.client.default.access_key + secretKeyRef: + name: s3-credentials + key: ACCESS_KEY + - key: s3.client.default.secret_key + secretKeyRef: + name: s3-credentials + key: SECRET_KEY + security: + config: + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: opensearch-1-security-config + key: internal_users.yml + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 + all_access: + reserved: false + backend_roles: + - admin {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} @@ -41,7 +82,6 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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.authcz.admin_dn: CN=opensearch-1-admin-certificate plugins.security.restapi.roles_enabled: all_access plugins.security.ssl.http.pemtrustedcas_filepath: /stackable/opensearch/config/tls/concatenated/ca.crt @@ -110,9 +150,6 @@ spec: containers: - name: opensearch volumeMounts: - - name: security-config - mountPath: /stackable/opensearch/config/opensearch-security - readOnly: true {% if test_scenario['values']['s3-use-tls'] == 'true' %} - name: system-trust-store mountPath: /etc/pki/java/cacerts @@ -127,10 +164,6 @@ spec: secret: secretName: opensearch-1-admin-certificate defaultMode: 0o660 - - name: security-config - secret: - secretName: opensearch-1-security-config - defaultMode: 0o660 {% if test_scenario['values']['s3-use-tls'] == 'true' %} - name: s3-ca-crt secret: @@ -149,51 +182,8 @@ kind: Secret metadata: name: opensearch-1-security-config stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} internal_users.yml: | --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - _meta: type: internalusers config_version: 2 @@ -204,38 +194,3 @@ stringData: backend_roles: - admin description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 diff --git a/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 b/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 index c861b32..7cf3e87 100644 --- a/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 @@ -12,14 +12,55 @@ spec: pullPolicy: IfNotPresent clusterConfig: keystore: - - key: s3.client.default.access_key - secretKeyRef: - name: s3-credentials - key: ACCESS_KEY - - key: s3.client.default.secret_key - secretKeyRef: - name: s3-credentials - key: SECRET_KEY + - key: s3.client.default.access_key + secretKeyRef: + name: s3-credentials + key: ACCESS_KEY + - key: s3.client.default.secret_key + secretKeyRef: + name: s3-credentials + key: SECRET_KEY + security: + config: + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: opensearch-2-security-config + key: internal_users.yml + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 + all_access: + reserved: false + backend_roles: + - admin {% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} @@ -41,7 +82,6 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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.authcz.admin_dn: CN=opensearch-2-admin-certificate plugins.security.restapi.roles_enabled: all_access plugins.security.ssl.http.pemtrustedcas_filepath: /stackable/opensearch/config/tls/concatenated/ca.crt @@ -110,9 +150,6 @@ spec: containers: - name: opensearch volumeMounts: - - name: security-config - mountPath: /stackable/opensearch/config/opensearch-security - readOnly: true {% if test_scenario['values']['s3-use-tls'] == 'true' %} - name: system-trust-store mountPath: /etc/pki/java/cacerts @@ -127,10 +164,6 @@ spec: secret: secretName: opensearch-2-admin-certificate defaultMode: 0o660 - - name: security-config - secret: - secretName: opensearch-2-security-config - defaultMode: 0o660 {% if test_scenario['values']['s3-use-tls'] == 'true' %} - name: s3-ca-crt secret: @@ -149,51 +182,8 @@ kind: Secret metadata: name: opensearch-2-security-config stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} internal_users.yml: | --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - _meta: type: internalusers config_version: 2 @@ -204,38 +194,3 @@ stringData: backend_roles: - admin description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 From c3d403e3492775c50a91a36731a4bfe8e0f8791a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 18 Feb 2026 17:21:34 +0100 Subject: [PATCH 15/53] test(external-access): Use securityConfig --- .../external-access/10-listener-classes.yaml | 5 +- ...-classes.yaml => 10_listener-classes.yaml} | 0 .../20-install-opensearch.yaml | 2 +- .../external-access/20_opensearch.yaml.j2 | 57 ++++++ .../kuttl/external-access/opensearch.yaml.j2 | 167 ------------------ 5 files changed, 61 insertions(+), 170 deletions(-) rename tests/templates/kuttl/external-access/{listener-classes.yaml => 10_listener-classes.yaml} (100%) create mode 100644 tests/templates/kuttl/external-access/20_opensearch.yaml.j2 delete mode 100644 tests/templates/kuttl/external-access/opensearch.yaml.j2 diff --git a/tests/templates/kuttl/external-access/10-listener-classes.yaml b/tests/templates/kuttl/external-access/10-listener-classes.yaml index 893032c..b1096dd 100644 --- a/tests/templates/kuttl/external-access/10-listener-classes.yaml +++ b/tests/templates/kuttl/external-access/10-listener-classes.yaml @@ -2,5 +2,6 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: - - script: | - envsubst < listener-classes.yaml | kubectl apply -n $NAMESPACE -f - + - script: > + envsubst '$NAMESPACE' < 10_listener-classes.yaml | + kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/external-access/listener-classes.yaml b/tests/templates/kuttl/external-access/10_listener-classes.yaml similarity index 100% rename from tests/templates/kuttl/external-access/listener-classes.yaml rename to tests/templates/kuttl/external-access/10_listener-classes.yaml diff --git a/tests/templates/kuttl/external-access/20-install-opensearch.yaml b/tests/templates/kuttl/external-access/20-install-opensearch.yaml index 78a7a56..cd9666a 100644 --- a/tests/templates/kuttl/external-access/20-install-opensearch.yaml +++ b/tests/templates/kuttl/external-access/20-install-opensearch.yaml @@ -4,5 +4,5 @@ kind: TestStep timeout: 600 commands: - script: > - envsubst < opensearch.yaml | + envsubst '$NAMESPACE' < 20_opensearch.yaml | kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/external-access/20_opensearch.yaml.j2 b/tests/templates/kuttl/external-access/20_opensearch.yaml.j2 new file mode 100644 index 0000000..f6c35d9 --- /dev/null +++ b/tests/templates/kuttl/external-access/20_opensearch.yaml.j2 @@ -0,0 +1,57 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% endif %} + productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" + pullPolicy: IfNotPresent +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + clusterConfig: + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleConfig: + discoveryServiceListenerClass: test-external-stable-$NAMESPACE + roleGroups: + cluster-manager: + config: + nodeRoles: + - cluster_manager + listenerClass: test-external-stable-$NAMESPACE + replicas: 1 + data1: + config: + nodeRoles: + - data + listenerClass: test-external-unstable-$NAMESPACE + replicas: 1 + data2: + config: + nodeRoles: + - data + listenerClass: test-cluster-internal-$NAMESPACE + replicas: 1 + envOverrides: + # Only required for the official image + # The official image (built with https://github.com/opensearch-project/opensearch-build) + # installs a demo configuration if not disabled explicitly. + DISABLE_INSTALL_DEMO_CONFIG: "true" + OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} + configOverrides: + opensearch.yml: + # Disable memory mapping in this test; If memory mapping were activated, the kernel setting + # vm.max_map_count would have to be increased to 262144 on the node. + node.store.allow_mmap: "false" + # Disable the disk allocation decider in this test; Otherwise the test depends on the disk + # usage of the node and if the relative watermark set in + # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could + # not be created even if enough disk space would be available. + cluster.routing.allocation.disk.threshold_enabled: "false" diff --git a/tests/templates/kuttl/external-access/opensearch.yaml.j2 b/tests/templates/kuttl/external-access/opensearch.yaml.j2 deleted file mode 100644 index 71eca2f..0000000 --- a/tests/templates/kuttl/external-access/opensearch.yaml.j2 +++ /dev/null @@ -1,167 +0,0 @@ ---- -apiVersion: opensearch.stackable.tech/v1alpha1 -kind: OpenSearchCluster -metadata: - name: opensearch -spec: - image: -{% if test_scenario['values']['opensearch'].find(",") > 0 %} - custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" -{% endif %} - productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" - pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - clusterConfig: - vectorAggregatorConfigMapName: vector-aggregator-discovery -{% endif %} - nodes: - config: - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - roleConfig: - discoveryServiceListenerClass: test-external-stable-$NAMESPACE - roleGroups: - cluster-manager: - config: - nodeRoles: - - cluster_manager - listenerClass: test-external-stable-$NAMESPACE - replicas: 1 - data1: - config: - nodeRoles: - - data - listenerClass: test-external-unstable-$NAMESPACE - replicas: 1 - data2: - config: - nodeRoles: - - data - listenerClass: test-cluster-internal-$NAMESPACE - replicas: 1 - envOverrides: - # Only required for the official image - # The official image (built with https://github.com/opensearch-project/opensearch-build) - # installs a demo configuration if not disabled explicitly. - DISABLE_INSTALL_DEMO_CONFIG: "true" - OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} - configOverrides: - opensearch.yml: - # Disable memory mapping in this test; If memory mapping were activated, the kernel setting - # vm.max_map_count would have to be increased to 262144 on the node. - node.store.allow_mmap: "false" - # Disable the disk allocation decider in this test; Otherwise the test depends on the disk - # usage of the node and if the relative watermark set in - # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could - # 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" - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - internal_users.yml: | - --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 From 0d313983770d4a0456d690fb542248bf190372df Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 18 Feb 2026 17:42:59 +0100 Subject: [PATCH 16/53] test(ldap): Use securityConfig --- .../20_opensearch-security-config.yaml.j2 | 33 ------------- .../kuttl/ldap/21-install-opensearch.yaml.j2 | 46 +++++++++++++------ 2 files changed, 31 insertions(+), 48 deletions(-) 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 2ee2485..aa838aa 100644 --- a/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 +++ b/tests/templates/kuttl/ldap/20_opensearch-security-config.yaml.j2 @@ -4,27 +4,6 @@ kind: Secret metadata: name: opensearch-security-config stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false config.yml: | --- _meta: @@ -83,8 +62,6 @@ stringData: rolename: cn internal_users.yml: | --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - _meta: type: internalusers config_version: 2 @@ -100,11 +77,6 @@ stringData: hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS reserved: true description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 roles.yml: | --- _meta: @@ -144,8 +116,3 @@ stringData: reserved: false backend_roles: - testgroup - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 diff --git a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 index 8312285..f77f7cc 100644 --- a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 @@ -10,8 +10,38 @@ spec: {% endif %} productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + security: + config: + config: + managedBy: operator + content: + valueFrom: + secretKeyRef: + name: opensearch-security-config + key: config.yml + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: opensearch-security-config + key: internal_users.yml + roles: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: opensearch-security-config + key: roles.yml + rolesMapping: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: opensearch-security-config + key: roles_mapping.yml +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -47,17 +77,3 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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" - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 From a8c71c2525304c8a072c486193ca4f17ce99e78b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 18 Feb 2026 17:52:55 +0100 Subject: [PATCH 17/53] test(logging): Use securityConfig --- .../logging/20-install-opensearch.yaml.j2 | 105 ------------------ 1 file changed, 105 deletions(-) diff --git a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 index a2ebd69..2d77a76 100644 --- a/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/logging/20-install-opensearch.yaml.j2 @@ -97,15 +97,9 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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" podOverrides: spec: containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: /stackable/opensearch/config/opensearch-security - readOnly: true - name: vector env: - name: VECTOR_CONFIG_YAML @@ -120,105 +114,6 @@ spec: readOnly: true subPath: vector-api-config.yaml volumes: - - name: security-config - secret: - secretName: opensearch-security-config - name: vector-api-config configMap: name: vector-api-config ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - internal_users.yml: | - --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 From dbd5eae874520cb796efdc6ae2dc439abf4dc38a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 18 Feb 2026 18:07:22 +0100 Subject: [PATCH 18/53] test(metrics): Use securityConfig --- .../metrics/20-install-opensearch.yaml.j2 | 179 +++++------------- 1 file changed, 47 insertions(+), 132 deletions(-) diff --git a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 index e5d8b16..0df958e 100644 --- a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 @@ -10,8 +10,54 @@ spec: {% endif %} productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" pullPolicy: IfNotPresent -{% if lookup('env', 'VECTOR_AGGREGATOR') %} clusterConfig: + security: + config: + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: {} + authz: {} + http: + anonymous_auth_enabled: true + roles: + managedBy: API + content: + value: + _meta: + type: roles + config_version: 2 + monitoring: + reserved: true + cluster_permissions: + - cluster:monitor/health + - cluster:monitor/nodes/info + - cluster:monitor/nodes/stats + - cluster:monitor/prometheus/metrics + - cluster:monitor/state + index_permissions: + - index_patterns: + - "*" + allowed_actions: + - indices:monitor/health + - indices:monitor/stats + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 + monitoring: + backend_roles: + - opendistro_security_anonymous_backendrole +{% if lookup('env', 'VECTOR_AGGREGATOR') %} vectorAggregatorConfigMapName: vector-aggregator-discovery {% endif %} nodes: @@ -37,134 +83,3 @@ spec: # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could # 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" - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: /stackable/opensearch/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: false - authentication_backend: - type: intern - authz: {} - http: - anonymous_auth_enabled: true - internal_users.yml: | - --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - - monitoring: - reserved: true - cluster_permissions: - - cluster:monitor/health - - cluster:monitor/nodes/info - - cluster:monitor/nodes/stats - - cluster:monitor/prometheus/metrics - - cluster:monitor/state - index_permissions: - - index_patterns: - - "*" - allowed_actions: - - indices:monitor/health - - indices:monitor/stats - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - - monitoring: - backend_roles: - - opendistro_security_anonymous_backendrole - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 From ad319808332b129c80d87a2a7f500346b84bad06 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 08:51:25 +0100 Subject: [PATCH 19/53] test(opensearch-dashboards): Use securityConfig --- .../10-install-opensearch.yaml.j2 | 163 ------------------ .../10-security-config-internal-users.yaml | 31 ++++ .../{10-assert.yaml.j2 => 11-assert.yaml.j2} | 0 .../11-install-opensearch.yaml.j2 | 90 ++++++++++ 4 files changed, 121 insertions(+), 163 deletions(-) delete mode 100644 tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 create mode 100644 tests/templates/kuttl/opensearch-dashboards/10-security-config-internal-users.yaml rename tests/templates/kuttl/opensearch-dashboards/{10-assert.yaml.j2 => 11-assert.yaml.j2} (100%) create mode 100644 tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 diff --git a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 deleted file mode 100644 index 106afd9..0000000 --- a/tests/templates/kuttl/opensearch-dashboards/10-install-opensearch.yaml.j2 +++ /dev/null @@ -1,163 +0,0 @@ ---- -apiVersion: opensearch.stackable.tech/v1alpha1 -kind: OpenSearchCluster -metadata: - name: opensearch -spec: - image: -{% if test_scenario['values']['opensearch'].find(",") > 0 %} - custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" -{% endif %} - productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" - pullPolicy: IfNotPresent - clusterConfig: -{% if test_scenario['values']['server-use-tls'] == 'false' %} - tls: - serverSecretClass: null -{% endif %} -{% if lookup('env', 'VECTOR_AGGREGATOR') %} - vectorAggregatorConfigMapName: vector-aggregator-discovery -{% endif %} - nodes: - config: - logging: - enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} - roleGroups: - default: - config: - listenerClass: external-unstable - replicas: 1 - envOverrides: - # Only required for the official image - # The official image (built with https://github.com/opensearch-project/opensearch-build) - # installs a demo configuration if not disabled explicitly. - DISABLE_INSTALL_DEMO_CONFIG: "true" - OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} - configOverrides: - opensearch.yml: - # Disable memory mapping in this test; If memory mapping were activated, the kernel setting - # vm.max_map_count would have to be increased to 262144 on the node. - node.store.allow_mmap: "false" - # Disable the disk allocation decider in this test; Otherwise the test depends on the disk - # usage of the node and if the relative watermark set in - # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could - # 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" - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-credentials -data: - admin: QUpWRnNHSkJicFQ2bUNobg== # AJVFsGJBbpT6mChn - kibanaserver: RTRrRU51RW1rcUgzanlIQw== # E4kENuEmkqH3jyHC ---- -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-security-config -stringData: - action_groups.yml: | - --- - _meta: - type: actiongroups - config_version: 2 - allowlist.yml: | - --- - _meta: - type: allowlist - config_version: 2 - - config: - enabled: false - audit.yml: | - --- - _meta: - type: audit - config_version: 2 - - config: - enabled: false - config.yml: | - --- - _meta: - type: config - config_version: 2 - - config: - dynamic: - authc: - basic_internal_auth_domain: - description: Authenticate via HTTP Basic against internal users database - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: basic - challenge: true - authentication_backend: - type: intern - authz: {} - internal_users.yml: | - --- - # The hash value is a bcrypt hash and can be generated with plugin/tools/hash.sh - - _meta: - type: internalusers - config_version: 2 - - admin: - hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e - reserved: true - backend_roles: - - admin - description: OpenSearch admin user - - kibanaserver: - hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS - reserved: true - description: OpenSearch Dashboards user - nodes_dn.yml: | - --- - _meta: - type: nodesdn - config_version: 2 - roles.yml: | - --- - _meta: - type: roles - config_version: 2 - roles_mapping.yml: | - --- - _meta: - type: rolesmapping - config_version: 2 - - all_access: - reserved: false - backend_roles: - - admin - - kibana_server: - reserved: true - users: - - kibanaserver - tenants.yml: | - --- - _meta: - type: tenants - config_version: 2 diff --git a/tests/templates/kuttl/opensearch-dashboards/10-security-config-internal-users.yaml b/tests/templates/kuttl/opensearch-dashboards/10-security-config-internal-users.yaml new file mode 100644 index 0000000..0a1b9dd --- /dev/null +++ b/tests/templates/kuttl/opensearch-dashboards/10-security-config-internal-users.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: opensearch-credentials +data: + admin: QUpWRnNHSkJicFQ2bUNobg== # AJVFsGJBbpT6mChn + kibanaserver: RTRrRU51RW1rcUgzanlIQw== # E4kENuEmkqH3jyHC +--- +apiVersion: v1 +kind: Secret +metadata: + name: security-config-file-internal-users +stringData: + internal_users.yml: | + --- + _meta: + type: internalusers + config_version: 2 + + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user diff --git a/tests/templates/kuttl/opensearch-dashboards/10-assert.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/11-assert.yaml.j2 similarity index 100% rename from tests/templates/kuttl/opensearch-dashboards/10-assert.yaml.j2 rename to tests/templates/kuttl/opensearch-dashboards/11-assert.yaml.j2 diff --git a/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 new file mode 100644 index 0000000..e037732 --- /dev/null +++ b/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 @@ -0,0 +1,90 @@ +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + image: +{% if test_scenario['values']['opensearch'].find(",") > 0 %} + custom: "{{ test_scenario['values']['opensearch'].split(',')[1] }}" +{% endif %} + productVersion: "{{ test_scenario['values']['opensearch'].split(',')[0] }}" + pullPolicy: IfNotPresent + clusterConfig: + security: + config: + config: + managedBy: API + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: security-config-file-internal-users + key: internal_users.yml + rolesMapping: + managedBy: API + content: + value: + _meta: + type: rolesmapping + config_version: 2 + all_access: + reserved: false + backend_roles: + - admin + kibana_server: + reserved: true + users: + - kibanaserver +{% if test_scenario['values']['server-use-tls'] == 'false' %} + tls: + serverSecretClass: null +{% endif %} +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nodes: + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + config: + listenerClass: external-unstable + replicas: 1 + envOverrides: + # Only required for the official image + # The official image (built with https://github.com/opensearch-project/opensearch-build) + # installs a demo configuration if not disabled explicitly. + DISABLE_INSTALL_DEMO_CONFIG: "true" + OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} + configOverrides: + opensearch.yml: + # Disable memory mapping in this test; If memory mapping were activated, the kernel setting + # vm.max_map_count would have to be increased to 262144 on the node. + node.store.allow_mmap: "false" + # Disable the disk allocation decider in this test; Otherwise the test depends on the disk + # usage of the node and if the relative watermark set in + # `cluster.routing.allocation.disk.watermark.high` is reached then the security index could + # not be created even if enough disk space would be available. + cluster.routing.allocation.disk.threshold_enabled: "false" From 1b4e332a52ee36f3893154ba502984dc34ba4639 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 11:35:41 +0100 Subject: [PATCH 20/53] Rename clusterConfig.security.config to clusterConfig.security.settings; Fix admin DN; Fix integration tests --- .../helm/opensearch-operator/crds/crds.yaml | 30 +++++++++---------- .../src/controller/build/node_config.rs | 18 ++--------- .../controller/build/role_group_builder.rs | 7 +++-- .../src/controller/preprocess.rs | 2 +- .../src/controller/validate.rs | 13 +++++--- rust/operator-binary/src/crd/mod.rs | 4 +-- .../21-install-opensearch-1.yaml.j2 | 2 +- .../51-install-opensearch-2.yaml.j2 | 2 +- .../kuttl/ldap/21-install-opensearch.yaml.j2 | 2 +- .../metrics/20-install-opensearch.yaml.j2 | 2 +- .../11-install-opensearch.yaml.j2 | 2 +- .../11-install-opensearch.yaml.j2 | 2 +- .../21-change-security-config.yaml | 2 +- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 4 +-- .../kuttl/smoke/10-install-opensearch.yaml.j2 | 2 +- 15 files changed, 44 insertions(+), 50 deletions(-) diff --git a/deploy/helm/opensearch-operator/crds/crds.yaml b/deploy/helm/opensearch-operator/crds/crds.yaml index 0a8cd55..3650968 100644 --- a/deploy/helm/opensearch-operator/crds/crds.yaml +++ b/deploy/helm/opensearch-operator/crds/crds.yaml @@ -32,7 +32,9 @@ spec: default: keystore: [] security: - config: + enabled: true + managingRoleGroup: security-config + settings: actionGroups: content: value: @@ -105,8 +107,6 @@ spec: config_version: 2 type: tenants managedBy: API - enabled: true - managingRoleGroup: security-config tls: internalSecretClass: tls serverSecretClass: tls @@ -148,7 +148,9 @@ spec: type: array security: default: - config: + enabled: true + managingRoleGroup: security-config + settings: actionGroups: content: value: @@ -221,11 +223,17 @@ spec: config_version: 2 type: tenants managedBy: API - enabled: true - managingRoleGroup: security-config description: TODO Add description properties: - config: + enabled: + default: true + type: boolean + managingRoleGroup: + default: security-config + maxLength: 16 + minLength: 1 + type: string + settings: default: actionGroups: content: @@ -976,14 +984,6 @@ spec: - managedBy type: object type: object - enabled: - default: true - type: boolean - managingRoleGroup: - default: security-config - maxLength: 16 - minLength: 1 - type: string type: object tls: default: diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 0222121..c903c52 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -251,21 +251,9 @@ impl NodeConfig { config } - pub fn admin_dn(&self) -> Option { - let security = self.cluster.security.as_ref()?; - - security - .managing_role_group - .as_ref() - .map(|managing_role_group| { - format!( - "CN={container}.{pod}-0.{namespace}.{cluster_domain_name}", - container = "update-security-config", - pod = managing_role_group, - namespace = self.cluster.namespace, - cluster_domain_name = self.cluster_domain_name - ) - }) + pub fn admin_dn(&self) -> String { + // The common name field is limited to 64 characters, see RFC 5280. + format!("CN=update-security-config.{}", self.cluster.uid) } pub fn tls_config(&self) -> serde_json::Map { 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 75b2ca6..a414cca 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -629,10 +629,11 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO return None; } - let admin_dn = self.node_config.admin_dn().expect(""); - let env_vars = EnvVarSet::new() - .with_value(&EnvVarName::from_str_unsafe("ADMIN_DN"), admin_dn) + .with_value( + &EnvVarName::from_str_unsafe("ADMIN_DN"), + self.node_config.admin_dn(), + ) .with_field_path( &EnvVarName::from_str_unsafe("POD_NAME"), FieldPathEnvVar::Name, diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs index c603f37..95d360f 100644 --- a/rust/operator-binary/src/controller/preprocess.rs +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -26,7 +26,7 @@ type Result = std::result::Result; pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result { let security = &cluster.spec.cluster_config.security; if security.enabled - && !security.config.is_only_managed_by_api() + && !security.settings.is_only_managed_by_api() && !cluster .spec .nodes diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 2bc3b1a..87680e4 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -283,7 +283,12 @@ fn validate_security_config( spec: &v1alpha1::OpenSearchClusterSpec, ) -> Result> { let security = if spec.cluster_config.security.enabled { - let managing_role_group = if !spec.cluster_config.security.config.is_only_managed_by_api() { + let managing_role_group = if !spec + .cluster_config + .security + .settings + .is_only_managed_by_api() + { let managing_role_group = spec.cluster_config.security.managing_role_group.clone(); ensure!( @@ -308,7 +313,7 @@ fn validate_security_config( Some(ValidatedSecurity { managing_role_group, - config: spec.cluster_config.security.config.clone(), + config: spec.cluster_config.security.settings.clone(), tls: spec.cluster_config.tls.clone(), }) } else { @@ -891,7 +896,7 @@ mod tests { .spec .cluster_config .security - .config + .settings .config .managed_by = v1alpha1::SecurityConfigFileTypeManagedBy::Operator; cluster.spec.cluster_config.tls.server_secret_class = None; @@ -946,7 +951,7 @@ mod tests { security: v1alpha1::Security { enabled: true, managing_role_group: RoleGroupName::from_str_unsafe("default"), - config: v1alpha1::SecurityConfig { + settings: v1alpha1::SecurityConfig { config: v1alpha1::SecurityConfigFileType { managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b4f5929..60d8e7a 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -140,7 +140,7 @@ pub mod versioned { pub managing_role_group: RoleGroupName, #[serde(default)] - pub config: SecurityConfig, + pub settings: SecurityConfig, } #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] @@ -511,7 +511,7 @@ impl Default for v1alpha1::Security { Self { enabled: security_config_enabled_default(), managing_role_group: security_config_managing_role_group(), - config: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecurityConfig::default(), } } } diff --git a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 index f5ce703..7864cea 100644 --- a/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/21-install-opensearch-1.yaml.j2 @@ -21,7 +21,7 @@ spec: name: s3-credentials key: SECRET_KEY security: - config: + settings: config: managedBy: API content: diff --git a/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 b/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 index 7cf3e87..e91ad3b 100644 --- a/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/51-install-opensearch-2.yaml.j2 @@ -21,7 +21,7 @@ spec: name: s3-credentials key: SECRET_KEY security: - config: + settings: config: managedBy: API content: diff --git a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 index f77f7cc..944262a 100644 --- a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 @@ -12,7 +12,7 @@ spec: pullPolicy: IfNotPresent clusterConfig: security: - config: + settings: config: managedBy: operator content: diff --git a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 index 0df958e..6915432 100644 --- a/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/metrics/20-install-opensearch.yaml.j2 @@ -12,7 +12,7 @@ spec: pullPolicy: IfNotPresent clusterConfig: security: - config: + settings: config: managedBy: API content: diff --git a/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 index e037732..872495f 100644 --- a/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/opensearch-dashboards/11-install-opensearch.yaml.j2 @@ -12,7 +12,7 @@ spec: pullPolicy: IfNotPresent clusterConfig: security: - config: + settings: config: managedBy: API content: diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index c6216c0..b3f4842 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -12,7 +12,7 @@ spec: pullPolicy: IfNotPresent clusterConfig: security: - config: + settings: config: managedBy: operator content: diff --git a/tests/templates/kuttl/security-config/21-change-security-config.yaml b/tests/templates/kuttl/security-config/21-change-security-config.yaml index b5b56f8..d3d4491 100644 --- a/tests/templates/kuttl/security-config/21-change-security-config.yaml +++ b/tests/templates/kuttl/security-config/21-change-security-config.yaml @@ -6,7 +6,7 @@ metadata: spec: clusterConfig: security: - config: + settings: config: content: value: diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index afa0b12..057818e 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -184,9 +184,9 @@ spec: name: log - mountPath: /stackable/listeners/discovery-service name: discovery-service-listener -{% if test_scenario['values']['server-use-tls'] == 'true' %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/internal name: tls-internal +{% if test_scenario['values']['server-use-tls'] == 'true' %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.crt name: tls-server subPath: tls.crt @@ -657,9 +657,9 @@ spec: name: listener - mountPath: /stackable/log name: log -{% if test_scenario['values']['server-use-tls'] == 'true' %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/internal name: tls-internal +{% if test_scenario['values']['server-use-tls'] == 'true' %} - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/tls/server/tls.crt name: tls-server subPath: tls.crt diff --git a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 index b70f971..5a371b7 100644 --- a/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-install-opensearch.yaml.j2 @@ -12,7 +12,7 @@ spec: pullPolicy: IfNotPresent clusterConfig: security: - config: + settings: config: managedBy: API content: From fd55edad3e1c096172fa4c77668c6c2be95f0c48 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 13:15:11 +0100 Subject: [PATCH 21/53] Update the CRD documentation --- extra/crds.yaml | 158 ++++++++++++++++++++++++++-- rust/operator-binary/src/crd/mod.rs | 56 +++++++++- 2 files changed, 201 insertions(+), 13 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index c7ae9d3..056d58e 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -221,13 +221,19 @@ spec: config_version: 2 type: tenants managedBy: API - description: TODO Add description + description: Configuration of the OpenSearch security plugin properties: enabled: default: true + description: |- + Whether to enable the OpenSearch security plugin + + Disabling the security plugin also disables TLS and exposes the security index if it + exists. type: boolean managingRoleGroup: default: security-config + description: The role group that updates the security index if any setting is managed by the operator. maxLength: 16 minLength: 1 type: string @@ -305,6 +311,7 @@ spec: config_version: 2 type: tenants managedBy: API + description: Settings for the OpenSearch security plugin properties: actionGroups: default: @@ -314,8 +321,13 @@ spec: config_version: 2 type: actiongroups managedBy: API + description: |- + User-defined action groups + + see https://docs.opensearch.org/latest/security/configuration/yaml/#action_groupsyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -323,9 +335,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -333,6 +347,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -351,6 +366,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -371,7 +387,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -390,8 +411,13 @@ spec: config: enabled: false managedBy: API + description: |- + List of allowed HTTP endpoints + + see https://docs.opensearch.org/latest/security/configuration/yaml/#allowlistyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -399,9 +425,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -409,6 +437,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -427,6 +456,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -447,7 +477,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -466,8 +501,13 @@ spec: config: enabled: false managedBy: API + description: |- + Settings for audit logging + + see https://docs.opensearch.org/latest/security/audit-logs/index/#settings-in-audityml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -475,9 +515,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -485,6 +527,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -503,6 +546,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -523,7 +567,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -545,8 +594,13 @@ spec: authz: {} http: {} managedBy: API + description: |- + Configuration of the security backend + + see https://docs.opensearch.org/latest/security/configuration/configuration/ properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -554,9 +608,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -564,6 +620,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -582,6 +639,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -602,7 +660,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -619,8 +682,13 @@ spec: config_version: 2 type: internalusers managedBy: API + description: |- + The internal user database + + see https://docs.opensearch.org/latest/security/configuration/yaml/#internal_usersyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -628,9 +696,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -638,6 +708,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -656,6 +727,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -676,7 +748,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -693,8 +770,13 @@ spec: config_version: 2 type: nodesdn managedBy: API + description: |- + Distinguished names (DNs) of nodes to allow communication between nodes and clusters + + see https://docs.opensearch.org/latest/security/configuration/yaml/#nodes_dnyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -702,9 +784,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -712,6 +796,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -730,6 +815,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -750,7 +836,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -767,8 +858,13 @@ spec: config_version: 2 type: roles managedBy: API + description: |- + Definition of roles in the security plugin + + see https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -776,9 +872,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -786,6 +884,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -804,6 +903,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -824,7 +924,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -841,8 +946,13 @@ spec: config_version: 2 type: rolesmapping managedBy: API + description: |- + Role mappings to users or backend roles + + see https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -850,9 +960,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -860,6 +972,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -878,6 +991,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -898,7 +1012,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -915,8 +1034,13 @@ spec: config_version: 2 type: tenants managedBy: API + description: |- + OpenSearch Dashboards tenants + + see https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml properties: content: + description: The content of the security configuration file oneOf: - required: - value @@ -924,9 +1048,11 @@ spec: - valueFrom properties: value: + description: Security configuration file content defined inline type: object x-kubernetes-preserve-unknown-fields: true valueFrom: + description: Security configuration file content ingested from a ConfigMap or Secret oneOf: - required: - configMapKeyRef @@ -934,6 +1060,7 @@ spec: - secretKeyRef properties: configMapKeyRef: + description: Reference to a key in a ConfigMap properties: key: description: Key in the ConfigMap that contains the value @@ -952,6 +1079,7 @@ spec: - name type: object secretKeyRef: + description: Reference to a key in a Secret properties: key: description: Key in the Secret that contains the value @@ -972,7 +1100,12 @@ spec: type: object type: object managedBy: - description: No default, so that the user is aware! + description: |- + Whether this configuration should only be applied initially and afterwards be managed + via the "API", or managed all the time by the "operator". + + If this configuration is changed later from "API" to "operator", then the changes made + via the API are overridden. enum: - API - operator @@ -987,7 +1120,10 @@ spec: default: internalSecretClass: tls serverSecretClass: tls - description: TLS configuration options for the server (REST API) and internal communication (transport). + description: |- + TLS configuration options for the server (REST API) and internal communication (transport). + + This configuration is only effective if the OpenSearch security plugin is not disabled. properties: internalSecretClass: default: tls diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 60d8e7a..b354ae6 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -104,11 +104,13 @@ pub mod versioned { #[serde(default)] pub keystore: Vec, - /// TODO Add description + /// Configuration of the OpenSearch security plugin #[serde(default)] pub security: Security, /// TLS configuration options for the server (REST API) and internal communication (transport). + /// + /// This configuration is only effective if the OpenSearch security plugin is not disabled. #[serde(default)] pub tls: OpenSearchTls, @@ -133,12 +135,18 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Security { + /// Whether to enable the OpenSearch security plugin + /// + /// Disabling the security plugin also disables TLS and exposes the security index if it + /// exists. #[serde(default = "security_config_enabled_default")] pub enabled: bool, + /// The role group that updates the security index if any setting is managed by the operator. #[serde(default = "security_config_managing_role_group")] pub managing_role_group: RoleGroupName, + /// Settings for the OpenSearch security plugin #[serde(default)] pub settings: SecurityConfig, } @@ -146,30 +154,57 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SecurityConfig { + /// User-defined action groups + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#action_groupsyml #[serde(default = "security_config_file_type_actiongroups_default")] pub action_groups: SecurityConfigFileType, + /// List of allowed HTTP endpoints + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#allowlistyml #[serde(default = "security_config_file_type_allowlist_default")] pub allow_list: SecurityConfigFileType, + /// Settings for audit logging + /// + /// see https://docs.opensearch.org/latest/security/audit-logs/index/#settings-in-audityml #[serde(default = "security_config_file_type_audit_default")] pub audit: SecurityConfigFileType, + /// Configuration of the security backend + /// + /// see https://docs.opensearch.org/latest/security/configuration/configuration/ #[serde(default = "security_config_file_type_config_default")] pub config: SecurityConfigFileType, + /// The internal user database + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#internal_usersyml #[serde(default = "security_config_file_type_internalusers_default")] pub internal_users: SecurityConfigFileType, + /// Distinguished names (DNs) of nodes to allow communication between nodes and clusters + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#nodes_dnyml #[serde(default = "security_config_file_type_nodesdn_default")] pub nodes_dn: SecurityConfigFileType, + /// Definition of roles in the security plugin + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml #[serde(default = "security_config_file_type_roles_default")] pub roles: SecurityConfigFileType, + /// Role mappings to users or backend roles + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml #[serde(default = "security_config_file_type_rolesmapping_default")] pub roles_mapping: SecurityConfigFileType, + /// OpenSearch Dashboards tenants + /// + /// see https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml #[serde(default = "security_config_file_type_tenants_default")] pub tenants: SecurityConfigFileType, } @@ -177,8 +212,15 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SecurityConfigFileType { - /// No default, so that the user is aware! + /// Whether this configuration should only be applied initially and afterwards be managed + /// via the "API", or managed all the time by the "operator". + /// + /// If this configuration is changed later from "API" to "operator", then the changes made + /// via the API are overridden. + // No default, so that the user is aware of! pub managed_by: SecurityConfigFileTypeManagedBy, + + /// The content of the security configuration file pub content: SecurityConfigFileTypeContent, } @@ -196,9 +238,11 @@ pub mod versioned { Serialize, )] pub enum SecurityConfigFileTypeManagedBy { + /// Only initially applied by the operator, but afterwards managed via the API. #[serde(rename = "API")] Api, + /// Managed by the operator; Changes made via the API will be eventually overridden. #[serde(rename = "operator")] Operator, } @@ -206,7 +250,10 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub enum SecurityConfigFileTypeContent { + /// Security configuration file content defined inline Value(SecurityConfigFileTypeContentValue), + + /// Security configuration file content ingested from a ConfigMap or Secret ValueFrom(SecurityConfigFileTypeContentValueFrom), } @@ -220,7 +267,10 @@ pub mod versioned { #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub enum SecurityConfigFileTypeContentValueFrom { + /// Reference to a key in a ConfigMap ConfigMapKeyRef(ConfigMapKeyRef), + + /// Reference to a key in a Secret SecretKeyRef(SecretKeyRef), } @@ -228,6 +278,7 @@ pub mod versioned { pub struct ConfigMapKeyRef { /// Name of the ConfigMap pub name: ConfigMapName, + /// Key in the ConfigMap that contains the value pub key: ConfigMapKey, } @@ -236,6 +287,7 @@ pub mod versioned { pub struct SecretKeyRef { /// Name of the Secret pub name: SecretName, + /// Key in the Secret that contains the value pub key: SecretKey, } From f78b37f7f486cb70ac8647eedef6477682eddfca Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 13:30:46 +0100 Subject: [PATCH 22/53] Rename admin_dn() to super_admin_dn() --- rust/operator-binary/src/controller/build/node_config.rs | 5 +++-- .../src/controller/build/role_group_builder.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index c903c52..14acfb9 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -243,7 +243,7 @@ impl NodeConfig { if !security.config.is_only_managed_by_api() { config.insert( CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), - json!(self.admin_dn()), + json!(self.super_admin_dn()), ); } } @@ -251,7 +251,8 @@ impl NodeConfig { config } - pub fn admin_dn(&self) -> String { + /// Distinguished name (DN) of the super admin certificate + pub fn super_admin_dn(&self) -> String { // The common name field is limited to 64 characters, see RFC 5280. format!("CN=update-security-config.{}", self.cluster.uid) } 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 a414cca..f796f9c 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -632,7 +632,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO let env_vars = EnvVarSet::new() .with_value( &EnvVarName::from_str_unsafe("ADMIN_DN"), - self.node_config.admin_dn(), + self.node_config.super_admin_dn(), ) .with_field_path( &EnvVarName::from_str_unsafe("POD_NAME"), From 732b082fcd5f561859c5209a802f5afc1e4b58e6 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 14:15:21 +0100 Subject: [PATCH 23/53] Do not use overrides to determine if TLS is enabled --- rust/operator-binary/src/controller.rs | 8 +++++ .../src/controller/build/node_config.rs | 30 +++++-------------- .../src/controller/build/role_builder.rs | 9 +----- .../controller/build/role_group_builder.rs | 2 +- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index e0b52ac..075b2a0 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -281,6 +281,14 @@ impl ValidatedCluster { .filter(|c| c.1.config.node_roles.contains(node_role)) .collect() } + + /// Whether security is enabled and a server TLS class is defined or not. + pub fn is_server_tls_enabled(&self) -> bool { + self.security + .as_ref() + .and_then(|security| security.tls.server_secret_class.as_ref()) + .is_some() + } } impl HasName for ValidatedCluster { diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 14acfb9..c4e1292 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -1,11 +1,10 @@ //! Configuration of an OpenSearch node -use std::str::FromStr; - use serde_json::{Value, json}; use stackable_operator::{ builder::pod::container::FieldPathEnvVar, commons::networking::DomainName, }; +use tracing::warn; use super::ValidatedCluster; use crate::{ @@ -184,7 +183,12 @@ impl NodeConfig { .into_iter() .flatten() { - config.insert(setting.to_owned(), json!(value)); + let old_value = config.insert(setting.to_owned(), json!(value)); + if let Some(old_value) = old_value { + warn!( + "configOverrides: Configuration setting {setting:?} changed from {old_value} to {value:?}." + ); + } } // Ensure a deterministic result @@ -310,26 +314,6 @@ impl NodeConfig { config } - // TODO Obsolete or should configOverrides be the truth? - /// Returns `true` if TLS is enabled on the HTTP port - pub fn tls_on_http_port_enabled(&self) -> bool { - self.opensearch_config() - .get(CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED) - .and_then(Self::value_as_bool) - == Some(true) - } - - /// Converts the given JSON value to [`bool`] if possible - pub fn value_as_bool(value: &Value) -> Option { - value.as_bool().or( - // OpenSearch parses the strings "true" and "false" as boolean, see - // https://github.com/opensearch-project/OpenSearch/blob/3.4.0/libs/common/src/main/java/org/opensearch/common/Booleans.java#L45-L84 - value - .as_str() - .and_then(|value| FromStr::from_str(value).ok()), - ) - } - /// Creates environment variables for the OpenSearch configurations /// /// The environment variables should only contain node-specific configuration options. diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index be5eaa8..6a7d35a 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -171,14 +171,7 @@ impl<'a> RoleBuilder<'a> { let metadata = self.common_metadata(discovery_config_map_name(&self.cluster.name)); - let tls_server_secret_class_defined = self - .cluster - .security - .as_ref() - .and_then(|security| security.tls.server_secret_class.as_ref()) - .is_some(); - - let protocol = if tls_server_secret_class_defined { + let protocol = if self.cluster.is_server_tls_enabled() { "https" } else { "http" 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 f796f9c..22ce7dd 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -948,7 +948,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .common_metadata(self.resource_names.headless_service_name()) .with_labels(Self::prometheus_labels()) .with_annotations(Self::prometheus_annotations( - self.node_config.tls_on_http_port_enabled(), + self.cluster.is_server_tls_enabled(), )) .build(); From 7b2bb044f9f2e8683c127277604c24ee22f21f2f Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 14:30:01 +0100 Subject: [PATCH 24/53] Delete unit tests for removed functions --- .../src/controller/build/node_config.rs | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index c4e1292..76d04e3 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -512,7 +512,7 @@ impl NodeConfig { #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::{collections::BTreeMap, str::FromStr}; use pretty_assertions::assert_eq; use stackable_operator::{ @@ -677,62 +677,6 @@ mod tests { ); } - #[test] - pub fn test_tls_on_http_port_enabled() { - let node_config_tls_undefined = node_config(TestConfig::default()); - - let node_config_tls_enabled = node_config(TestConfig { - config_settings: &[("plugins.security.ssl.http.enabled", "true")], - ..TestConfig::default() - }); - - let node_config_tls_disabled = node_config(TestConfig { - config_settings: &[("plugins.security.ssl.http.enabled", "false")], - ..TestConfig::default() - }); - - 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()); - } - - #[test] - pub fn test_value_as_bool() { - // boolean - assert_eq!(Some(true), NodeConfig::value_as_bool(&Value::Bool(true))); - assert_eq!(Some(false), NodeConfig::value_as_bool(&Value::Bool(false))); - - // valid strings - assert_eq!( - Some(true), - NodeConfig::value_as_bool(&Value::String("true".to_owned())) - ); - assert_eq!( - Some(false), - NodeConfig::value_as_bool(&Value::String("false".to_owned())) - ); - - // invalid strings - assert_eq!( - None, - NodeConfig::value_as_bool(&Value::String("True".to_owned())) - ); - - // invalid types - assert_eq!(None, NodeConfig::value_as_bool(&Value::Null)); - assert_eq!( - None, - NodeConfig::value_as_bool(&Value::Number( - serde_json::Number::from_i128(1).expect("should be a valid number") - )) - ); - assert_eq!(None, NodeConfig::value_as_bool(&Value::Array(vec![]))); - assert_eq!( - None, - NodeConfig::value_as_bool(&Value::Object(serde_json::Map::new())) - ); - } - #[test] pub fn test_environment_variables() { let node_config = node_config(TestConfig { From 01c34dc73b6a7c01ce454c91356108a5d71fba17 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 14:36:47 +0100 Subject: [PATCH 25/53] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c547dd..2ec446b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to this project will be documented in this file. - Configuration parameter `spec.nodes.roleGroups..config.discoveryServiceExposed` added to expose a role-group via the discovery service. - Add support for OpenSearch 3.4.0 ([#108]). +- Allow the configuration of the OpenSearch security plugin ([#117]). ### Changed @@ -49,6 +50,7 @@ All notable changes to this project will be documented in this file. [#108]: https://github.com/stackabletech/opensearch-operator/pull/108 [#110]: https://github.com/stackabletech/opensearch-operator/pull/110 [#114]: https://github.com/stackabletech/opensearch-operator/pull/114 +[#117]: https://github.com/stackabletech/opensearch-operator/pull/117 ## [25.11.0] - 2025-11-07 From 1e119a08fb01a71228ebab8d3fa5e48f49a30714 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 15:54:10 +0100 Subject: [PATCH 26/53] Fix shellcheck warnings --- .../src/controller/build/role_group_builder.rs | 18 ++++++------------ .../{ => scripts}/create-admin-certificate.sh | 8 ++++++-- .../{ => scripts}/update-security-config.sh | 8 ++++++-- 3 files changed, 18 insertions(+), 16 deletions(-) rename rust/operator-binary/src/controller/build/{ => scripts}/create-admin-certificate.sh (93%) rename rust/operator-binary/src/controller/build/{ => scripts}/update-security-config.sh (97%) 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 22ce7dd..fd699b7 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -666,13 +666,10 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .expect("should be a valid container name"), ) .image_from_product_image(&self.cluster.image) - .command(vec![ - "/bin/bash".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), + .command(vec!["/bin/bash".to_string(), "-c".to_string()]) + .args(vec![ + include_str!("scripts/create-admin-certificate.sh").to_owned(), ]) - .args(vec![include_str!("create-admin-certificate.sh").to_owned()]) .add_env_vars(env_vars.into()) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") @@ -754,13 +751,10 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO new_container_builder(&v1alpha1::Container::UpdateSecurityConfig.to_container_name()) .image_from_product_image(&self.cluster.image) - .command(vec![ - "/bin/bash".to_string(), - "-uo".to_string(), - "pipefail".to_string(), - "-c".to_string(), + .command(vec!["/bin/bash".to_string(), "-c".to_string()]) + .args(vec![ + include_str!("scripts/update-security-config.sh").to_owned(), ]) - .args(vec![include_str!("update-security-config.sh").to_owned()]) .add_env_vars(env_vars.into()) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") diff --git a/rust/operator-binary/src/controller/build/create-admin-certificate.sh b/rust/operator-binary/src/controller/build/scripts/create-admin-certificate.sh similarity index 93% rename from rust/operator-binary/src/controller/build/create-admin-certificate.sh rename to rust/operator-binary/src/controller/build/scripts/create-admin-certificate.sh index 3bc2ea5..9de5407 100644 --- a/rust/operator-binary/src/controller/build/create-admin-certificate.sh +++ b/rust/operator-binary/src/controller/build/scripts/create-admin-certificate.sh @@ -1,3 +1,7 @@ +#!/usr/bin/env bash + +set -e -u -o pipefail + function log () { level="$1" message="$2" @@ -7,7 +11,7 @@ function log () { } function info () { - message="$@" + message="$*" log INFO "$message" } @@ -39,7 +43,7 @@ function create_admin_certificate () { openssl req \ -x509 \ -nodes \ - -subj=/$ADMIN_DN \ + -subj=/"$ADMIN_DN" \ -out=/stackable/tls-admin-cert/tls.crt \ -keyout=/stackable/tls-admin-cert/tls.key } diff --git a/rust/operator-binary/src/controller/build/update-security-config.sh b/rust/operator-binary/src/controller/build/scripts/update-security-config.sh similarity index 97% rename from rust/operator-binary/src/controller/build/update-security-config.sh rename to rust/operator-binary/src/controller/build/scripts/update-security-config.sh index 4859ab0..d84a550 100644 --- a/rust/operator-binary/src/controller/build/update-security-config.sh +++ b/rust/operator-binary/src/controller/build/scripts/update-security-config.sh @@ -1,3 +1,7 @@ +#!/usr/bin/env bash + +set -u -o pipefail + function log () { level="$1" message="$2" @@ -7,7 +11,7 @@ function log () { } function info () { - message="$@" + message="$*" log INFO "$message" } @@ -33,7 +37,7 @@ function wait_seconds () { mkdir --parents /stackable/log/_vector inotifywait \ --quiet --quiet \ - --timeout $seconds \ + --timeout "$seconds" \ --event create \ /stackable/log/_vector fi From 62548beddbaa905e44e12e29baf618937ff73f71 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 16:39:56 +0100 Subject: [PATCH 27/53] Extend node_config unit test --- .../src/controller/build/node_config.rs | 28 ++++++++++++++----- 1 file changed, 21 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 76d04e3..dc42c36 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -71,8 +71,8 @@ const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; /// Type: string const CONFIG_OPTION_PATH_LOGS: &str = "path.logs"; -/// If this is set to true OpenSearch Security will automatically initialize the configuration index -/// with the files in the config directory if the index does not exist. +/// If this is set to true, the OpenSearch security plugin will automatically initialize the +/// configuration index with the files in the config directory if the index does not exist. /// Type: boolean const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = "plugins.security.allow_default_init_securityindex"; @@ -81,7 +81,7 @@ const CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX: &str = /// Type: (comma-separated) list of strings const CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN: &str = "plugins.security.authcz.admin_dn"; -/// Disables the security plugin +/// Whether to disable the security plugin /// Type: boolean const CONFIG_OPTION_PLUGINS_SECURITY_DISABLED: &str = "plugins.security.disabled"; @@ -536,7 +536,7 @@ mod tests { product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ListenerClassName, NamespaceName}, + kubernetes::{ConfigMapKey, ConfigMapName, ListenerClassName, NamespaceName}, operator::{ClusterName, ProductVersion, RoleGroupName}, }, }, @@ -627,8 +627,21 @@ mod tests { )] .into(), Some(ValidatedSecurity { - managing_role_group: None, - config: v1alpha1::SecurityConfig::default(), + managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), + config: v1alpha1::SecurityConfig { + config: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { + name: ConfigMapName::from_str_unsafe("security-config"), + key: ConfigMapKey::from_str_unsafe("config.yml"), + }, + ), + ), + }, + ..v1alpha1::SecurityConfig::default() + }, tls: v1alpha1::OpenSearchTls::default(), }), vec![], @@ -659,7 +672,8 @@ mod tests { "network.host: \"0.0.0.0\"\n", "node.attr.role-group: \"data\"\n", "path.logs: \"/stackable/log/opensearch\"\n", - "plugins.security.allow_default_init_securityindex: true\n", + "plugins.security.allow_default_init_securityindex: false\n", + "plugins.security.authcz.admin_dn: \"CN=update-security-config.0b1e30e6-326e-4c1a-868d-ad6598b49e8b\"\n", "plugins.security.disabled: false\n", "plugins.security.nodes_dn: [\"CN=generated certificate for pod\"]\n", "plugins.security.ssl.http.enabled: true\n", From 1742f570ab356fb2592ca08840a4afe908c3599b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 19 Feb 2026 16:43:08 +0100 Subject: [PATCH 28/53] Rename ValidatedSecurity::config to settings --- rust/operator-binary/src/controller.rs | 4 ++-- rust/operator-binary/src/controller/build/node_config.rs | 6 +++--- rust/operator-binary/src/controller/build/role_builder.rs | 2 +- .../src/controller/build/role_group_builder.rs | 8 ++++---- rust/operator-binary/src/controller/validate.rs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 075b2a0..5ec66f1 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -184,7 +184,7 @@ impl ValidatedLogging { #[derive(Clone, Debug, PartialEq)] pub struct ValidatedSecurity { pub managing_role_group: Option, - pub config: v1alpha1::SecurityConfig, + pub settings: v1alpha1::SecurityConfig, pub tls: v1alpha1::OpenSearchTls, } @@ -577,7 +577,7 @@ mod tests { .into(), Some(ValidatedSecurity { managing_role_group: None, - config: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecurityConfig::default(), tls: v1alpha1::OpenSearchTls::default(), }), vec![], diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index dc42c36..c2cf6d3 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -242,9 +242,9 @@ impl NodeConfig { if let Some(security) = &self.cluster.security { config.insert( CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), - json!(security.config.is_only_managed_by_api()), + json!(security.settings.is_only_managed_by_api()), ); - if !security.config.is_only_managed_by_api() { + if !security.settings.is_only_managed_by_api() { config.insert( CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), json!(self.super_admin_dn()), @@ -628,7 +628,7 @@ mod tests { .into(), Some(ValidatedSecurity { managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), - config: v1alpha1::SecurityConfig { + settings: v1alpha1::SecurityConfig { config: v1alpha1::SecurityConfigFileType { managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 6a7d35a..6ef3601 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -429,7 +429,7 @@ mod tests { .into(), Some(ValidatedSecurity { managing_role_group: None, - config: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecurityConfig::default(), tls: v1alpha1::OpenSearchTls::default(), }), vec![], 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 fd699b7..3763f57 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -211,7 +211,7 @@ impl<'a> RoleGroupBuilder<'a> { && self.security_managing_container(security).is_some() { for file_type in SecurityConfigFileType::iter() { - if let Some(value) = security.config.value(file_type) { + if let Some(value) = security.settings.value(file_type) { data.insert(file_type.filename(), value.to_string()); } } @@ -442,7 +442,7 @@ impl<'a> RoleGroupBuilder<'a> { if self.security_managing_container(security).is_some() { volumes.extend(RoleGroupBuilder::security_config_volumes( - &security.config, + &security.settings, self.resource_names.role_group_config_map(), )); } @@ -737,7 +737,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ); for file_type in SecurityConfigFileType::iter() { - let managed_by_operator = security.config.security_config(file_type).managed_by + let managed_by_operator = security.settings.security_config(file_type).managed_by == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; env_vars = env_vars.with_value( @@ -1332,7 +1332,7 @@ mod tests { .into(), Some(ValidatedSecurity { managing_role_group: None, - config: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecurityConfig::default(), tls: v1alpha1::OpenSearchTls::default(), }), vec![v1alpha1::OpenSearchKeystore { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 87680e4..06317c9 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -313,7 +313,7 @@ fn validate_security_config( Some(ValidatedSecurity { managing_role_group, - config: spec.cluster_config.security.settings.clone(), + settings: spec.cluster_config.security.settings.clone(), tls: spec.cluster_config.tls.clone(), }) } else { @@ -659,7 +659,7 @@ mod tests { .into(), Some(ValidatedSecurity { managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), - config: v1alpha1::SecurityConfig { + settings: v1alpha1::SecurityConfig { config: v1alpha1::SecurityConfigFileType { managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( From 771bd5b0a02ecbdb9273e67e309665795d5d7ce6 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 20 Feb 2026 14:23:31 +0100 Subject: [PATCH 29/53] Restructure role group builder --- .../controller/build/role_group_builder.rs | 790 +++++++++--------- 1 file changed, 411 insertions(+), 379 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 3763f57..ab16d86 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -33,7 +33,6 @@ 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, }; use strum::IntoEnumIterator; @@ -69,8 +68,8 @@ use crate::{ role_group_utils::ResourceNames, types::{ kubernetes::{ - ConfigMapName, ContainerName, ListenerName, PersistentVolumeClaimName, - SecretClassName, ServiceAccountName, ServiceName, VolumeName, + ContainerName, ListenerName, PersistentVolumeClaimName, ServiceAccountName, + ServiceName, VolumeName, }, operator::RoleGroupName, }, @@ -313,179 +312,56 @@ impl<'a> RoleGroupBuilder<'a> { ) .build(); - let opensearch_container = self.build_opensearch_container(); - let vector_container = self - .role_group_config - .config - .logging - .vector_container - .as_ref() - .map(|vector_container_log_config| { - vector_container( - &v1alpha1::Container::Vector.to_container_name(), - &self.cluster.image, - vector_container_log_config, - &self.resource_names, - &CONFIG_VOLUME_NAME, - &LOG_VOLUME_NAME, - vector_config_file_extra_env_vars(), - ) - }); - - let security_config_container = if let Some(security) = &self.cluster.security - && self.security_managing_container(security) - == Some(v1alpha1::Container::UpdateSecurityConfig) - { - Some(self.build_security_config_container(security)) - } else { - None - }; - - let mut init_containers = vec![]; - if let Some(keystore_init_container) = self.build_maybe_keystore_init_container() { - init_containers.push(keystore_init_container); - } - if let Some(admin_certificate_init_container) = - self.build_maybe_admin_certificate_init_container() - { - init_containers.push(admin_certificate_init_container); - } + let containers = [ + Some(self.build_opensearch_container()), + self.build_maybe_vector_container(), + self.build_maybe_security_config_container(), + ] + .into_iter() + .flatten() + .collect(); - let log_config_volume_config_map = - if let ValidatedContainerLogConfigChoice::Custom(config_map_name) = - &self.role_group_config.config.logging.opensearch_container - { - config_map_name.clone() - } else { - self.resource_names.role_group_config_map() - }; + let init_containers = [ + self.build_maybe_keystore_init_container(), + self.build_maybe_admin_certificate_init_container(), + ] + .into_iter() + .flatten() + .collect(); - let mut volumes = vec![ - Volume { - name: CONFIG_VOLUME_NAME.to_string(), - config_map: Some(ConfigMapVolumeSource { - default_mode: Some(0o660), - name: self.resource_names.role_group_config_map().to_string(), - ..Default::default() - }), - ..Volume::default() - }, - Volume { - name: LOG_CONFIG_VOLUME_NAME.to_string(), - config_map: Some(ConfigMapVolumeSource { - default_mode: Some(0o660), - name: log_config_volume_config_map.to_string(), - ..Default::default() - }), - ..Volume::default() - }, - Volume { - name: LOG_VOLUME_NAME.to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(calculate_log_volume_size_limit(&[ - MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, - ])), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }, - ]; + let volumes = [ + self.build_config_volumes(), + self.build_log_volumes(), + self.build_security_volumes(), + self.build_keystore_volumes(), + ] + .into_iter() + .flatten() + .collect(); - if let Some(security) = &self.cluster.security { - let mut internal_tls_volume_service_scopes = vec![]; - if self + let affinity = Affinity { + node_affinity: self.role_group_config.config.affinity.node_affinity.clone(), + pod_affinity: self.role_group_config.config.affinity.pod_affinity.clone(), + pod_anti_affinity: self .role_group_config .config - .node_roles - .contains(&v1alpha1::NodeRole::ClusterManager) - { - internal_tls_volume_service_scopes - .push(self.node_config.seed_nodes_service_name.clone()); - } - let internal_tls_volume = self.build_tls_volume( - &TLS_INTERNAL_VOLUME_NAME, - &security.tls.internal_secret_class, - internal_tls_volume_service_scopes, - SecretFormat::TlsPem, - &self.role_group_config.config.requested_secret_lifetime, - vec![ROLE_GROUP_LISTENER_VOLUME_NAME.clone()], - ); - volumes.push(internal_tls_volume); - - if let Some(tls_http_secret_class_name) = &security.tls.server_secret_class { - let mut listener_scopes = vec![ROLE_GROUP_LISTENER_VOLUME_NAME.to_owned()]; - if self.role_group_config.config.discovery_service_exposed { - listener_scopes.push(DISCOVERY_SERVICE_LISTENER_VOLUME_NAME.to_owned()); - } - - volumes.push(self.build_tls_volume( - &TLS_SERVER_VOLUME_NAME, - tls_http_secret_class_name, - vec![], - SecretFormat::TlsPem, - &self.role_group_config.config.requested_secret_lifetime, - listener_scopes, - )) - }; - - if self.server_ca_volume(security) != TLS_SERVER_VOLUME_NAME.to_owned() { - // An init container is responsible for creating the file ca.crt. - volumes.push(Volume { - name: self.server_ca_volume(security).to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }); - } - - if self.security_managing_container(security).is_some() { - volumes.extend(RoleGroupBuilder::security_config_volumes( - &security.settings, - self.resource_names.role_group_config_map(), - )); - } - - if security.managing_role_group.as_ref() == Some(&self.role_group_name) { - volumes.push(Volume { - name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }); - } - } - - if !self.cluster.keystores.is_empty() { - volumes.push(Volume { - name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(OPENSEARCH_KEYSTORE_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }) - } + .affinity + .pod_anti_affinity + .clone(), + }; - for (index, keystore) in self.cluster.keystores.iter().enumerate() { - volumes.push(Volume { - name: format!("keystore-{index}"), - secret: Some(SecretVolumeSource { - default_mode: Some(0o660), - secret_name: Some(keystore.secret_key_ref.name.to_string()), - items: Some(vec![KeyToPath { - key: keystore.secret_key_ref.key.to_string(), - path: keystore.secret_key_ref.key.to_string(), - ..KeyToPath::default() - }]), - ..SecretVolumeSource::default() - }), - ..Volume::default() - }); - } + let node_selector = self + .role_group_config + .config + .affinity + .node_selector + .clone() + .map(|wrapped| wrapped.node_selector); + + let security_context = PodSecurityContext { + fs_group: Some(1000), + ..PodSecurityContext::default() + }; // The PodBuilder is not used because it re-validates the values which are already // validated. For instance, it would be necessary to convert the @@ -494,36 +370,11 @@ impl<'a> RoleGroupBuilder<'a> { let mut pod_template = PodTemplateSpec { metadata: Some(metadata), spec: Some(PodSpec { - affinity: Some(Affinity { - node_affinity: self.role_group_config.config.affinity.node_affinity.clone(), - pod_affinity: self.role_group_config.config.affinity.pod_affinity.clone(), - pod_anti_affinity: self - .role_group_config - .config - .affinity - .pod_anti_affinity - .clone(), - }), - containers: [ - Some(opensearch_container), - vector_container, - security_config_container, - ] - .into_iter() - .flatten() - .collect(), + affinity: Some(affinity), + containers, init_containers: Some(init_containers), - node_selector: self - .role_group_config - .config - .affinity - .node_selector - .clone() - .map(|wrapped| wrapped.node_selector), - security_context: Some(PodSecurityContext { - fs_group: Some(1000), - ..PodSecurityContext::default() - }), + node_selector, + security_context: Some(security_context), service_account_name: Some(self.service_account_name.to_string()), termination_grace_period_seconds: Some( self.role_group_config @@ -621,7 +472,7 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ) } - /// Builds the container for the [`PodTemplateSpec`] + /// Builds the create-admin-certificate container for the [`PodTemplateSpec`] fn build_maybe_admin_certificate_init_container(&self) -> Option { let security = self.cluster.security.as_ref()?; @@ -692,89 +543,6 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO Some(container) } - /// Builds the container for the [`PodTemplateSpec`] - fn build_security_config_container(&self, security: &ValidatedSecurity) -> Container { - let opensearch_path_conf = self.node_config.opensearch_path_conf(); - - let mut volume_mounts = vec![ - VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/tls.crt"), - name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), - read_only: Some(true), - sub_path: Some("tls.crt".to_owned()), - ..VolumeMount::default() - }, - VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/tls.key"), - name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), - read_only: Some(true), - sub_path: Some("tls.key".to_owned()), - ..VolumeMount::default() - }, - VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/ca.crt"), - name: TLS_SERVER_CA_VOLUME_NAME.to_string(), - read_only: Some(true), - sub_path: Some("ca.crt".to_owned()), - ..VolumeMount::default() - }, - VolumeMount { - mount_path: LOG_VOLUME_DIR.to_owned(), - name: LOG_VOLUME_NAME.to_string(), - ..VolumeMount::default() - }, - ]; - volume_mounts.extend(self.security_config_volume_mounts()); - - let mut env_vars = EnvVarSet::new() - .with_value( - &EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"), - opensearch_path_conf, - ) - .with_field_path( - &EnvVarName::from_str_unsafe("POD_NAME"), - FieldPathEnvVar::Name, - ); - - for file_type in SecurityConfigFileType::iter() { - let managed_by_operator = security.settings.security_config(file_type).managed_by - == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; - - env_vars = env_vars.with_value( - &EnvVarName::from_str_unsafe(&format!( - "MANAGE_{}", - file_type.volume_name().to_uppercase() - )), - managed_by_operator.to_string(), - ); - } - - new_container_builder(&v1alpha1::Container::UpdateSecurityConfig.to_container_name()) - .image_from_product_image(&self.cluster.image) - .command(vec!["/bin/bash".to_string(), "-c".to_string()]) - .args(vec![ - include_str!("scripts/update-security-config.sh").to_owned(), - ]) - .add_env_vars(env_vars.into()) - .add_volume_mounts(volume_mounts) - .expect("The mount paths are statically defined and there should be no duplicates.") - .resources( - Resources::<()> { - memory: MemoryLimits { - limit: Some(Quantity("512Mi".to_owned())), - ..MemoryLimits::default() - }, - cpu: CpuLimits { - min: Some(Quantity("100m".to_owned())), - max: Some(Quantity("400m".to_owned())), - }, - ..Resources::default() - } - .into(), - ) - .build() - } - /// Builds the container for the [`PodTemplateSpec`] fn build_opensearch_container(&self) -> Container { // Probe values taken from the official Helm chart @@ -936,6 +704,364 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO .build() } + /// Builds the vector container for the [`PodTemplateSpec`] if it is enabled + fn build_maybe_vector_container(&self) -> Option { + let vector_container_log_config = self + .role_group_config + .config + .logging + .vector_container + .as_ref()?; + + Some(vector_container( + &v1alpha1::Container::Vector.to_container_name(), + &self.cluster.image, + vector_container_log_config, + &self.resource_names, + &CONFIG_VOLUME_NAME, + &LOG_VOLUME_NAME, + vector_config_file_extra_env_vars(), + )) + } + + /// Builds the update-security-config container for the [`PodTemplateSpec`] if security is + /// enabled and managed by this role group + fn build_maybe_security_config_container(&self) -> Option { + let security = self.cluster.security.as_ref()?; + + let is_security_config_container_required = self.security_managing_container(security) + == Some(v1alpha1::Container::UpdateSecurityConfig); + is_security_config_container_required.then_some(())?; + + let opensearch_path_conf = self.node_config.opensearch_path_conf(); + + let mut volume_mounts = vec![ + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/tls.crt"), + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("tls.crt".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/tls.key"), + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("tls.key".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/ca.crt"), + name: TLS_SERVER_CA_VOLUME_NAME.to_string(), + read_only: Some(true), + sub_path: Some("ca.crt".to_owned()), + ..VolumeMount::default() + }, + VolumeMount { + mount_path: LOG_VOLUME_DIR.to_owned(), + name: LOG_VOLUME_NAME.to_string(), + ..VolumeMount::default() + }, + ]; + volume_mounts.extend(self.security_config_volume_mounts()); + + let mut env_vars = EnvVarSet::new() + .with_value( + &EnvVarName::from_str_unsafe("OPENSEARCH_PATH_CONF"), + opensearch_path_conf, + ) + .with_field_path( + &EnvVarName::from_str_unsafe("POD_NAME"), + FieldPathEnvVar::Name, + ); + + for file_type in SecurityConfigFileType::iter() { + let managed_by_operator = security.settings.security_config(file_type).managed_by + == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; + + env_vars = env_vars.with_value( + &EnvVarName::from_str_unsafe(&format!( + "MANAGE_{}", + file_type.volume_name().to_uppercase() + )), + managed_by_operator.to_string(), + ); + } + + Some( + new_container_builder(&v1alpha1::Container::UpdateSecurityConfig.to_container_name()) + .image_from_product_image(&self.cluster.image) + .command(vec!["/bin/bash".to_string(), "-c".to_string()]) + .args(vec![ + include_str!("scripts/update-security-config.sh").to_owned(), + ]) + .add_env_vars(env_vars.into()) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources( + Resources::<()> { + memory: MemoryLimits { + limit: Some(Quantity("512Mi".to_owned())), + ..MemoryLimits::default() + }, + cpu: CpuLimits { + min: Some(Quantity("100m".to_owned())), + max: Some(Quantity("400m".to_owned())), + }, + ..Resources::default() + } + .into(), + ) + .build(), + ) + } + + fn build_config_volumes(&self) -> Vec { + vec![Volume { + name: CONFIG_VOLUME_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + default_mode: Some(0o660), + name: self.resource_names.role_group_config_map().to_string(), + ..Default::default() + }), + ..Volume::default() + }] + } + + fn build_log_volumes(&self) -> Vec { + let log_config_volume_config_map = + if let ValidatedContainerLogConfigChoice::Custom(config_map_name) = + &self.role_group_config.config.logging.opensearch_container + { + config_map_name.clone() + } else { + self.resource_names.role_group_config_map() + }; + + vec![ + Volume { + name: LOG_CONFIG_VOLUME_NAME.to_string(), + config_map: Some(ConfigMapVolumeSource { + default_mode: Some(0o660), + name: log_config_volume_config_map.to_string(), + ..Default::default() + }), + ..Volume::default() + }, + Volume { + name: LOG_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(calculate_log_volume_size_limit(&[ + MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, + ])), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }, + ] + } + + fn build_security_volumes(&self) -> Vec { + if let Some(security) = &self.cluster.security { + [ + Self::build_security_internal_tls_volumes, + Self::build_security_server_tls_volumes, + Self::build_security_server_tls_ca_volumes, + Self::build_security_settings_volumes, + Self::build_security_admin_cert_volumes, + ] + .into_iter() + .flat_map(|f| f(self, security)) + .collect() + } else { + vec![] + } + } + + fn build_security_internal_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { + let mut volume_source_builder = + SecretOperatorVolumeSourceBuilder::new(&security.tls.internal_secret_class); + + volume_source_builder + .with_pod_scope() + .with_listener_volume_scope(ROLE_GROUP_LISTENER_VOLUME_NAME.to_string()) + .with_format(SecretFormat::TlsPem) + .with_auto_tls_cert_lifetime(self.role_group_config.config.requested_secret_lifetime); + + if self + .role_group_config + .config + .node_roles + .contains(&v1alpha1::NodeRole::ClusterManager) + { + volume_source_builder.with_service_scope(&self.node_config.seed_nodes_service_name); + } + + let volume_source = volume_source_builder + .build() + .expect("volume should be built without parse errors"); + + vec![ + VolumeBuilder::new(TLS_INTERNAL_VOLUME_NAME.to_string()) + .ephemeral(volume_source) + .build(), + ] + } + + fn build_security_server_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { + let Some(tls_http_secret_class_name) = &security.tls.server_secret_class else { + return vec![]; + }; + + let mut volume_source_builder = + SecretOperatorVolumeSourceBuilder::new(tls_http_secret_class_name); + + volume_source_builder + .with_pod_scope() + .with_listener_volume_scope(ROLE_GROUP_LISTENER_VOLUME_NAME.to_string()) + .with_format(SecretFormat::TlsPem) + .with_auto_tls_cert_lifetime(self.role_group_config.config.requested_secret_lifetime); + + if self.role_group_config.config.discovery_service_exposed { + volume_source_builder + .with_listener_volume_scope(DISCOVERY_SERVICE_LISTENER_VOLUME_NAME.to_string()); + } + + let volume_source = volume_source_builder + .build() + .expect("volume should be built without parse errors"); + + vec![ + VolumeBuilder::new(TLS_SERVER_VOLUME_NAME.to_string()) + .ephemeral(volume_source) + .build(), + ] + } + + fn build_security_server_tls_ca_volumes(&self, security: &ValidatedSecurity) -> Vec { + if self.server_ca_volume(security) != TLS_SERVER_VOLUME_NAME.to_owned() { + vec![Volume { + name: self.server_ca_volume(security).to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }] + } else { + vec![] + } + } + + fn build_security_settings_volumes(&self, security: &ValidatedSecurity) -> Vec { + let mut volumes = vec![]; + + for file_type in SecurityConfigFileType::iter() { + let volume_name = format!("security-config-file-{}", file_type.volume_name()); + if security.settings.value(file_type).is_some() { + let volume = Volume { + name: volume_name, + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: file_type.filename(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: self.resource_names.role_group_config_map().to_string(), + ..Default::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = + security.settings.config_map_key_ref(file_type) + { + let volume = Volume { + name: volume_name, + config_map: Some(ConfigMapVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + name: name.to_string(), + ..ConfigMapVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } else if let Some(v1alpha1::SecretKeyRef { name, key }) = + security.settings.secret_key_ref(file_type) + { + let volume = Volume { + name: volume_name, + secret: Some(SecretVolumeSource { + items: Some(vec![KeyToPath { + key: key.to_string(), + mode: Some(0o660), + path: file_type.filename(), + }]), + secret_name: Some(name.to_string()), + ..SecretVolumeSource::default() + }), + ..Volume::default() + }; + volumes.push(volume); + } + } + + volumes + } + + fn build_security_admin_cert_volumes(&self, security: &ValidatedSecurity) -> Vec { + if security.managing_role_group.as_ref() == Some(&self.role_group_name) { + vec![Volume { + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }] + } else { + vec![] + } + } + + fn build_keystore_volumes(&self) -> Vec { + let mut volumes = vec![]; + + if !self.cluster.keystores.is_empty() { + volumes.push(Volume { + name: OPENSEARCH_KEYSTORE_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(OPENSEARCH_KEYSTORE_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }) + } + + for (index, keystore) in self.cluster.keystores.iter().enumerate() { + volumes.push(Volume { + name: format!("keystore-{index}"), + secret: Some(SecretVolumeSource { + default_mode: Some(0o660), + secret_name: Some(keystore.secret_key_ref.name.to_string()), + items: Some(vec![KeyToPath { + key: keystore.secret_key_ref.key.to_string(), + path: keystore.secret_key_ref.key.to_string(), + ..KeyToPath::default() + }]), + ..SecretVolumeSource::default() + }), + ..Volume::default() + }); + } + + volumes + } + /// Builds the headless [`Service`] for the role-group pub fn build_headless_service(&self) -> Service { let metadata = self @@ -1076,100 +1202,6 @@ cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTO ) } - fn build_tls_volume( - &self, - volume_name: &VolumeName, - tls_secret_class_name: &SecretClassName, - service_scopes: Vec, - secret_format: SecretFormat, - requested_secret_lifetime: &Duration, - listener_volume_scopes: Vec, - ) -> 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); - } - for scope in listener_volume_scopes { - secret_volume_source_builder.with_listener_volume_scope(scope); - } - - VolumeBuilder::new(volume_name.to_string()) - .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() - } - - fn security_config_volumes( - security_config: &v1alpha1::SecurityConfig, - role_group_config_map_name: ConfigMapName, - ) -> Vec { - let mut volumes = vec![]; - - for file_type in SecurityConfigFileType::iter() { - let volume_name = format!("security-config-file-{}", file_type.volume_name()); - if security_config.value(file_type).is_some() { - let volume = Volume { - name: volume_name, - config_map: Some(ConfigMapVolumeSource { - items: Some(vec![KeyToPath { - key: file_type.filename(), - mode: Some(0o660), - path: file_type.filename(), - }]), - name: role_group_config_map_name.to_string(), - ..Default::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = - security_config.config_map_key_ref(file_type) - { - let volume = Volume { - name: volume_name, - config_map: Some(ConfigMapVolumeSource { - items: Some(vec![KeyToPath { - key: key.to_string(), - mode: Some(0o660), - path: file_type.filename(), - }]), - name: name.to_string(), - ..ConfigMapVolumeSource::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::SecretKeyRef { name, key }) = - security_config.secret_key_ref(file_type) - { - let volume = Volume { - name: volume_name, - secret: Some(SecretVolumeSource { - items: Some(vec![KeyToPath { - key: key.to_string(), - mode: Some(0o660), - path: file_type.filename(), - }]), - secret_name: Some(name.to_string()), - ..SecretVolumeSource::default() - }), - ..Volume::default() - }; - volumes.push(volume); - } - } - - volumes - } - fn security_config_volume_mounts(&self) -> Vec { let mut volume_mounts = vec![]; @@ -1900,7 +1932,7 @@ mod tests { "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-seed-nodes,listener-volume=listener,pod" + "secrets.stackable.tech/scope": "pod,listener-volume=listener,service=my-opensearch-cluster-seed-nodes" } }, "spec": { @@ -1926,7 +1958,7 @@ mod tests { "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,listener-volume=discovery-service-listener,pod" + "secrets.stackable.tech/scope": "pod,listener-volume=listener,listener-volume=discovery-service-listener" } }, "spec": { From 63fbded68dd616497cb96f4d61448c9cc8c6a594 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 20 Feb 2026 15:04:29 +0100 Subject: [PATCH 30/53] Move init-keystore script into separate file --- .../controller/build/role_group_builder.rs | 30 +++---------------- .../controller/build/scripts/init-keystore.sh | 13 ++++++++ 2 files changed, 17 insertions(+), 26 deletions(-) create mode 100644 rust/operator-binary/src/controller/build/scripts/init-keystore.sh 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 ab16d86..ef908a1 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -425,6 +425,7 @@ impl<'a> RoleGroupBuilder<'a> { if self.cluster.keystores.is_empty() { return None; } + let opensearch_home = self.node_config.opensearch_home(); let mut volume_mounts = vec![VolumeMount { mount_path: format!( @@ -450,21 +451,8 @@ impl<'a> RoleGroupBuilder<'a> { Some( new_container_builder(&v1alpha1::Container::InitKeystore.to_container_name()) .image_from_product_image(&self.cluster.image) - .command(vec![ - "/bin/bash".to_string(), - "-x".to_string(), - "-euo".to_string(), - "pipefail".to_string(), - "-c".to_string(), - ]) - .args(vec![format!( - "bin/opensearch-keystore create -for i in keystore-secrets/*; do - key=$(basename $i) - bin/opensearch-keystore add-file \"$key\" \"$i\" -done -cp --archive config/opensearch.keystore {OPENSEARCH_INITIALIZED_KEYSTORE_DIRECTORY_NAME}", - )]) + .command(vec!["/bin/bash".to_owned(), "-c".to_owned()]) + .args(vec![include_str!("scripts/init-keystore.sh").to_owned()]) .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") .resources(self.role_group_config.config.resources.clone().into()) @@ -1864,20 +1852,10 @@ mod tests { "initContainers": [ { "args": [ - concat!( - "bin/opensearch-keystore create\n", - "for i in keystore-secrets/*; do\n", - " key=$(basename $i)\n", - " bin/opensearch-keystore add-file \"$key\" \"$i\"\n", - "done\n", - "cp --archive config/opensearch.keystore initialized-keystore" - ), + include_str!("scripts/init-keystore.sh") ], "command": [ "/bin/bash", - "-x", - "-euo", - "pipefail", "-c" ], "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", diff --git a/rust/operator-binary/src/controller/build/scripts/init-keystore.sh b/rust/operator-binary/src/controller/build/scripts/init-keystore.sh new file mode 100644 index 0000000..d4f4ac4 --- /dev/null +++ b/rust/operator-binary/src/controller/build/scripts/init-keystore.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e -u -x -o pipefail + +bin/opensearch-keystore create + +for i in keystore-secrets/* +do + key=$(basename "$i") + bin/opensearch-keystore add-file "$key" "$i" +done + +cp --archive config/opensearch.keystore initialized-keystore From 87585ae9b9bf16fb7fd9524558f3060f7c20d7db Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 20 Feb 2026 17:41:03 +0100 Subject: [PATCH 31/53] Add security modes to the role group builder --- .../src/controller/build/role_builder.rs | 2 +- .../controller/build/role_group_builder.rs | 309 ++++++++++-------- 2 files changed, 176 insertions(+), 135 deletions(-) diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 6ef3601..3759069 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -67,7 +67,7 @@ impl<'a> RoleBuilder<'a> { .map(|(role_group_name, role_group_config)| { RoleGroupBuilder::new( self.resource_names.service_account_name(), - self.cluster.clone(), + &self.cluster, role_group_name.clone(), role_group_config.clone(), self.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 ef908a1..8fdbef4 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -1,4 +1,4 @@ -//! Builder for role-group resources +//! Builder for role group resources use std::{collections::BTreeMap, str::FromStr}; @@ -105,22 +105,43 @@ const OPENSEARCH_KEYSTORE_SECRETS_DIRECTORY: &str = "keystore-secrets"; constant!(OPENSEARCH_KEYSTORE_VOLUME_NAME: VolumeName = "keystore"); const OPENSEARCH_KEYSTORE_VOLUME_SIZE: &str = "1Mi"; +/// Depending on the security settings, the role group builder operates in one of these modes. +enum RoleGroupBuilderSecurityMode<'a> { + /// The security plugin is enabled and all settings are initialized by an arbitrary role group. + /// The security settings are mounted to the main container for all role groups. + Initializing(&'a ValidatedSecurity), + + /// The security plugin is enabled and some or all settings are initialized and updated by this + /// role group. + /// The admin certificate is created in an init container and the security settings are mounted + /// to and updated in a side-car container. + Managing(&'a ValidatedSecurity), + + /// The security plugin is enabled and the settings are managed by another role group. + /// The security settings are not mounted. + Participating(&'a ValidatedSecurity), + + /// The security plugin is disabled. + Disabled, +} + /// Builder for role-group resources pub struct RoleGroupBuilder<'a> { service_account_name: ServiceAccountName, - cluster: ValidatedCluster, + cluster: &'a ValidatedCluster, node_config: NodeConfig, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, context_names: &'a ContextNames, resource_names: ResourceNames, discovery_service_listener_name: ListenerName, + security_mode: RoleGroupBuilderSecurityMode<'a>, } impl<'a> RoleGroupBuilder<'a> { pub fn new( service_account_name: ServiceAccountName, - cluster: ValidatedCluster, + cluster: &'a ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, context_names: &'a ContextNames, @@ -132,9 +153,32 @@ impl<'a> RoleGroupBuilder<'a> { role_name: ValidatedCluster::role_name(), role_group_name: role_group_name.clone(), }; + + let security_mode = match &cluster.security { + Some( + security @ ValidatedSecurity { + managing_role_group: None, + .. + }, + ) => RoleGroupBuilderSecurityMode::Initializing(security), + Some( + security @ ValidatedSecurity { + managing_role_group: Some(role_group), + .. + }, + ) if role_group == &role_group_name => RoleGroupBuilderSecurityMode::Managing(security), + Some( + security @ ValidatedSecurity { + managing_role_group: Some(_), + .. + }, + ) => RoleGroupBuilderSecurityMode::Participating(security), + None => RoleGroupBuilderSecurityMode::Disabled, + }; + RoleGroupBuilder { service_account_name, - cluster: cluster.clone(), + cluster, node_config: NodeConfig::new( cluster.clone(), role_group_name.clone(), @@ -148,34 +192,12 @@ impl<'a> RoleGroupBuilder<'a> { context_names, resource_names, discovery_service_listener_name, + security_mode, } } - /// Returns the container of this role group that manages the security or None if security is - /// not managed by this role group. - fn security_managing_container( - &self, - security: &ValidatedSecurity, - ) -> Option { - match &security.managing_role_group { - None => Some(v1alpha1::Container::OpenSearch), - Some(managing_role_group) if managing_role_group == &self.role_group_name => { - Some(v1alpha1::Container::UpdateSecurityConfig) - } - _ => None, - } - } - - fn server_ca_volume(&self, security: &ValidatedSecurity) -> VolumeName { - if security.managing_role_group.as_ref() == Some(&self.role_group_name) { - TLS_SERVER_CA_VOLUME_NAME.to_owned() - } else { - TLS_SERVER_VOLUME_NAME.to_owned() - } - } - - /// Builds the [`ConfigMap`] containing the configuration files of the role-group - /// [`StatefulSet`] + /// Builds the [`ConfigMap`] containing the configuration files for the [`StatefulSet`] of the + /// role group pub fn build_config_map(&self) -> ConfigMap { let metadata = self .common_metadata(self.resource_names.role_group_config_map()) @@ -206,8 +228,8 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if let Some(security) = &self.cluster.security - && self.security_managing_container(security).is_some() + if let RoleGroupBuilderSecurityMode::Initializing(security) + | RoleGroupBuilderSecurityMode::Managing(security) = self.security_mode { for file_type in SecurityConfigFileType::iter() { if let Some(value) = security.settings.value(file_type) { @@ -223,7 +245,7 @@ impl<'a> RoleGroupBuilder<'a> { } } - /// Builds the role-group [`StatefulSet`] + /// Builds the [`StatefulSet`] of the role group pub fn build_stateful_set(&self) -> StatefulSet { let metadata = self .common_metadata(self.resource_names.stateful_set_name()) @@ -292,7 +314,7 @@ impl<'a> RoleGroupBuilder<'a> { } } - /// Builds the [`PodTemplateSpec`] for the role-group [`StatefulSet`] + /// Builds the [`PodTemplateSpec`] for the [`StatefulSet`] of the role group fn build_pod_template(&self) -> PodTemplateSpec { let mut node_role_labels = Labels::new(); @@ -420,7 +442,8 @@ impl<'a> RoleGroupBuilder<'a> { .expect("should be a valid label") } - /// Builds the container for the [`PodTemplateSpec`] + /// Builds the [`v1alpha1::Container::InitKeystore`] init container for the [`PodTemplateSpec`] + /// if keystores are defined fn build_maybe_keystore_init_container(&self) -> Option { if self.cluster.keystores.is_empty() { return None; @@ -460,13 +483,12 @@ impl<'a> RoleGroupBuilder<'a> { ) } - /// Builds the create-admin-certificate container for the [`PodTemplateSpec`] + /// Builds the [`v1alpha1::Container::CreateAdminCertificate`] init container for the + /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] fn build_maybe_admin_certificate_init_container(&self) -> Option { - let security = self.cluster.security.as_ref()?; - - if security.managing_role_group.as_ref() != Some(&self.role_group_name) { + let RoleGroupBuilderSecurityMode::Managing(_) = self.security_mode else { return None; - } + }; let env_vars = EnvVarSet::new() .with_value( @@ -531,7 +553,7 @@ impl<'a> RoleGroupBuilder<'a> { Some(container) } - /// Builds the container for the [`PodTemplateSpec`] + /// Builds the [`v1alpha1::Container::OpenSearch`] container for the [`PodTemplateSpec`] fn build_opensearch_container(&self) -> Container { // Probe values taken from the official Helm chart let startup_probe = Probe { @@ -623,13 +645,17 @@ impl<'a> RoleGroupBuilder<'a> { }); volume_mounts.push(VolumeMount { mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), - name: self.server_ca_volume(security).to_string(), + name: if let RoleGroupBuilderSecurityMode::Managing(_) = self.security_mode { + TLS_SERVER_CA_VOLUME_NAME.to_string() + } else { + TLS_SERVER_VOLUME_NAME.to_string() + }, sub_path: Some("ca.crt".to_owned()), ..VolumeMount::default() }); } - if self.security_managing_container(security) == Some(v1alpha1::Container::OpenSearch) { + if let RoleGroupBuilderSecurityMode::Initializing(_) = self.security_mode { volume_mounts.extend(self.security_config_volume_mounts()); } } @@ -692,7 +718,32 @@ impl<'a> RoleGroupBuilder<'a> { .build() } - /// Builds the vector container for the [`PodTemplateSpec`] if it is enabled + /// Builds the security settings volume mounts for the [`v1alpha1::Container::OpenSearch`] + /// container or the [`v1alpha1::Container::UpdateSecurityConfig`] container + fn security_config_volume_mounts(&self) -> Vec { + let mut volume_mounts = vec![]; + + let opensearch_path_conf = self.node_config.opensearch_path_conf(); + + for file_type in SecurityConfigFileType::iter() { + let volume_name = format!("security-config-file-{}", file_type.volume_name()); + volume_mounts.push(VolumeMount { + mount_path: format!( + "{opensearch_path_conf}/opensearch-security/{filename}", + filename = file_type.filename() + ), + name: volume_name, + read_only: Some(true), + sub_path: Some(file_type.filename()), + ..VolumeMount::default() + }); + } + + volume_mounts + } + + /// Builds the [`v1alpha1::Container::Vector`] container for the [`PodTemplateSpec`] if it is + /// enabled fn build_maybe_vector_container(&self) -> Option { let vector_container_log_config = self .role_group_config @@ -712,14 +763,12 @@ impl<'a> RoleGroupBuilder<'a> { )) } - /// Builds the update-security-config container for the [`PodTemplateSpec`] if security is - /// enabled and managed by this role group + /// Builds the [`v1alpha1::Container::UpdateSecurityConfig`] container for the + /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] fn build_maybe_security_config_container(&self) -> Option { - let security = self.cluster.security.as_ref()?; - - let is_security_config_container_required = self.security_managing_container(security) - == Some(v1alpha1::Container::UpdateSecurityConfig); - is_security_config_container_required.then_some(())?; + let RoleGroupBuilderSecurityMode::Managing(security) = self.security_mode else { + return None; + }; let opensearch_path_conf = self.node_config.opensearch_path_conf(); @@ -804,6 +853,7 @@ impl<'a> RoleGroupBuilder<'a> { ) } + /// Builds the config volumes for the [`PodTemplateSpec`] fn build_config_volumes(&self) -> Vec { vec![Volume { name: CONFIG_VOLUME_NAME.to_string(), @@ -816,6 +866,7 @@ impl<'a> RoleGroupBuilder<'a> { }] } + /// Builds the log volumes for the [`PodTemplateSpec`] fn build_log_volumes(&self) -> Vec { let log_config_volume_config_map = if let ValidatedContainerLogConfigChoice::Custom(config_map_name) = @@ -849,23 +900,33 @@ impl<'a> RoleGroupBuilder<'a> { ] } + /// Builds the security volumes for the [`PodTemplateSpec`] depending on the + /// [`RoleGroupBuilderSecurityMode`] fn build_security_volumes(&self) -> Vec { - if let Some(security) = &self.cluster.security { - [ - Self::build_security_internal_tls_volumes, - Self::build_security_server_tls_volumes, - Self::build_security_server_tls_ca_volumes, - Self::build_security_settings_volumes, - Self::build_security_admin_cert_volumes, - ] - .into_iter() - .flat_map(|f| f(self, security)) - .collect() - } else { - vec![] - } + let volumes = match self.security_mode { + RoleGroupBuilderSecurityMode::Initializing(security) => vec![ + self.build_security_internal_tls_volumes(security), + self.build_security_server_tls_volumes(security), + self.build_security_settings_volumes(security), + ], + RoleGroupBuilderSecurityMode::Managing(security) => vec![ + self.build_security_internal_tls_volumes(security), + self.build_security_server_tls_volumes(security), + self.build_security_settings_volumes(security), + self.build_security_server_tls_ca_volumes(), + self.build_security_admin_cert_volumes(), + ], + RoleGroupBuilderSecurityMode::Participating(security) => vec![ + self.build_security_internal_tls_volumes(security), + self.build_security_server_tls_volumes(security), + ], + RoleGroupBuilderSecurityMode::Disabled => vec![], + }; + + volumes.into_iter().flatten().collect() } + /// Builds the internal TLS volumes for the [`PodTemplateSpec`] fn build_security_internal_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { let mut volume_source_builder = SecretOperatorVolumeSourceBuilder::new(&security.tls.internal_secret_class); @@ -896,13 +957,15 @@ impl<'a> RoleGroupBuilder<'a> { ] } + /// Builds the server TLS volumes for the [`PodTemplateSpec`] if a TLS server secret class is + /// defined fn build_security_server_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { - let Some(tls_http_secret_class_name) = &security.tls.server_secret_class else { + let Some(tls_server_secret_class) = &security.tls.server_secret_class else { return vec![]; }; let mut volume_source_builder = - SecretOperatorVolumeSourceBuilder::new(tls_http_secret_class_name); + SecretOperatorVolumeSourceBuilder::new(tls_server_secret_class); volume_source_builder .with_pod_scope() @@ -926,21 +989,21 @@ impl<'a> RoleGroupBuilder<'a> { ] } - fn build_security_server_tls_ca_volumes(&self, security: &ValidatedSecurity) -> Vec { - if self.server_ca_volume(security) != TLS_SERVER_VOLUME_NAME.to_owned() { - vec![Volume { - name: self.server_ca_volume(security).to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }] - } else { - vec![] - } + /// Builds the server TLS CA volumes for the [`PodTemplateSpec`] + /// It is not checked if these volumes are required in this role group. + fn build_security_server_tls_ca_volumes(&self) -> Vec { + vec![Volume { + name: TLS_SERVER_CA_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_SERVER_CA_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }] } + /// Builds the security settings volumes for the [`PodTemplateSpec`] + /// It is not checked if these volumes are required in this role group. fn build_security_settings_volumes(&self, security: &ValidatedSecurity) -> Vec { let mut volumes = vec![]; @@ -1001,21 +1064,20 @@ impl<'a> RoleGroupBuilder<'a> { volumes } - fn build_security_admin_cert_volumes(&self, security: &ValidatedSecurity) -> Vec { - if security.managing_role_group.as_ref() == Some(&self.role_group_name) { - vec![Volume { - name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), - empty_dir: Some(EmptyDirVolumeSource { - size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), - ..EmptyDirVolumeSource::default() - }), - ..Volume::default() - }] - } else { - vec![] - } + /// Builds the admin certificate volumes for the [`PodTemplateSpec`] + /// It is not checked if these volumes are required in this role group. + fn build_security_admin_cert_volumes(&self) -> Vec { + vec![Volume { + name: TLS_ADMIN_CERT_VOLUME_NAME.to_string(), + empty_dir: Some(EmptyDirVolumeSource { + size_limit: Some(Quantity(TLS_ADMIN_CERT_VOLUME_SIZE.to_owned())), + ..EmptyDirVolumeSource::default() + }), + ..Volume::default() + }] } + /// Builds the keystore volumes for the [`PodTemplateSpec`] fn build_keystore_volumes(&self) -> Vec { let mut volumes = vec![]; @@ -1050,7 +1112,7 @@ impl<'a> RoleGroupBuilder<'a> { volumes } - /// Builds the headless [`Service`] for the role-group + /// Builds the headless [`Service`] for the role group pub fn build_headless_service(&self) -> Service { let metadata = self .common_metadata(self.resource_names.headless_service_name()) @@ -1120,10 +1182,10 @@ impl<'a> RoleGroupBuilder<'a> { .expect("should be valid annotations") } - /// Builds the [`listener::v1alpha1::Listener`] for the role-group + /// Builds the [`listener::v1alpha1::Listener`] for the role group /// /// The Listener exposes only the HTTP port. - /// The Listener operator will create a Service per role-group. + /// The Listener operator will create a Service per role group. pub fn build_listener(&self) -> listener::v1alpha1::Listener { let metadata = self .common_metadata(self.resource_names.listener_name()) @@ -1148,27 +1210,23 @@ impl<'a> RoleGroupBuilder<'a> { } } - /// Common metadata for role-group resources + /// Common metadata for role group resources fn common_metadata(&self, resource_name: impl Into) -> ObjectMetaBuilder { let mut builder = ObjectMetaBuilder::new(); builder .name(resource_name) .namespace(&self.cluster.namespace) - .ownerreference(ownerreference_from_resource( - &self.cluster, - None, - Some(true), - )) + .ownerreference(ownerreference_from_resource(self.cluster, None, Some(true))) .with_labels(self.recommended_labels()); builder } - /// Recommended labels for role-group resources + /// Recommended labels for role group resources fn recommended_labels(&self) -> Labels { recommended_labels( - &self.cluster, + self.cluster, &self.context_names.product_name, &self.cluster.product_version, &self.context_names.operator_name, @@ -1178,39 +1236,17 @@ impl<'a> RoleGroupBuilder<'a> { ) } - /// Labels to select a [`Pod`] in the role-group + /// Labels to select a [`Pod`] in the role group /// /// [`Pod`]: stackable_operator::k8s_openapi::api::core::v1::Pod fn pod_selector(&self) -> Labels { role_group_selector( - &self.cluster, + self.cluster, &self.context_names.product_name, &ValidatedCluster::role_name(), &self.role_group_name, ) } - - fn security_config_volume_mounts(&self) -> Vec { - let mut volume_mounts = vec![]; - - let opensearch_path_conf = self.node_config.opensearch_path_conf(); - - for file_type in SecurityConfigFileType::iter() { - let volume_name = format!("security-config-file-{}", file_type.volume_name()); - volume_mounts.push(VolumeMount { - mount_path: format!( - "{opensearch_path_conf}/opensearch-security/{filename}", - filename = file_type.filename() - ), - name: volume_name, - read_only: Some(true), - sub_path: Some(file_type.filename()), - ..VolumeMount::default() - }); - } - - volume_mounts - } } #[cfg(test)] @@ -1366,9 +1402,10 @@ mod tests { ) } - fn role_group_builder<'a>(context_names: &'a ContextNames) -> RoleGroupBuilder<'a> { - let cluster = validated_cluster(); - + fn role_group_builder<'a>( + cluster: &'a ValidatedCluster, + context_names: &'a ContextNames, + ) -> RoleGroupBuilder<'a> { let (role_group_name, role_group_config) = cluster .role_group_configs .first_key_value() @@ -1390,8 +1427,9 @@ mod tests { #[test] fn test_build_config_map() { + let cluster = validated_cluster(); let context_names = context_names(); - let role_group_builder = role_group_builder(&context_names); + let role_group_builder = role_group_builder(&cluster, &context_names); let mut config_map = serde_json::to_value(role_group_builder.build_config_map()) .expect("should be serializable"); @@ -1452,8 +1490,9 @@ mod tests { #[test] fn test_build_stateful_set() { + let cluster = validated_cluster(); let context_names = context_names(); - let role_group_builder = role_group_builder(&context_names); + let role_group_builder = role_group_builder(&cluster, &context_names); let stateful_set = serde_json::to_value(role_group_builder.build_stateful_set()) .expect("should be serializable"); @@ -2198,8 +2237,9 @@ mod tests { #[test] fn test_build_headless_service() { + let cluster = validated_cluster(); let context_names = context_names(); - let role_group_builder = role_group_builder(&context_names); + let role_group_builder = role_group_builder(&cluster, &context_names); let headless_service = serde_json::to_value(role_group_builder.build_headless_service()) .expect("should be serializable"); @@ -2265,8 +2305,9 @@ mod tests { #[test] fn test_build_listener() { + let cluster = validated_cluster(); let context_names = context_names(); - let role_group_builder = role_group_builder(&context_names); + let role_group_builder = role_group_builder(&cluster, &context_names); let listener = serde_json::to_value(role_group_builder.build_listener()) .expect("should be serializable"); From f5de51d2f0d062bfe87b2ad4ef5b7e1fdb2a0ca0 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 23 Feb 2026 11:53:05 +0100 Subject: [PATCH 32/53] test(smoke): Fix assertion --- .../src/controller/build/role_group_builder.rs | 2 +- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 8 ++++---- 2 files changed, 5 insertions(+), 5 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 8fdbef4..c066184 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -125,7 +125,7 @@ enum RoleGroupBuilderSecurityMode<'a> { Disabled, } -/// Builder for role-group resources +/// Builder for role group resources pub struct RoleGroupBuilder<'a> { service_account_name: ServiceAccountName, cluster: &'a ValidatedCluster, diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 057818e..716feb3 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -320,7 +320,7 @@ spec: 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-seed-nodes,listener-volume=listener,pod + secrets.stackable.tech/scope: pod,listener-volume=listener,service=opensearch-seed-nodes spec: accessModes: - ReadWriteOnce @@ -338,7 +338,7 @@ spec: 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,listener-volume=discovery-service-listener,pod + secrets.stackable.tech/scope: pod,listener-volume=listener,listener-volume=discovery-service-listener spec: accessModes: - ReadWriteOnce @@ -793,7 +793,7 @@ spec: 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 + secrets.stackable.tech/scope: pod,listener-volume=listener spec: accessModes: - ReadWriteOnce @@ -811,7 +811,7 @@ spec: 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 + secrets.stackable.tech/scope: pod,listener-volume=listener spec: accessModes: - ReadWriteOnce From f8dcd3336fca8cb036505ca436e93fefbadfe9c6 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 23 Feb 2026 16:48:41 +0100 Subject: [PATCH 33/53] test: Test role group security modes --- .../src/controller/build/node_config.rs | 2 +- .../controller/build/role_group_builder.rs | 2148 ++++++++++++----- rust/operator-binary/src/crd/mod.rs | 19 +- 3 files changed, 1512 insertions(+), 657 deletions(-) diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index c2cf6d3..26aa19b 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -66,7 +66,7 @@ const CONFIG_OPTION_NODE_ROLES: &str = "node.roles"; /// Defines the path for the logs /// OpenSearch grants the required access rights, see -/// https://github.com/opensearch-project/OpenSearch/blob/3.4.0/server/src/main/java/org/opensearch/bootstrap/Security.java#L369 +/// /// The permissions "write" and "delete" are required for the log file rollover. /// Type: string const CONFIG_OPTION_PATH_LOGS: &str = "path.logs"; 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 c066184..128a110 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -68,8 +68,8 @@ use crate::{ role_group_utils::ResourceNames, types::{ kubernetes::{ - ContainerName, ListenerName, PersistentVolumeClaimName, ServiceAccountName, - ServiceName, VolumeName, + ListenerName, PersistentVolumeClaimName, ServiceAccountName, ServiceName, + VolumeName, }, operator::RoleGroupName, }, @@ -91,8 +91,8 @@ const DISCOVERY_SERVICE_LISTENER_VOLUME_DIR: &str = "/stackable/listeners/discov constant!(TLS_SERVER_VOLUME_NAME: VolumeName = "tls-server"); constant!(TLS_SERVER_CA_VOLUME_NAME: VolumeName = "tls-server-ca"); -constant!(TLS_INTERNAL_VOLUME_NAME: VolumeName = "tls-internal"); const TLS_SERVER_CA_VOLUME_SIZE: &str = "1Mi"; +constant!(TLS_INTERNAL_VOLUME_NAME: VolumeName = "tls-internal"); constant!(TLS_ADMIN_CERT_VOLUME_NAME: VolumeName = "tls-admin-cert"); const TLS_ADMIN_CERT_VOLUME_SIZE: &str = "1Mi"; @@ -113,8 +113,9 @@ enum RoleGroupBuilderSecurityMode<'a> { /// The security plugin is enabled and some or all settings are initialized and updated by this /// role group. - /// The admin certificate is created in an init container and the security settings are mounted - /// to and updated in a side-car container. + /// The admin certificate is created in the [`v1alpha1::Container::CreateAdminCertificate`] + /// init container and the security settings are mounted to and updated in the + /// [`v1alpha1::Container::UpdateSecurityConfig`] side-car container. Managing(&'a ValidatedSecurity), /// The security plugin is enabled and the settings are managed by another role group. @@ -471,7 +472,7 @@ impl<'a> RoleGroupBuilder<'a> { }); } - Some( + let container = new_container_builder(&v1alpha1::Container::InitKeystore.to_container_name()) .image_from_product_image(&self.cluster.image) .command(vec!["/bin/bash".to_owned(), "-c".to_owned()]) @@ -479,8 +480,9 @@ impl<'a> RoleGroupBuilder<'a> { .add_volume_mounts(volume_mounts) .expect("The mount paths are statically defined and there should be no duplicates.") .resources(self.role_group_config.config.resources.clone().into()) - .build(), - ) + .build(); + + Some(container) } /// Builds the [`v1alpha1::Container::CreateAdminCertificate`] init container for the @@ -522,33 +524,31 @@ impl<'a> RoleGroupBuilder<'a> { }, ]; - let container = new_container_builder( - &ContainerName::from_str("create-admin-certificate") - .expect("should be a valid container name"), - ) - .image_from_product_image(&self.cluster.image) - .command(vec!["/bin/bash".to_string(), "-c".to_string()]) - .args(vec![ - include_str!("scripts/create-admin-certificate.sh").to_owned(), - ]) - .add_env_vars(env_vars.into()) - .add_volume_mounts(volume_mounts) - .expect("The mount paths are statically defined and there should be no duplicates.") - .resources( - Resources::<()> { - memory: MemoryLimits { - limit: Some(Quantity("128Mi".to_owned())), - ..MemoryLimits::default() - }, - cpu: CpuLimits { - min: Some(Quantity("100m".to_owned())), - max: Some(Quantity("400m".to_owned())), - }, - ..Resources::default() - } - .into(), - ) - .build(); + let container = + new_container_builder(&v1alpha1::Container::CreateAdminCertificate.to_container_name()) + .image_from_product_image(&self.cluster.image) + .command(vec!["/bin/bash".to_string(), "-c".to_string()]) + .args(vec![ + include_str!("scripts/create-admin-certificate.sh").to_owned(), + ]) + .add_env_vars(env_vars.into()) + .add_volume_mounts(volume_mounts) + .expect("The mount paths are statically defined and there should be no duplicates.") + .resources( + Resources::<()> { + memory: MemoryLimits { + limit: Some(Quantity("128Mi".to_owned())), + ..MemoryLimits::default() + }, + cpu: CpuLimits { + min: Some(Quantity("100m".to_owned())), + max: Some(Quantity("400m".to_owned())), + }, + ..Resources::default() + } + .into(), + ) + .build(); Some(container) } @@ -825,7 +825,7 @@ impl<'a> RoleGroupBuilder<'a> { ); } - Some( + let container = new_container_builder(&v1alpha1::Container::UpdateSecurityConfig.to_container_name()) .image_from_product_image(&self.cluster.image) .command(vec!["/bin/bash".to_string(), "-c".to_string()]) @@ -849,8 +849,9 @@ impl<'a> RoleGroupBuilder<'a> { } .into(), ) - .build(), - ) + .build(); + + Some(container) } /// Builds the config volumes for the [`PodTemplateSpec`] @@ -1257,6 +1258,7 @@ mod tests { }; use pretty_assertions::assert_eq; + use rstest::rstest; use serde_json::json; use stackable_operator::{ commons::{ @@ -1282,7 +1284,7 @@ mod tests { ValidatedSecurity, build::role_group_builder::{ DISCOVERY_SERVICE_LISTENER_VOLUME_NAME, OPENSEARCH_KEYSTORE_VOLUME_NAME, - TLS_INTERNAL_VOLUME_NAME, TLS_SERVER_VOLUME_NAME, + TLS_INTERNAL_VOLUME_NAME, TLS_SERVER_CA_VOLUME_NAME, TLS_SERVER_VOLUME_NAME, }, }, crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, @@ -1292,8 +1294,8 @@ mod tests { role_utils::GenericProductSpecificCommonConfig, types::{ kubernetes::{ - ConfigMapName, ListenerClassName, ListenerName, NamespaceName, SecretKey, - SecretName, ServiceAccountName, ServiceName, + ConfigMapKey, ConfigMapName, ListenerClassName, ListenerName, NamespaceName, + SecretKey, SecretName, ServiceAccountName, ServiceName, }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, @@ -1312,6 +1314,7 @@ mod tests { let _ = ROLE_GROUP_LISTENER_VOLUME_NAME; let _ = DISCOVERY_SERVICE_LISTENER_VOLUME_NAME; let _ = TLS_SERVER_VOLUME_NAME; + let _ = TLS_SERVER_CA_VOLUME_NAME; let _ = TLS_INTERNAL_VOLUME_NAME; let _ = LOG_VOLUME_NAME; let _ = OPENSEARCH_KEYSTORE_VOLUME_NAME; @@ -1327,7 +1330,15 @@ mod tests { } } - fn validated_cluster() -> ValidatedCluster { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum TestSecurityMode { + Initializing, + Managing, + Participating, + Disabled, + } + + fn validated_cluster(security_mode: TestSecurityMode) -> ValidatedCluster { let image = ResolvedProductImage { product_version: "3.4.0".to_owned(), app_version_label_value: LabelValue::from_str("3.4.0-stackable0.0.0-dev") @@ -1374,6 +1385,71 @@ mod tests { product_specific_common_config: GenericProductSpecificCommonConfig::default(), }; + let security_settings = v1alpha1::SecurityConfig { + config: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, + content: v1alpha1::SecurityConfigFileTypeContent::Value( + v1alpha1::SecurityConfigFileTypeContentValue { + value: json!({ + "_meta": { + "type": "config", + "config_version": 2 + }, + "config": { + "dynamic": { + "http": {}, + "authc": {}, + "authz": {} + } + } + }), + }, + ), + }, + internal_users: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::SecretKeyRef( + v1alpha1::SecretKeyRef { + name: SecretName::from_str_unsafe("opensearch-security-config"), + key: SecretKey::from_str_unsafe("internal_users.yml"), + }, + ), + ), + }, + roles: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { + name: ConfigMapName::from_str_unsafe("opensearch-security-config"), + key: ConfigMapKey::from_str_unsafe("roles.yml"), + }, + ), + ), + }, + ..v1alpha1::SecurityConfig::default() + }; + + let security = match security_mode { + TestSecurityMode::Initializing => Some(ValidatedSecurity { + managing_role_group: None, + settings: security_settings, + tls: v1alpha1::OpenSearchTls::default(), + }), + TestSecurityMode::Managing => Some(ValidatedSecurity { + managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), + settings: security_settings, + tls: v1alpha1::OpenSearchTls::default(), + }), + TestSecurityMode::Participating => Some(ValidatedSecurity { + managing_role_group: Some(RoleGroupName::from_str_unsafe("other")), + settings: security_settings, + tls: v1alpha1::OpenSearchTls::default(), + }), + TestSecurityMode::Disabled => None, + }; + ValidatedCluster::new( image.clone(), ProductVersion::from_str_unsafe(&image.product_version), @@ -1386,11 +1462,7 @@ mod tests { role_group_config.clone(), )] .into(), - Some(ValidatedSecurity { - managing_role_group: None, - settings: v1alpha1::SecurityConfig::default(), - tls: v1alpha1::OpenSearchTls::default(), - }), + security, vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { @@ -1425,9 +1497,13 @@ mod tests { ) } - #[test] - fn test_build_config_map() { - let cluster = validated_cluster(); + #[rstest] + #[case::security_mode_initializing(TestSecurityMode::Initializing)] + #[case::security_mode_managing(TestSecurityMode::Managing)] + #[case::security_mode_participating(TestSecurityMode::Participating)] + #[case::security_mode_disabled(TestSecurityMode::Disabled)] + fn test_build_config_map(#[case] security_mode: TestSecurityMode) { + let cluster = validated_cluster(security_mode); let context_names = context_names(); let role_group_builder = role_group_builder(&cluster, &context_names); @@ -1443,6 +1519,26 @@ mod tests { // vector.yaml is a static file and does not have to be repeated here. config_map["data"]["vector.yaml"].take(); + let expected_data = match security_mode { + TestSecurityMode::Initializing | TestSecurityMode::Managing => json!({ + "action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}", + "allow_list.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\":{}}}}", + "log4j2.properties": null, + "nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}", + "opensearch.yml": null, + "roles_mapping.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"rolesmapping\"}}", + "tenants.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"tenants\"}}", + "vector.yaml": null + }), + TestSecurityMode::Participating | TestSecurityMode::Disabled => json!({ + "log4j2.properties": null, + "opensearch.yml": null, + "vector.yaml": null + }), + }; + assert_eq!( json!({ "apiVersion": "v1", @@ -1469,34 +1565,1338 @@ mod tests { } ] }, - "data": { - "action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}", - "allow_list.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\"}}", - "log4j2.properties": null, - "nodes_dn.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"nodesdn\"}}", - "opensearch.yml": null, - "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\"}}", - "vector.yaml": null - } + "data": expected_data }), config_map ); } - #[test] - fn test_build_stateful_set() { - let cluster = validated_cluster(); + #[rstest] + #[case::security_mode_initializing(TestSecurityMode::Initializing)] + #[case::security_mode_managing(TestSecurityMode::Managing)] + #[case::security_mode_participating(TestSecurityMode::Participating)] + #[case::security_mode_disabled(TestSecurityMode::Disabled)] + fn test_build_stateful_set(#[case] security_mode: TestSecurityMode) { + let cluster = validated_cluster(security_mode); let context_names = context_names(); let role_group_builder = role_group_builder(&cluster, &context_names); let stateful_set = serde_json::to_value(role_group_builder.build_stateful_set()) .expect("should be serializable"); + let expected_opensearch_container_volume_mounts = match security_mode { + TestSecurityMode::Initializing => json!([ + { + "mountPath": "/stackable/opensearch/config/opensearch.yml", + "name": "config", + "readOnly": true, + "subPath": "opensearch.yml" + }, + { + "mountPath": "/stackable/opensearch/config/log4j2.properties", + "name": "log-config", + "readOnly": true, + "subPath": "log4j2.properties" + }, + { + "mountPath": "/stackable/opensearch/data", + "name": "data" + }, + { + "mountPath": "/stackable/listeners/role-group", + "name": "listener" + }, + { + "mountPath": "/stackable/log", + "name": "log" + }, + { + "mountPath": "/stackable/listeners/discovery-service", + "name": "discovery-service-listener" + }, + { + "mountPath": "/stackable/opensearch/config/tls/internal", + "name": "tls-internal" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server", + "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", + "name": "tls-server", + "subPath": "tls.crt" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/tls.key", + "name": "tls-server", + "subPath": "tls.key" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/ca.crt", + "name": "tls-server", + "subPath": "ca.crt" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/action_groups.yml", + "name": "security-config-file-actiongroups", + "readOnly": true, + "subPath": "action_groups.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", + "name": "security-config-file-allowlist", + "readOnly": true, + "subPath": "allow_list.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", + "name": "security-config-file-audit", + "readOnly": true, + "subPath": "audit.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/config.yml", + "name": "security-config-file-config", + "readOnly": true, + "subPath": "config.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/internal_users.yml", + "name": "security-config-file-internalusers", + "readOnly": true, + "subPath": "internal_users.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/nodes_dn.yml", + "name": "security-config-file-nodesdn", + "readOnly": true, + "subPath": "nodes_dn.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles.yml", + "name": "security-config-file-roles", + "readOnly": true, + "subPath": "roles.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles_mapping.yml", + "name": "security-config-file-rolesmapping", + "readOnly": true, + "subPath": "roles_mapping.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/tenants.yml", + "name": "security-config-file-tenants", + "readOnly": true, + "subPath": "tenants.yml" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore" + } + ]), + TestSecurityMode::Managing => json!([ + { + "mountPath": "/stackable/opensearch/config/opensearch.yml", + "name": "config", + "readOnly": true, + "subPath": "opensearch.yml" + }, + { + "mountPath": "/stackable/opensearch/config/log4j2.properties", + "name": "log-config", + "readOnly": true, + "subPath": "log4j2.properties" + }, + { + "mountPath": "/stackable/opensearch/data", + "name": "data" + }, + { + "mountPath": "/stackable/listeners/role-group", + "name": "listener" + }, + { + "mountPath": "/stackable/log", + "name": "log" + }, + { + "mountPath": "/stackable/listeners/discovery-service", + "name": "discovery-service-listener" + }, + { + "mountPath": "/stackable/opensearch/config/tls/internal", + "name": "tls-internal" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server", + "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", + "name": "tls-server", + "subPath": "tls.crt" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/tls.key", + "name": "tls-server", + "subPath": "tls.key" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/ca.crt", + "name": "tls-server-ca", + "subPath": "ca.crt" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore" + } + ]), + TestSecurityMode::Participating => json!([ + { + "mountPath": "/stackable/opensearch/config/opensearch.yml", + "name": "config", + "readOnly": true, + "subPath": "opensearch.yml" + }, + { + "mountPath": "/stackable/opensearch/config/log4j2.properties", + "name": "log-config", + "readOnly": true, + "subPath": "log4j2.properties" + }, + { + "mountPath": "/stackable/opensearch/data", + "name": "data" + }, + { + "mountPath": "/stackable/listeners/role-group", + "name": "listener" + }, + { + "mountPath": "/stackable/log", + "name": "log" + }, + { + "mountPath": "/stackable/listeners/discovery-service", + "name": "discovery-service-listener" + }, + { + "mountPath": "/stackable/opensearch/config/tls/internal", + "name": "tls-internal" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server", + "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", + "name": "tls-server", + "subPath": "tls.crt" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/tls.key", + "name": "tls-server", + "subPath": "tls.key" + }, + { + "mountPath": "/stackable/opensearch/config/tls/server/ca.crt", + "name": "tls-server", + "subPath": "ca.crt" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore" + } + ]), + TestSecurityMode::Disabled => json!([ + { + "mountPath": "/stackable/opensearch/config/opensearch.yml", + "name": "config", + "readOnly": true, + "subPath": "opensearch.yml" + }, + { + "mountPath": "/stackable/opensearch/config/log4j2.properties", + "name": "log-config", + "readOnly": true, + "subPath": "log4j2.properties" + }, + { + "mountPath": "/stackable/opensearch/data", + "name": "data" + }, + { + "mountPath": "/stackable/listeners/role-group", + "name": "listener" + }, + { + "mountPath": "/stackable/log", + "name": "log" + }, + { + "mountPath": "/stackable/listeners/discovery-service", + "name": "discovery-service-listener" + }, + { + "mountPath": "/stackable/opensearch/config/opensearch.keystore", + "name": "keystore", + "readOnly": true, + "subPath": "opensearch.keystore" + } + ]), + }; + + let expected_opensearch_container = json!({ + "args": [ + concat!( + "\n", + "prepare_signal_handlers()\n", + "{\n", + " unset term_child_pid\n", + " unset term_kill_needed\n", + " trap 'handle_term_signal' TERM\n", + "}\n", + "\n", + "handle_term_signal()\n", + "{\n", + " if [ \"${term_child_pid}\" ]; then\n", + " kill -TERM \"${term_child_pid}\" 2>/dev/null\n", + " else\n", + " term_kill_needed=\"yes\"\n", + " fi\n", + "}\n", + "\n", + "wait_for_termination()\n", + "{\n", + " set +e\n", + " term_child_pid=$1\n", + " if [[ -v term_kill_needed ]]; then\n", + " kill -TERM \"${term_child_pid}\" 2>/dev/null\n", + " fi\n", + " wait ${term_child_pid} 2>/dev/null\n", + " trap - TERM\n", + " wait ${term_child_pid} 2>/dev/null\n", + " set -e\n", + "}\n", + "\n", + "rm -f /stackable/log/_vector/shutdown\n", + "prepare_signal_handlers\n", + "if command --search containerdebug >/dev/null 2>&1; then\n", + "containerdebug --output=/stackable/log/containerdebug-state.json --loop &\n", + "else\n", + "echo >&2 \"containerdebug not installed; Proceed without it.\"\n", + "fi\n", + "./opensearch-docker-entrypoint.sh &\n", + "wait_for_termination $!\n", + "mkdir -p /stackable/log/_vector && touch /stackable/log/_vector/shutdown" + ) + ], + "command": [ + "/bin/bash", + "-x", + "-euo", + "pipefail", + "-c" + ], + "env": [ + { + "name": "_POD_NAME", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.name" + } + } + }, + { + "name": "discovery.seed_hosts", + "value": "my-opensearch-cluster-seed-nodes.default.svc.cluster.local" + }, + { + "name": "http.publish_host", + "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" + }, + { + "name": "network.publish_host", + "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" + }, + { + "name": "node.name", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.name" + } + } + }, + { + "name": "node.roles", + "value": "cluster_manager,data,ingest,remote_cluster_client" + }, + { + "name": "transport.publish_host", + "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" + }, + ], + "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "opensearch", + "ports": [ + { + "containerPort": 9200, + "name": "http" + }, + { + "containerPort": 9300, + "name": "transport" + } + ], + "readinessProbe": { + "failureThreshold": 3, + "periodSeconds": 5, + "tcpSocket": { + "port": "http" + }, + "timeoutSeconds": 3 + }, + "resources": {}, + "startupProbe": { + "failureThreshold": 30, + "initialDelaySeconds": 5, + "periodSeconds": 10, + "tcpSocket": { + "port": "http" + }, + "timeoutSeconds": 3 + }, + "volumeMounts": expected_opensearch_container_volume_mounts + }); + + let expected_vector_container = json!({ + "args": [ + concat!( + "# Vector will ignore SIGTERM (as PID != 1) and must be shut down by writing a shutdown trigger file\n", + "vector & vector_pid=$!\n", + "if [ ! -f \"/stackable/log/_vector/shutdown\" ]; then\n", + "mkdir -p /stackable/log/_vector\n", + "inotifywait -qq --event create /stackable/log/_vector;\n", + "fi\n", + "sleep 1\n", + "kill $vector_pid" + ), + ], + "command": [ + "/bin/bash", + "-x", + "-euo", + "pipefail", + "-c" + ], + "env": [ + { + "name": "CLUSTER_NAME", + "value":"my-opensearch-cluster", + }, + { + "name": "LOG_DIR", + "value": "/stackable/log", + }, + { + "name": "NAMESPACE", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.namespace", + }, + }, + }, + { + "name": "OPENSEARCH_SERVER_LOG_FILE", + "value": "opensearch_server.json", + }, + { + "name": "ROLE_GROUP_NAME", + "value": "default", + }, + { + "name": "ROLE_NAME", + "value": "nodes", + }, + { + "name": "VECTOR_AGGREGATOR_ADDRESS", + "valueFrom": { + "configMapKeyRef": { + "key": "ADDRESS", + "name": "vector-aggregator", + }, + }, + }, + { + "name": "VECTOR_CONFIG_YAML", + "value": "/stackable/config/vector.yaml", + }, + { + "name": "VECTOR_FILE_LOG_LEVEL", + "value": "info", + }, + { + "name": "VECTOR_LOG", + "value": "info", + }, + ], + "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "vector", + "resources": { + "limits": { + "cpu": "500m", + "memory": "128Mi", + }, + "requests": { + "cpu": "250m", + "memory": "128Mi", + }, + }, + "volumeMounts": [ + { + "mountPath": "/stackable/config/vector.yaml", + "name": "config", + "readOnly": true, + "subPath": "vector.yaml", + }, + { + "mountPath": "/stackable/log", + "name": "log", + }, + ], + }); + + let expected_update_security_config_container = json!({ + "args": [ + include_str!("scripts/update-security-config.sh") + ], + "command": [ + "/bin/bash", + "-c", + ], + "env": [ + { + "name": "MANAGE_ACTIONGROUPS", + "value": "false", + }, + { + "name": "MANAGE_ALLOWLIST", + "value": "false", + }, + { + "name": "MANAGE_AUDIT", + "value": "false", + }, + { + "name": "MANAGE_CONFIG", + "value": "true", + }, + { + "name": "MANAGE_INTERNALUSERS", + "value": "false", + }, + { + "name": "MANAGE_NODESDN", + "value": "false", + }, + { + "name": "MANAGE_ROLES", + "value": "false", + }, + { + "name": "MANAGE_ROLESMAPPING", + "value": "false", + }, + { + "name": "MANAGE_TENANTS", + "value": "false", + }, + { + "name": "OPENSEARCH_PATH_CONF", + "value": "/stackable/opensearch/config", + }, + { + "name": "POD_NAME", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.name", + }, + }, + }, + ], + "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "update-security-config", + "resources": { + "limits": { + "cpu": "400m", + "memory": "512Mi", + }, + "requests": { + "cpu": "100m", + "memory": "512Mi", + }, + }, + "volumeMounts": [ + { + "mountPath": "/stackable/opensearch/config/tls/tls.crt", + "name": "tls-admin-cert", + "readOnly": true, + "subPath": "tls.crt", + }, + { + "mountPath": "/stackable/opensearch/config/tls/tls.key", + "name": "tls-admin-cert", + "readOnly": true, + "subPath": "tls.key", + }, + { + "mountPath": "/stackable/opensearch/config/tls/ca.crt", + "name": "tls-server-ca", + "readOnly": true, + "subPath": "ca.crt", + }, + { + "mountPath": "/stackable/log", + "name": "log", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/action_groups.yml", + "name": "security-config-file-actiongroups", + "readOnly": true, + "subPath": "action_groups.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", + "name": "security-config-file-allowlist", + "readOnly": true, + "subPath": "allow_list.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", + "name": "security-config-file-audit", + "readOnly": true, + "subPath": "audit.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/config.yml", + "name": "security-config-file-config", + "readOnly": true, + "subPath": "config.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/internal_users.yml", + "name": "security-config-file-internalusers", + "readOnly": true, + "subPath": "internal_users.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/nodes_dn.yml", + "name": "security-config-file-nodesdn", + "readOnly": true, + "subPath": "nodes_dn.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles.yml", + "name": "security-config-file-roles", + "readOnly": true, + "subPath": "roles.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/roles_mapping.yml", + "name": "security-config-file-rolesmapping", + "readOnly": true, + "subPath": "roles_mapping.yml", + }, + { + "mountPath": "/stackable/opensearch/config/opensearch-security/tenants.yml", + "name": "security-config-file-tenants", + "readOnly": true, + "subPath": "tenants.yml", + }, + ], + }); + + let expected_containers = match security_mode { + TestSecurityMode::Initializing => { + json!([expected_opensearch_container, expected_vector_container]) + } + TestSecurityMode::Managing => { + json!([ + expected_opensearch_container, + expected_vector_container, + expected_update_security_config_container + ]) + } + TestSecurityMode::Participating => { + json!([expected_opensearch_container, expected_vector_container]) + } + TestSecurityMode::Disabled => { + json!([expected_opensearch_container, expected_vector_container]) + } + }; + + let expected_init_keystore_container = json!({ + "args": [ + include_str!("scripts/init-keystore.sh") + ], + "command": [ + "/bin/bash", + "-c" + ], + "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "init-keystore", + "resources": {}, + "volumeMounts": [ + { + "mountPath": "/stackable/opensearch/initialized-keystore", + "name": "keystore", + }, + { + "mountPath": "/stackable/opensearch/keystore-secrets/Keystore1", + "name": "keystore-0", + "readOnly": true, + "subPath": "my-keystore-file" + } + ] + }); + + let expected_create_admin_certificate_container = json!({ + "args": [ + include_str!("scripts/create-admin-certificate.sh") + ], + "command": [ + "/bin/bash", + "-c", + ], + "env": [ + { + "name": "ADMIN_DN", + "value": "CN=update-security-config.0b1e30e6-326e-4c1a-868d-ad6598b49e8b", + }, + { + "name": "POD_NAME", + "valueFrom": { + "fieldRef": { + "fieldPath": "metadata.name", + }, + }, + }, + ], + "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", + "imagePullPolicy": "Always", + "name": "create-admin-certificate", + "resources": { + "limits": { + "cpu": "400m", + "memory": "128Mi", + }, + "requests": { + "cpu": "100m", + "memory": "128Mi", + }, + }, + "volumeMounts": [ + { + "mountPath": "/stackable/tls-server/ca.crt", + "name": "tls-server", + "readOnly": true, + "subPath": "ca.crt", + }, + { + "mountPath": "/stackable/tls-admin-cert", + "name": "tls-admin-cert", + "readOnly": false, + }, + { + "mountPath": "/stackable/tls-server-ca", + "name": "tls-server-ca", + "readOnly": false, + }, + ], + }); + + let expected_init_containers = match security_mode { + TestSecurityMode::Initializing => json!([expected_init_keystore_container]), + TestSecurityMode::Managing => json!([ + expected_init_keystore_container, + expected_create_admin_certificate_container + ]), + TestSecurityMode::Participating => json!([expected_init_keystore_container]), + TestSecurityMode::Disabled => json!([expected_init_keystore_container]), + }; + + let expected_volumes = match security_mode { + TestSecurityMode::Initializing => json!([ + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "config" + }, + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "log-config" + }, + { + "emptyDir": { + "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": "pod,listener-volume=listener,service=my-opensearch-cluster-seed-nodes" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-internal" + }, + { + "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": "pod,listener-volume=listener,listener-volume=discovery-service-listener" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-server" + }, + { + "configMap": { + "items": [ + { + "key": "action_groups.yml", + "mode": 0o660, + "path": "action_groups.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-actiongroups" + }, + { + "configMap": { + "items": [ + { + "key": "allow_list.yml", + "mode": 0o660, + "path": "allow_list.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-allowlist" + }, + { + "configMap": { + "items": [ + { + "key": "audit.yml", + "mode": 0o660, + "path": "audit.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-audit" + }, + { + "configMap": { + "items": [ + { + "key": "config.yml", + "mode": 0o660, + "path": "config.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-config" + }, + { + "name": "security-config-file-internalusers", + "secret": { + "items": [ + { + "key": "internal_users.yml", + "mode": 0o660, + "path": "internal_users.yml" + } + ], + "secretName": "opensearch-security-config" + } + }, + { + "configMap": { + "items": [ + { + "key": "nodes_dn.yml", + "mode": 0o660, + "path": "nodes_dn.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-nodesdn" + }, + { + "configMap": { + "items": [ + { + "key": "roles.yml", + "mode": 0o660, + "path": "roles.yml" + } + ], + "name": "opensearch-security-config" + }, + "name": "security-config-file-roles" + }, + { + "configMap": { + "items": [ + { + "key": "roles_mapping.yml", + "mode": 0o660, + "path": "roles_mapping.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-rolesmapping" + }, + { + "configMap": { + "items": [ + { + "key": "tenants.yml", + "mode": 0o660, + "path": "tenants.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-tenants" + }, + { + "emptyDir": { + "sizeLimit": "1Mi" + }, + "name": "keystore" + }, + { + "name": "keystore-0", + "secret": { + "defaultMode": 0o660, + "items": [ + { + "key": "my-keystore-file", + "path": "my-keystore-file" + } + ], + "secretName": "my-keystore-secret" + } + } + ]), + TestSecurityMode::Managing => json!([ + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "config" + }, + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "log-config" + }, + { + "emptyDir": { + "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": "pod,listener-volume=listener,service=my-opensearch-cluster-seed-nodes" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-internal" + }, + { + "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": "pod,listener-volume=listener,listener-volume=discovery-service-listener" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-server" + }, + { + "configMap": { + "items": [ + { + "key": "action_groups.yml", + "mode": 0o660, + "path": "action_groups.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-actiongroups" + }, + { + "configMap": { + "items": [ + { + "key": "allow_list.yml", + "mode": 0o660, + "path": "allow_list.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-allowlist" + }, + { + "configMap": { + "items": [ + { + "key": "audit.yml", + "mode": 0o660, + "path": "audit.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-audit" + }, + { + "configMap": { + "items": [ + { + "key": "config.yml", + "mode": 0o660, + "path": "config.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-config" + }, + { + "name": "security-config-file-internalusers", + "secret": { + "items": [ + { + "key": "internal_users.yml", + "mode": 0o660, + "path": "internal_users.yml" + } + ], + "secretName": "opensearch-security-config" + } + }, + { + "configMap": { + "items": [ + { + "key": "nodes_dn.yml", + "mode": 0o660, + "path": "nodes_dn.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-nodesdn" + }, + { + "configMap": { + "items": [ + { + "key": "roles.yml", + "mode": 0o660, + "path": "roles.yml" + } + ], + "name": "opensearch-security-config" + }, + "name": "security-config-file-roles" + }, + { + "configMap": { + "items": [ + { + "key": "roles_mapping.yml", + "mode": 0o660, + "path": "roles_mapping.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-rolesmapping" + }, + { + "configMap": { + "items": [ + { + "key": "tenants.yml", + "mode": 0o660, + "path": "tenants.yml" + } + ], + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "security-config-file-tenants" + }, + { + "emptyDir": { + "sizeLimit": "1Mi", + }, + "name": "tls-server-ca", + }, + { + "emptyDir": { + "sizeLimit": "1Mi", + }, + "name": "tls-admin-cert", + }, + { + "emptyDir": { + "sizeLimit": "1Mi" + }, + "name": "keystore" + }, + { + "name": "keystore-0", + "secret": { + "defaultMode": 0o660, + "items": [ + { + "key": "my-keystore-file", + "path": "my-keystore-file" + } + ], + "secretName": "my-keystore-secret" + } + } + ]), + TestSecurityMode::Participating => json!([ + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "config" + }, + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "log-config" + }, + { + "emptyDir": { + "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": "pod,listener-volume=listener,service=my-opensearch-cluster-seed-nodes" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-internal" + }, + { + "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": "pod,listener-volume=listener,listener-volume=discovery-service-listener" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1" + } + }, + "storageClassName": "secrets.stackable.tech" + } + } + }, + "name": "tls-server" + }, + { + "emptyDir": { + "sizeLimit": "1Mi" + }, + "name": "keystore" + }, + { + "name": "keystore-0", + "secret": { + "defaultMode": 0o660, + "items": [ + { + "key": "my-keystore-file", + "path": "my-keystore-file" + } + ], + "secretName": "my-keystore-secret" + } + } + ]), + TestSecurityMode::Disabled => json!([ + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "config" + }, + { + "configMap": { + "defaultMode": 0o660, + "name": "my-opensearch-cluster-nodes-default" + }, + "name": "log-config" + }, + { + "emptyDir": { + "sizeLimit": "30Mi" + }, + "name": "log" + }, + { + "emptyDir": { + "sizeLimit": "1Mi" + }, + "name": "keystore" + }, + { + "name": "keystore-0", + "secret": { + "defaultMode": 0o660, + "items": [ + { + "key": "my-keystore-file", + "path": "my-keystore-file" + } + ], + "secretName": "my-keystore-secret" + } + } + ]), + }; + assert_eq!( json!({ "apiVersion": "apps/v1", @@ -1557,580 +2957,14 @@ mod tests { }, "spec": { "affinity": {}, - "containers": [ - { - "args": [ - concat!( - "\n", - "prepare_signal_handlers()\n", - "{\n", - " unset term_child_pid\n", - " unset term_kill_needed\n", - " trap 'handle_term_signal' TERM\n", - "}\n", - "\n", - "handle_term_signal()\n", - "{\n", - " if [ \"${term_child_pid}\" ]; then\n", - " kill -TERM \"${term_child_pid}\" 2>/dev/null\n", - " else\n", - " term_kill_needed=\"yes\"\n", - " fi\n", - "}\n", - "\n", - "wait_for_termination()\n", - "{\n", - " set +e\n", - " term_child_pid=$1\n", - " if [[ -v term_kill_needed ]]; then\n", - " kill -TERM \"${term_child_pid}\" 2>/dev/null\n", - " fi\n", - " wait ${term_child_pid} 2>/dev/null\n", - " trap - TERM\n", - " wait ${term_child_pid} 2>/dev/null\n", - " set -e\n", - "}\n", - "\n", - "rm -f /stackable/log/_vector/shutdown\n", - "prepare_signal_handlers\n", - "if command --search containerdebug >/dev/null 2>&1; then\n", - "containerdebug --output=/stackable/log/containerdebug-state.json --loop &\n", - "else\n", - "echo >&2 \"containerdebug not installed; Proceed without it.\"\n", - "fi\n", - "./opensearch-docker-entrypoint.sh &\n", - "wait_for_termination $!\n", - "mkdir -p /stackable/log/_vector && touch /stackable/log/_vector/shutdown" - ) - ], - "command": [ - "/bin/bash", - "-x", - "-euo", - "pipefail", - "-c" - ], - "env": [ - { - "name": "_POD_NAME", - "valueFrom": { - "fieldRef": { - "fieldPath": "metadata.name" - } - } - }, - { - "name": "discovery.seed_hosts", - "value": "my-opensearch-cluster-seed-nodes.default.svc.cluster.local" - }, - { - "name": "http.publish_host", - "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" - }, - { - "name": "network.publish_host", - "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" - }, - { - "name": "node.name", - "valueFrom": { - "fieldRef": { - "fieldPath": "metadata.name" - } - } - }, - { - "name": "node.roles", - "value": "cluster_manager,data,ingest,remote_cluster_client" - }, - { - "name": "transport.publish_host", - "value": "$(_POD_NAME).my-opensearch-cluster-nodes-default-headless.default.svc.cluster.local" - }, - ], - "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", - "imagePullPolicy": "Always", - "name": "opensearch", - "ports": [ - { - "containerPort": 9200, - "name": "http" - }, - { - "containerPort": 9300, - "name": "transport" - } - ], - "readinessProbe": { - "failureThreshold": 3, - "periodSeconds": 5, - "tcpSocket": { - "port": "http" - }, - "timeoutSeconds": 3 - }, - "resources": {}, - "startupProbe": { - "failureThreshold": 30, - "initialDelaySeconds": 5, - "periodSeconds": 10, - "tcpSocket": { - "port": "http" - }, - "timeoutSeconds": 3 - }, - "volumeMounts": [ - { - "mountPath": "/stackable/opensearch/config/opensearch.yml", - "name": "config", - "readOnly": true, - "subPath": "opensearch.yml" - }, - { - "mountPath": "/stackable/opensearch/config/log4j2.properties", - "name": "log-config", - "readOnly": true, - "subPath": "log4j2.properties" - }, - { - "mountPath": "/stackable/opensearch/data", - "name": "data" - }, - { - "mountPath": "/stackable/listeners/role-group", - "name": "listener" - }, - { - "mountPath": "/stackable/log", - "name": "log" - }, - { - "mountPath": "/stackable/listeners/discovery-service", - "name": "discovery-service-listener" - }, - { - "mountPath": "/stackable/opensearch/config/tls/internal", - "name": "tls-internal" - }, - { - "mountPath": "/stackable/opensearch/config/tls/server", - "mountPath": "/stackable/opensearch/config/tls/server/tls.crt", - "name": "tls-server", - "subPath": "tls.crt" - }, - { - "mountPath": "/stackable/opensearch/config/tls/server/tls.key", - "name": "tls-server", - "subPath": "tls.key" - }, - { - "mountPath": "/stackable/opensearch/config/tls/server/ca.crt", - "name": "tls-server", - "subPath": "ca.crt" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/action_groups.yml", - "name": "security-config-file-actiongroups", - "readOnly": true, - "subPath": "action_groups.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", - "name": "security-config-file-allowlist", - "readOnly": true, - "subPath": "allow_list.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", - "name": "security-config-file-audit", - "readOnly": true, - "subPath": "audit.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/config.yml", - "name": "security-config-file-config", - "readOnly": true, - "subPath": "config.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/internal_users.yml", - "name": "security-config-file-internalusers", - "readOnly": true, - "subPath": "internal_users.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/nodes_dn.yml", - "name": "security-config-file-nodesdn", - "readOnly": true, - "subPath": "nodes_dn.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/roles.yml", - "name": "security-config-file-roles", - "readOnly": true, - "subPath": "roles.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/roles_mapping.yml", - "name": "security-config-file-rolesmapping", - "readOnly": true, - "subPath": "roles_mapping.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch-security/tenants.yml", - "name": "security-config-file-tenants", - "readOnly": true, - "subPath": "tenants.yml" - }, - { - "mountPath": "/stackable/opensearch/config/opensearch.keystore", - "name": "keystore", - "readOnly": true, - "subPath": "opensearch.keystore" - } - ] - }, - { - "args": [ - concat!( - "# Vector will ignore SIGTERM (as PID != 1) and must be shut down by writing a shutdown trigger file\n", - "vector & vector_pid=$!\n", - "if [ ! -f \"/stackable/log/_vector/shutdown\" ]; then\n", - "mkdir -p /stackable/log/_vector\n", - "inotifywait -qq --event create /stackable/log/_vector;\n", - "fi\n", - "sleep 1\n", - "kill $vector_pid" - ), - ], - "command": [ - "/bin/bash", - "-x", - "-euo", - "pipefail", - "-c" - ], - "env": [ - { - "name": "CLUSTER_NAME", - "value":"my-opensearch-cluster", - }, - { - "name": "LOG_DIR", - "value": "/stackable/log", - }, - { - "name": "NAMESPACE", - "valueFrom": { - "fieldRef": { - "fieldPath": "metadata.namespace", - }, - }, - }, - { - "name": "OPENSEARCH_SERVER_LOG_FILE", - "value": "opensearch_server.json", - }, - { - "name": "ROLE_GROUP_NAME", - "value": "default", - }, - { - "name": "ROLE_NAME", - "value": "nodes", - }, - { - "name": "VECTOR_AGGREGATOR_ADDRESS", - "valueFrom": { - "configMapKeyRef": { - "key": "ADDRESS", - "name": "vector-aggregator", - }, - }, - }, - { - "name": "VECTOR_CONFIG_YAML", - "value": "/stackable/config/vector.yaml", - }, - { - "name": "VECTOR_FILE_LOG_LEVEL", - "value": "info", - }, - { - "name": "VECTOR_LOG", - "value": "info", - }, - ], - "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", - "imagePullPolicy": "Always", - "name": "vector", - "resources": { - "limits": { - "cpu": "500m", - "memory": "128Mi", - }, - "requests": { - "cpu": "250m", - "memory": "128Mi", - }, - }, - "volumeMounts": [ - { - "mountPath": "/stackable/config/vector.yaml", - "name": "config", - "readOnly": true, - "subPath": "vector.yaml", - }, - { - "mountPath": "/stackable/log", - "name": "log", - }, - ], - }, - ], - "initContainers": [ - { - "args": [ - include_str!("scripts/init-keystore.sh") - ], - "command": [ - "/bin/bash", - "-c" - ], - "image": "oci.stackable.tech/sdp/opensearch:3.4.0-stackable0.0.0-dev", - "imagePullPolicy": "Always", - "name": "init-keystore", - "resources": {}, - "volumeMounts": [ - { - "mountPath": "/stackable/opensearch/initialized-keystore", - "name": "keystore", - }, - { - "mountPath": "/stackable/opensearch/keystore-secrets/Keystore1", - "name": "keystore-0", - "readOnly": true, - "subPath": "my-keystore-file" - } - ] - } - ], + "containers": expected_containers, + "initContainers": expected_init_containers, "securityContext": { "fsGroup": 1000 }, "serviceAccountName": "my-opensearch-cluster-serviceaccount", "terminationGracePeriodSeconds": 30, - "volumes": [ - { - "configMap": { - "defaultMode": 0o660, - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "config" - }, - { - "configMap": { - "defaultMode": 0o660, - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "log-config" - }, - { - "emptyDir": { - "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": "pod,listener-volume=listener,service=my-opensearch-cluster-seed-nodes" - } - }, - "spec": { - "accessModes": [ - "ReadWriteOnce" - ], - "resources": { - "requests": { - "storage": "1" - } - }, - "storageClassName": "secrets.stackable.tech" - } - } - }, - "name": "tls-internal" - }, - { - "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": "pod,listener-volume=listener,listener-volume=discovery-service-listener" - } - }, - "spec": { - "accessModes": [ - "ReadWriteOnce" - ], - "resources": { - "requests": { - "storage": "1" - } - }, - "storageClassName": "secrets.stackable.tech" - } - } - }, - "name": "tls-server" - }, - { - "configMap": { - "items": [ - { - "key": "action_groups.yml", - "mode": 0o660, - "path": "action_groups.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-actiongroups" - }, - { - "configMap": { - "items": [ - { - "key": "allow_list.yml", - "mode": 0o660, - "path": "allow_list.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-allowlist" - }, - { - "configMap": { - "items": [ - { - "key": "audit.yml", - "mode": 0o660, - "path": "audit.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-audit" - }, - { - "configMap": { - "items": [ - { - "key": "config.yml", - "mode": 0o660, - "path": "config.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-config" - }, - { - "configMap": { - "items": [ - { - "key": "internal_users.yml", - "mode": 0o660, - "path": "internal_users.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-internalusers" - }, - { - "configMap": { - "items": [ - { - "key": "nodes_dn.yml", - "mode": 0o660, - "path": "nodes_dn.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-nodesdn" - }, - { - "configMap": { - "items": [ - { - "key": "roles.yml", - "mode": 0o660, - "path": "roles.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-roles" - }, - { - "configMap": { - "items": [ - { - "key": "roles_mapping.yml", - "mode": 0o660, - "path": "roles_mapping.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-rolesmapping" - }, - { - "configMap": { - "items": [ - { - "key": "tenants.yml", - "mode": 0o660, - "path": "tenants.yml" - } - ], - "name": "my-opensearch-cluster-nodes-default" - }, - "name": "security-config-file-tenants" - }, - - { - "emptyDir": { - "sizeLimit": "1Mi" - }, - "name": "keystore" - }, - { - "name": "keystore-0", - "secret": { - "defaultMode": 0o660, - "items": [ - { - "key": "my-keystore-file", - "path": "my-keystore-file" - } - ], - "secretName": "my-keystore-secret" - } - } - ] + "volumes": expected_volumes, } }, "volumeClaimTemplates": [ @@ -2216,10 +3050,16 @@ mod tests { ); } - #[test] - fn test_build_cluster_manager_labels() { - let cluster_manager_labels = - RoleGroupBuilder::cluster_manager_labels(&validated_cluster(), &context_names()); + #[rstest] + #[case::security_mode_initializing(TestSecurityMode::Initializing)] + #[case::security_mode_managing(TestSecurityMode::Managing)] + #[case::security_mode_participating(TestSecurityMode::Participating)] + #[case::security_mode_disabled(TestSecurityMode::Disabled)] + fn test_build_cluster_manager_labels(#[case] security_mode: TestSecurityMode) { + let cluster_manager_labels = RoleGroupBuilder::cluster_manager_labels( + &validated_cluster(security_mode), + &context_names(), + ); assert_eq!( BTreeMap::from( @@ -2235,15 +3075,25 @@ mod tests { ); } - #[test] - fn test_build_headless_service() { - let cluster = validated_cluster(); + #[rstest] + #[case::security_mode_initializing(TestSecurityMode::Initializing)] + #[case::security_mode_managing(TestSecurityMode::Managing)] + #[case::security_mode_participating(TestSecurityMode::Participating)] + #[case::security_mode_disabled(TestSecurityMode::Disabled)] + fn test_build_headless_service(#[case] security_mode: TestSecurityMode) { + let cluster = validated_cluster(security_mode); let context_names = context_names(); let role_group_builder = role_group_builder(&cluster, &context_names); let headless_service = serde_json::to_value(role_group_builder.build_headless_service()) .expect("should be serializable"); + let expected_scheme = if security_mode == TestSecurityMode::Disabled { + json!("http") + } else { + json!("https") + }; + assert_eq!( json!({ "apiVersion": "v1", @@ -2252,7 +3102,7 @@ mod tests { "annotations": { "prometheus.io/path": "/_prometheus/metrics", "prometheus.io/port": "9200", - "prometheus.io/scheme": "https", + "prometheus.io/scheme": expected_scheme, "prometheus.io/scrape": "true" }, "labels": { @@ -2303,9 +3153,13 @@ mod tests { ); } - #[test] - fn test_build_listener() { - let cluster = validated_cluster(); + #[rstest] + #[case::security_mode_initializing(TestSecurityMode::Initializing)] + #[case::security_mode_managing(TestSecurityMode::Managing)] + #[case::security_mode_participating(TestSecurityMode::Participating)] + #[case::security_mode_disabled(TestSecurityMode::Disabled)] + fn test_build_listener(#[case] security_mode: TestSecurityMode) { + let cluster = validated_cluster(security_mode); let context_names = context_names(); let role_group_builder = role_group_builder(&cluster, &context_names); diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index b354ae6..44ba45b 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -156,55 +156,56 @@ pub mod versioned { pub struct SecurityConfig { /// User-defined action groups /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#action_groupsyml + /// see #[serde(default = "security_config_file_type_actiongroups_default")] pub action_groups: SecurityConfigFileType, /// List of allowed HTTP endpoints /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#allowlistyml + /// see #[serde(default = "security_config_file_type_allowlist_default")] pub allow_list: SecurityConfigFileType, /// Settings for audit logging /// - /// see https://docs.opensearch.org/latest/security/audit-logs/index/#settings-in-audityml + /// see + /// #[serde(default = "security_config_file_type_audit_default")] pub audit: SecurityConfigFileType, /// Configuration of the security backend /// - /// see https://docs.opensearch.org/latest/security/configuration/configuration/ + /// see #[serde(default = "security_config_file_type_config_default")] pub config: SecurityConfigFileType, /// The internal user database /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#internal_usersyml + /// see #[serde(default = "security_config_file_type_internalusers_default")] pub internal_users: SecurityConfigFileType, /// Distinguished names (DNs) of nodes to allow communication between nodes and clusters /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#nodes_dnyml + /// see #[serde(default = "security_config_file_type_nodesdn_default")] pub nodes_dn: SecurityConfigFileType, /// Definition of roles in the security plugin /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml + /// see #[serde(default = "security_config_file_type_roles_default")] pub roles: SecurityConfigFileType, /// Role mappings to users or backend roles /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml + /// see #[serde(default = "security_config_file_type_rolesmapping_default")] pub roles_mapping: SecurityConfigFileType, /// OpenSearch Dashboards tenants /// - /// see https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml + /// see #[serde(default = "security_config_file_type_tenants_default")] pub tenants: SecurityConfigFileType, } From 6e0c459c338fd8562e8be1ebb358679c1aea58ce Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 24 Feb 2026 09:37:50 +0100 Subject: [PATCH 34/53] Regenerate charts --- extra/crds.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/extra/crds.yaml b/extra/crds.yaml index 056d58e..45474d9 100644 --- a/extra/crds.yaml +++ b/extra/crds.yaml @@ -324,7 +324,7 @@ spec: description: |- User-defined action groups - see https://docs.opensearch.org/latest/security/configuration/yaml/#action_groupsyml + see properties: content: description: The content of the security configuration file @@ -414,7 +414,7 @@ spec: description: |- List of allowed HTTP endpoints - see https://docs.opensearch.org/latest/security/configuration/yaml/#allowlistyml + see properties: content: description: The content of the security configuration file @@ -504,7 +504,8 @@ spec: description: |- Settings for audit logging - see https://docs.opensearch.org/latest/security/audit-logs/index/#settings-in-audityml + see + properties: content: description: The content of the security configuration file @@ -597,7 +598,7 @@ spec: description: |- Configuration of the security backend - see https://docs.opensearch.org/latest/security/configuration/configuration/ + see properties: content: description: The content of the security configuration file @@ -685,7 +686,7 @@ spec: description: |- The internal user database - see https://docs.opensearch.org/latest/security/configuration/yaml/#internal_usersyml + see properties: content: description: The content of the security configuration file @@ -773,7 +774,7 @@ spec: description: |- Distinguished names (DNs) of nodes to allow communication between nodes and clusters - see https://docs.opensearch.org/latest/security/configuration/yaml/#nodes_dnyml + see properties: content: description: The content of the security configuration file @@ -861,7 +862,7 @@ spec: description: |- Definition of roles in the security plugin - see https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml + see properties: content: description: The content of the security configuration file @@ -949,7 +950,7 @@ spec: description: |- Role mappings to users or backend roles - see https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml + see properties: content: description: The content of the security configuration file @@ -1037,7 +1038,7 @@ spec: description: |- OpenSearch Dashboards tenants - see https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml + see properties: content: description: The content of the security configuration file From ee78b3fcd0f5e50c4fa463492f1230ab7748a607 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 25 Feb 2026 13:47:56 +0100 Subject: [PATCH 35/53] Rework RoleGroupSecurityMode --- rust/operator-binary/src/controller.rs | 44 ++- .../src/controller/build/node_config.rs | 147 +++++---- .../src/controller/build/role_builder.rs | 8 +- .../controller/build/role_group_builder.rs | 284 ++++++++++++------ .../src/controller/validate.rs | 47 +-- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 2 - 6 files changed, 334 insertions(+), 198 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 5ec66f1..657ea46 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -39,7 +39,7 @@ use crate::{ role_utils::{GenericProductSpecificCommonConfig, RoleGroupConfig}, types::{ common::Port, - kubernetes::{Hostname, ListenerClassName, NamespaceName, Uid}, + kubernetes::{Hostname, ListenerClassName, NamespaceName, SecretClassName, Uid}, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, @@ -181,11 +181,23 @@ impl ValidatedLogging { } } +/// Validated security configuration #[derive(Clone, Debug, PartialEq)] -pub struct ValidatedSecurity { - pub managing_role_group: Option, - pub settings: v1alpha1::SecurityConfig, - pub tls: v1alpha1::OpenSearchTls, +pub enum ValidatedSecurity { + /// At least one security setting is managed by the operator + ManagedByOperator { + managing_role_group: RoleGroupName, + settings: v1alpha1::SecurityConfig, + tls_server_secret_class: SecretClassName, + tls_internal_secret_class: SecretClassName, + }, + + /// All security settings are managed by the API + ManagedByApi { + settings: v1alpha1::SecurityConfig, + tls_server_secret_class: Option, + tls_internal_secret_class: SecretClassName, + }, } #[derive(Clone, Debug, PartialEq)] @@ -284,10 +296,16 @@ impl ValidatedCluster { /// Whether security is enabled and a server TLS class is defined or not. pub fn is_server_tls_enabled(&self) -> bool { - self.security - .as_ref() - .and_then(|security| security.tls.server_secret_class.as_ref()) - .is_some() + matches!( + self.security, + Some(ValidatedSecurity::ManagedByApi { + tls_server_secret_class: Some(_), + .. + }) | Some(ValidatedSecurity::ManagedByOperator { + tls_server_secret_class: _, + .. + }) + ) } } @@ -459,7 +477,7 @@ mod tests { product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ListenerClassName, NamespaceName}, + kubernetes::{ListenerClassName, NamespaceName, SecretClassName}, operator::{ClusterName, OperatorName, ProductVersion, RoleGroupName}, }, }, @@ -575,10 +593,10 @@ mod tests { ), ] .into(), - Some(ValidatedSecurity { - managing_role_group: None, + Some(ValidatedSecurity::ManagedByApi { settings: v1alpha1::SecurityConfig::default(), - tls: v1alpha1::OpenSearchTls::default(), + tls_server_secret_class: None, + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), vec![], None, diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 26aa19b..d39e099 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -8,7 +8,7 @@ use tracing::warn; use super::ValidatedCluster; use crate::{ - controller::OpenSearchRoleGroupConfig, + controller::{OpenSearchRoleGroupConfig, build::role_group_builder::RoleGroupSecurityMode}, crd::v1alpha1, framework::{ builder::pod::container::{EnvVarName, EnvVarSet}, @@ -140,6 +140,7 @@ pub struct NodeConfig { cluster: ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, + role_group_security_mode: RoleGroupSecurityMode, pub seed_nodes_service_name: ServiceName, cluster_domain_name: DomainName, headless_service_name: ServiceName, @@ -152,6 +153,7 @@ impl NodeConfig { cluster: ValidatedCluster, role_group_name: RoleGroupName, role_group_config: OpenSearchRoleGroupConfig, + role_group_security_mode: RoleGroupSecurityMode, seed_nodes_service_name: ServiceName, cluster_domain_name: DomainName, headless_service_name: ServiceName, @@ -160,6 +162,7 @@ impl NodeConfig { cluster, role_group_name, role_group_config, + role_group_security_mode, seed_nodes_service_name, cluster_domain_name, headless_service_name, @@ -217,10 +220,6 @@ impl NodeConfig { CONFIG_OPTION_DISCOVERY_TYPE.to_owned(), json!(self.discovery_type()), ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_DISABLED.to_owned(), - json!(self.cluster.security.is_none()), - ); config.insert // Accept certificates generated by the secret-operator ( @@ -239,18 +238,27 @@ impl NodeConfig { )), ); - if let Some(security) = &self.cluster.security { - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), - json!(security.settings.is_only_managed_by_api()), - ); - if !security.settings.is_only_managed_by_api() { + match self.role_group_security_mode { + RoleGroupSecurityMode::Initializing { .. } => { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX.to_owned(), + json!(true), + ); + } + RoleGroupSecurityMode::Managing { .. } + | RoleGroupSecurityMode::Participating { .. } => { config.insert( CONFIG_OPTION_PLUGINS_SECURITY_AUTHCZ_ADMIN_DN.to_owned(), json!(self.super_admin_dn()), ); } - } + RoleGroupSecurityMode::Disabled => { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_DISABLED.to_owned(), + json!(true), + ); + } + }; config } @@ -264,9 +272,13 @@ impl NodeConfig { pub fn tls_config(&self) -> serde_json::Map { let mut config = serde_json::Map::new(); - if let Some(security) = &self.cluster.security { - let opensearch_path_conf = self.opensearch_path_conf(); + let opensearch_path_conf = self.opensearch_path_conf(); + if self + .role_group_security_mode + .tls_internal_secret_class() + .is_some() + { // TLS config for TRANSPORT port which is always enabled. config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), @@ -284,31 +296,34 @@ impl NodeConfig { CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH.to_owned(), json!(format!("{opensearch_path_conf}/tls/internal/ca.crt")), ); + } - // TLS config for HTTP port (REST API) (optional). - if security.tls.server_secret_class.is_some() { - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), - json!(true), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/tls.crt")), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/tls.key")), - ); - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), - json!(format!("{opensearch_path_conf}/tls/server/ca.crt")), - ); - } else { - config.insert( - CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), - json!(false), - ); - } + if self + .role_group_security_mode + .tls_server_secret_class() + .is_some() + { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!(true), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMCERT_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/tls.crt")), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMKEY_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/tls.key")), + ); + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH.to_owned(), + json!(format!("{opensearch_path_conf}/tls/server/ca.crt")), + ); + } else { + config.insert( + CONFIG_OPTION_PLUGINS_SECURITY_SSL_HTTP_ENABLED.to_owned(), + json!(false), + ); } config @@ -536,7 +551,9 @@ mod tests { product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, types::{ - kubernetes::{ConfigMapKey, ConfigMapName, ListenerClassName, NamespaceName}, + kubernetes::{ + ConfigMapKey, ConfigMapName, ListenerClassName, NamespaceName, SecretClassName, + }, operator::{ClusterName, ProductVersion, RoleGroupName}, }, }, @@ -607,6 +624,30 @@ mod tests { product_specific_common_config: GenericProductSpecificCommonConfig::default(), }; + let security_settings = v1alpha1::SecurityConfig { + config: v1alpha1::SecurityConfigFileType { + managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, + content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( + v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { + name: ConfigMapName::from_str_unsafe("security-config"), + key: ConfigMapKey::from_str_unsafe("config.yml"), + }, + ), + ), + }, + ..v1alpha1::SecurityConfig::default() + }; + let tls_server_secret_class = SecretClassName::from_str_unsafe("tls"); + let tls_internal_secret_class = SecretClassName::from_str_unsafe("tls"); + + let validated_security = ValidatedSecurity::ManagedByOperator { + managing_role_group: role_group_name.clone(), + settings: security_settings.clone(), + tls_server_secret_class: tls_server_secret_class.clone(), + tls_internal_secret_class: tls_internal_secret_class.clone(), + }; + let cluster = ValidatedCluster::new( ResolvedProductImage { product_version: "3.4.0".to_owned(), @@ -626,32 +667,22 @@ mod tests { role_group_config.clone(), )] .into(), - Some(ValidatedSecurity { - managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), - settings: v1alpha1::SecurityConfig { - config: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( - v1alpha1::ConfigMapKeyRef { - name: ConfigMapName::from_str_unsafe("security-config"), - key: ConfigMapKey::from_str_unsafe("config.yml"), - }, - ), - ), - }, - ..v1alpha1::SecurityConfig::default() - }, - tls: v1alpha1::OpenSearchTls::default(), - }), + Some(validated_security), vec![], None, ); + let role_group_security_config = RoleGroupSecurityMode::Managing { + settings: security_settings.clone(), + tls_server_secret_class: tls_server_secret_class.clone(), + tls_internal_secret_class: tls_internal_secret_class.clone(), + }; + NodeConfig::new( cluster, role_group_name, role_group_config, + role_group_security_config, ServiceName::from_str_unsafe("my-opensearch-seed-nodes"), DomainName::from_str("cluster.local").expect("should be a valid domain name"), ServiceName::from_str_unsafe("my-opensearch-cluster-default-headless"), @@ -672,9 +703,7 @@ mod tests { "network.host: \"0.0.0.0\"\n", "node.attr.role-group: \"data\"\n", "path.logs: \"/stackable/log/opensearch\"\n", - "plugins.security.allow_default_init_securityindex: false\n", "plugins.security.authcz.admin_dn: \"CN=update-security-config.0b1e30e6-326e-4c1a-868d-ad6598b49e8b\"\n", - "plugins.security.disabled: false\n", "plugins.security.nodes_dn: [\"CN=generated certificate for pod\"]\n", "plugins.security.ssl.http.enabled: true\n", "plugins.security.ssl.http.pemcert_filepath: \"/stackable/opensearch/config/tls/server/tls.crt\"\n", diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 3759069..12ffbf0 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -349,7 +349,7 @@ mod tests { common::Port, kubernetes::{ ConfigMapName, Hostname, ListenerClassName, ListenerName, NamespaceName, - ServiceName, + SecretClassName, ServiceName, }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, @@ -427,10 +427,10 @@ mod tests { role_group_config.clone(), )] .into(), - Some(ValidatedSecurity { - managing_role_group: None, + Some(ValidatedSecurity::ManagedByApi { settings: v1alpha1::SecurityConfig::default(), - tls: v1alpha1::OpenSearchTls::default(), + tls_server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), vec![], Some(ValidatedDiscoveryEndpoint { 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 128a110..c131b86 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -68,8 +68,8 @@ use crate::{ role_group_utils::ResourceNames, types::{ kubernetes::{ - ListenerName, PersistentVolumeClaimName, ServiceAccountName, ServiceName, - VolumeName, + ListenerName, PersistentVolumeClaimName, SecretClassName, ServiceAccountName, + ServiceName, VolumeName, }, operator::RoleGroupName, }, @@ -106,26 +106,82 @@ constant!(OPENSEARCH_KEYSTORE_VOLUME_NAME: VolumeName = "keystore"); const OPENSEARCH_KEYSTORE_VOLUME_SIZE: &str = "1Mi"; /// Depending on the security settings, the role group builder operates in one of these modes. -enum RoleGroupBuilderSecurityMode<'a> { +#[derive(Clone, Debug)] +pub enum RoleGroupSecurityMode { /// The security plugin is enabled and all settings are initialized by an arbitrary role group. /// The security settings are mounted to the main container for all role groups. - Initializing(&'a ValidatedSecurity), + Initializing { + settings: v1alpha1::SecurityConfig, + tls_server_secret_class: Option, + tls_internal_secret_class: SecretClassName, + }, /// The security plugin is enabled and some or all settings are initialized and updated by this /// role group. /// The admin certificate is created in the [`v1alpha1::Container::CreateAdminCertificate`] /// init container and the security settings are mounted to and updated in the /// [`v1alpha1::Container::UpdateSecurityConfig`] side-car container. - Managing(&'a ValidatedSecurity), + Managing { + settings: v1alpha1::SecurityConfig, + tls_server_secret_class: SecretClassName, + tls_internal_secret_class: SecretClassName, + }, /// The security plugin is enabled and the settings are managed by another role group. /// The security settings are not mounted. - Participating(&'a ValidatedSecurity), + Participating { + tls_server_secret_class: SecretClassName, + tls_internal_secret_class: SecretClassName, + }, /// The security plugin is disabled. Disabled, } +impl RoleGroupSecurityMode { + /// Return the TLS server SecretClass if set + pub fn tls_server_secret_class(&self) -> Option { + if let RoleGroupSecurityMode::Initializing { + tls_server_secret_class: Some(tls_server_secret_class), + .. + } + | RoleGroupSecurityMode::Managing { + tls_server_secret_class, + .. + } + | RoleGroupSecurityMode::Participating { + tls_server_secret_class, + .. + } = self + { + Some(tls_server_secret_class.clone()) + } else { + None + } + } + + /// Return the TLS internal SecretClass if set + pub fn tls_internal_secret_class(&self) -> Option { + if let RoleGroupSecurityMode::Initializing { + tls_internal_secret_class, + .. + } + | RoleGroupSecurityMode::Managing { + tls_internal_secret_class, + .. + } + | RoleGroupSecurityMode::Participating { + tls_internal_secret_class, + .. + } = self + { + Some(tls_internal_secret_class.clone()) + } else { + None + } + } +} + /// Builder for role group resources pub struct RoleGroupBuilder<'a> { service_account_name: ServiceAccountName, @@ -136,7 +192,7 @@ pub struct RoleGroupBuilder<'a> { context_names: &'a ContextNames, resource_names: ResourceNames, discovery_service_listener_name: ListenerName, - security_mode: RoleGroupBuilderSecurityMode<'a>, + security_mode: RoleGroupSecurityMode, } impl<'a> RoleGroupBuilder<'a> { @@ -155,26 +211,35 @@ impl<'a> RoleGroupBuilder<'a> { role_group_name: role_group_name.clone(), }; - let security_mode = match &cluster.security { - Some( - security @ ValidatedSecurity { - managing_role_group: None, - .. - }, - ) => RoleGroupBuilderSecurityMode::Initializing(security), - Some( - security @ ValidatedSecurity { - managing_role_group: Some(role_group), - .. - }, - ) if role_group == &role_group_name => RoleGroupBuilderSecurityMode::Managing(security), - Some( - security @ ValidatedSecurity { - managing_role_group: Some(_), - .. - }, - ) => RoleGroupBuilderSecurityMode::Participating(security), - None => RoleGroupBuilderSecurityMode::Disabled, + let security_mode = match cluster.security.clone() { + Some(ValidatedSecurity::ManagedByApi { + settings, + tls_server_secret_class, + tls_internal_secret_class, + }) => RoleGroupSecurityMode::Initializing { + settings, + tls_server_secret_class, + tls_internal_secret_class, + }, + Some(ValidatedSecurity::ManagedByOperator { + managing_role_group, + settings, + tls_server_secret_class, + tls_internal_secret_class, + }) if managing_role_group == role_group_name => RoleGroupSecurityMode::Managing { + settings, + tls_server_secret_class, + tls_internal_secret_class, + }, + Some(ValidatedSecurity::ManagedByOperator { + tls_server_secret_class, + tls_internal_secret_class, + .. + }) => RoleGroupSecurityMode::Participating { + tls_server_secret_class, + tls_internal_secret_class, + }, + None => RoleGroupSecurityMode::Disabled, }; RoleGroupBuilder { @@ -184,6 +249,7 @@ impl<'a> RoleGroupBuilder<'a> { cluster.clone(), role_group_name.clone(), role_group_config.clone(), + security_mode.clone(), seed_nodes_service_name, context_names.cluster_domain_name.clone(), resource_names.headless_service_name(), @@ -229,11 +295,11 @@ impl<'a> RoleGroupBuilder<'a> { data.insert(VECTOR_CONFIG_FILE.to_owned(), vector_config_file_content()); } - if let RoleGroupBuilderSecurityMode::Initializing(security) - | RoleGroupBuilderSecurityMode::Managing(security) = self.security_mode + if let RoleGroupSecurityMode::Initializing { settings, .. } + | RoleGroupSecurityMode::Managing { settings, .. } = &self.security_mode { for file_type in SecurityConfigFileType::iter() { - if let Some(value) = security.settings.value(file_type) { + if let Some(value) = settings.value(file_type) { data.insert(file_type.filename(), value.to_string()); } } @@ -488,7 +554,7 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the [`v1alpha1::Container::CreateAdminCertificate`] init container for the /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] fn build_maybe_admin_certificate_init_container(&self) -> Option { - let RoleGroupBuilderSecurityMode::Managing(_) = self.security_mode else { + let RoleGroupSecurityMode::Managing { .. } = self.security_mode else { return None; }; @@ -623,42 +689,42 @@ impl<'a> RoleGroupBuilder<'a> { }); } - if let Some(security) = &self.cluster.security { + if self.security_mode.tls_internal_secret_class().is_some() { volume_mounts.push(VolumeMount { mount_path: format!("{opensearch_path_conf}/tls/internal"), name: TLS_INTERNAL_VOLUME_NAME.to_string(), ..VolumeMount::default() }); + }; - if security.tls.server_secret_class.is_some() { - volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/tls.crt"), - name: TLS_SERVER_VOLUME_NAME.to_string(), - sub_path: Some("tls.crt".to_owned()), - ..VolumeMount::default() - }); - volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/tls.key"), - name: TLS_SERVER_VOLUME_NAME.to_string(), - sub_path: Some("tls.key".to_owned()), - ..VolumeMount::default() - }); - volume_mounts.push(VolumeMount { - mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), - name: if let RoleGroupBuilderSecurityMode::Managing(_) = self.security_mode { - TLS_SERVER_CA_VOLUME_NAME.to_string() - } else { - TLS_SERVER_VOLUME_NAME.to_string() - }, - sub_path: Some("ca.crt".to_owned()), - ..VolumeMount::default() - }); - } + if self.security_mode.tls_server_secret_class().is_some() { + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/tls.crt"), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.crt".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/tls.key"), + name: TLS_SERVER_VOLUME_NAME.to_string(), + sub_path: Some("tls.key".to_owned()), + ..VolumeMount::default() + }); + volume_mounts.push(VolumeMount { + mount_path: format!("{opensearch_path_conf}/tls/server/ca.crt"), + name: if let RoleGroupSecurityMode::Managing { .. } = self.security_mode { + TLS_SERVER_CA_VOLUME_NAME.to_string() + } else { + TLS_SERVER_VOLUME_NAME.to_string() + }, + sub_path: Some("ca.crt".to_owned()), + ..VolumeMount::default() + }); + }; - if let RoleGroupBuilderSecurityMode::Initializing(_) = self.security_mode { - volume_mounts.extend(self.security_config_volume_mounts()); - } - } + if let RoleGroupSecurityMode::Initializing { .. } = self.security_mode { + volume_mounts.extend(self.security_config_volume_mounts()); + }; if !self.cluster.keystores.is_empty() { volume_mounts.push(VolumeMount { @@ -766,7 +832,7 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the [`v1alpha1::Container::UpdateSecurityConfig`] container for the /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] fn build_maybe_security_config_container(&self) -> Option { - let RoleGroupBuilderSecurityMode::Managing(security) = self.security_mode else { + let RoleGroupSecurityMode::Managing { settings, .. } = &self.security_mode else { return None; }; @@ -813,7 +879,7 @@ impl<'a> RoleGroupBuilder<'a> { ); for file_type in SecurityConfigFileType::iter() { - let managed_by_operator = security.settings.security_config(file_type).managed_by + let managed_by_operator = settings.security_config(file_type).managed_by == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; env_vars = env_vars.with_value( @@ -904,33 +970,54 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the security volumes for the [`PodTemplateSpec`] depending on the /// [`RoleGroupBuilderSecurityMode`] fn build_security_volumes(&self) -> Vec { - let volumes = match self.security_mode { - RoleGroupBuilderSecurityMode::Initializing(security) => vec![ - self.build_security_internal_tls_volumes(security), - self.build_security_server_tls_volumes(security), - self.build_security_settings_volumes(security), - ], - RoleGroupBuilderSecurityMode::Managing(security) => vec![ - self.build_security_internal_tls_volumes(security), - self.build_security_server_tls_volumes(security), - self.build_security_settings_volumes(security), + let volumes = match &self.security_mode { + RoleGroupSecurityMode::Initializing { + settings, + tls_server_secret_class, + tls_internal_secret_class, + } => vec![ + Some(self.build_security_internal_tls_volumes(tls_internal_secret_class)), + tls_server_secret_class + .as_ref() + .map(|tls_server_secret_class| { + self.build_security_server_tls_volumes(tls_server_secret_class) + }), + Some(self.build_security_settings_volumes(settings)), + ] + .into_iter() + .flatten() + .collect(), + RoleGroupSecurityMode::Managing { + settings, + tls_server_secret_class, + tls_internal_secret_class, + } => vec![ + self.build_security_internal_tls_volumes(tls_internal_secret_class), + self.build_security_server_tls_volumes(tls_server_secret_class), + self.build_security_settings_volumes(settings), self.build_security_server_tls_ca_volumes(), self.build_security_admin_cert_volumes(), ], - RoleGroupBuilderSecurityMode::Participating(security) => vec![ - self.build_security_internal_tls_volumes(security), - self.build_security_server_tls_volumes(security), + RoleGroupSecurityMode::Participating { + tls_server_secret_class, + tls_internal_secret_class, + } => vec![ + self.build_security_internal_tls_volumes(tls_internal_secret_class), + self.build_security_server_tls_volumes(tls_server_secret_class), ], - RoleGroupBuilderSecurityMode::Disabled => vec![], + RoleGroupSecurityMode::Disabled => vec![], }; volumes.into_iter().flatten().collect() } /// Builds the internal TLS volumes for the [`PodTemplateSpec`] - fn build_security_internal_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { + fn build_security_internal_tls_volumes( + &self, + tls_internal_secret_class: &SecretClassName, + ) -> Vec { let mut volume_source_builder = - SecretOperatorVolumeSourceBuilder::new(&security.tls.internal_secret_class); + SecretOperatorVolumeSourceBuilder::new(tls_internal_secret_class); volume_source_builder .with_pod_scope() @@ -960,11 +1047,10 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the server TLS volumes for the [`PodTemplateSpec`] if a TLS server secret class is /// defined - fn build_security_server_tls_volumes(&self, security: &ValidatedSecurity) -> Vec { - let Some(tls_server_secret_class) = &security.tls.server_secret_class else { - return vec![]; - }; - + fn build_security_server_tls_volumes( + &self, + tls_server_secret_class: &SecretClassName, + ) -> Vec { let mut volume_source_builder = SecretOperatorVolumeSourceBuilder::new(tls_server_secret_class); @@ -1005,12 +1091,12 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the security settings volumes for the [`PodTemplateSpec`] /// It is not checked if these volumes are required in this role group. - fn build_security_settings_volumes(&self, security: &ValidatedSecurity) -> Vec { + fn build_security_settings_volumes(&self, settings: &v1alpha1::SecurityConfig) -> Vec { let mut volumes = vec![]; for file_type in SecurityConfigFileType::iter() { let volume_name = format!("security-config-file-{}", file_type.volume_name()); - if security.settings.value(file_type).is_some() { + if settings.value(file_type).is_some() { let volume = Volume { name: volume_name, config_map: Some(ConfigMapVolumeSource { @@ -1026,7 +1112,7 @@ impl<'a> RoleGroupBuilder<'a> { }; volumes.push(volume); } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = - security.settings.config_map_key_ref(file_type) + settings.config_map_key_ref(file_type) { let volume = Volume { name: volume_name, @@ -1043,7 +1129,7 @@ impl<'a> RoleGroupBuilder<'a> { }; volumes.push(volume); } else if let Some(v1alpha1::SecretKeyRef { name, key }) = - security.settings.secret_key_ref(file_type) + settings.secret_key_ref(file_type) { let volume = Volume { name: volume_name, @@ -1119,7 +1205,7 @@ impl<'a> RoleGroupBuilder<'a> { .common_metadata(self.resource_names.headless_service_name()) .with_labels(Self::prometheus_labels()) .with_annotations(Self::prometheus_annotations( - self.cluster.is_server_tls_enabled(), + self.security_mode.tls_server_secret_class().is_some(), )) .build(); @@ -1295,7 +1381,7 @@ mod tests { types::{ kubernetes::{ ConfigMapKey, ConfigMapName, ListenerClassName, ListenerName, NamespaceName, - SecretKey, SecretName, ServiceAccountName, ServiceName, + SecretClassName, SecretKey, SecretName, ServiceAccountName, ServiceName, }, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, @@ -1432,20 +1518,22 @@ mod tests { }; let security = match security_mode { - TestSecurityMode::Initializing => Some(ValidatedSecurity { - managing_role_group: None, + TestSecurityMode::Initializing => Some(ValidatedSecurity::ManagedByApi { settings: security_settings, - tls: v1alpha1::OpenSearchTls::default(), + tls_server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), - TestSecurityMode::Managing => Some(ValidatedSecurity { - managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), + TestSecurityMode::Managing => Some(ValidatedSecurity::ManagedByOperator { + managing_role_group: RoleGroupName::from_str_unsafe("default"), settings: security_settings, - tls: v1alpha1::OpenSearchTls::default(), + tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), - TestSecurityMode::Participating => Some(ValidatedSecurity { - managing_role_group: Some(RoleGroupName::from_str_unsafe("other")), + TestSecurityMode::Participating => Some(ValidatedSecurity::ManagedByOperator { + managing_role_group: RoleGroupName::from_str_unsafe("other"), settings: security_settings, - tls: v1alpha1::OpenSearchTls::default(), + tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), TestSecurityMode::Disabled => None, }; diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 06317c9..8347a66 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -283,12 +283,18 @@ fn validate_security_config( spec: &v1alpha1::OpenSearchClusterSpec, ) -> Result> { let security = if spec.cluster_config.security.enabled { - let managing_role_group = if !spec + if spec .cluster_config .security .settings .is_only_managed_by_api() { + Some(ValidatedSecurity::ManagedByApi { + settings: spec.cluster_config.security.settings.clone(), + tls_server_secret_class: spec.cluster_config.tls.server_secret_class.clone(), + tls_internal_secret_class: spec.cluster_config.tls.internal_secret_class.clone(), + }) + } else { let managing_role_group = spec.cluster_config.security.managing_role_group.clone(); ensure!( @@ -301,21 +307,20 @@ fn validate_security_config( ); // The role group requires server TLS to communicate with the cluster. - ensure!( - spec.cluster_config.tls.server_secret_class.is_some(), - CheckSecurityConfigTlsSettingsSnafu {} - ); - - Some(managing_role_group) - } else { - None - }; - - Some(ValidatedSecurity { - managing_role_group, - settings: spec.cluster_config.security.settings.clone(), - tls: spec.cluster_config.tls.clone(), - }) + let tls_server_secret_class = spec + .cluster_config + .tls + .server_secret_class + .clone() + .context(CheckSecurityConfigTlsSettingsSnafu)?; + + Some(ValidatedSecurity::ManagedByOperator { + managing_role_group, + settings: spec.cluster_config.security.settings.clone(), + tls_server_secret_class, + tls_internal_secret_class: spec.cluster_config.tls.internal_secret_class.clone(), + }) + } } else { None }; @@ -657,8 +662,8 @@ mod tests { } )] .into(), - Some(ValidatedSecurity { - managing_role_group: Some(RoleGroupName::from_str_unsafe("default")), + Some(ValidatedSecurity::ManagedByOperator { + managing_role_group: RoleGroupName::from_str_unsafe("default"), settings: v1alpha1::SecurityConfig { config: v1alpha1::SecurityConfigFileType { managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, @@ -673,10 +678,8 @@ mod tests { }, ..v1alpha1::SecurityConfig::default() }, - tls: v1alpha1::OpenSearchTls { - server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), - internal_secret_class: SecretClassName::from_str_unsafe("tls") - }, + tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), + tls_internal_secret_class: SecretClassName::from_str_unsafe("tls") }), vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 716feb3..1ab23c9 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -965,7 +965,6 @@ data: node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" plugins.security.allow_default_init_securityindex: true - plugins.security.disabled: false plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true @@ -1016,7 +1015,6 @@ data: node.store.allow_mmap: "false" path.logs: "/stackable/log/opensearch" plugins.security.allow_default_init_securityindex: true - plugins.security.disabled: false plugins.security.nodes_dn: ["CN=generated certificate for pod"] {% if test_scenario['values']['server-use-tls'] == 'true' %} plugins.security.ssl.http.enabled: true From 7a1fbda3fd8c298f850ac630961f66fe5aa8ad2a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 25 Feb 2026 14:07:24 +0100 Subject: [PATCH 36/53] Test NodeConfig::super_admin_dn --- .../src/controller/build/node_config.rs | 18 +++++++++++++++++- 1 file changed, 17 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 d39e099..59dc786 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -279,7 +279,6 @@ impl NodeConfig { .tls_internal_secret_class() .is_some() { - // TLS config for TRANSPORT port which is always enabled. config.insert( CONFIG_OPTION_PLUGINS_SECURITY_SSL_TRANSPORT_ENABLED.to_owned(), json!(true), @@ -720,6 +719,23 @@ mod tests { ); } + #[test] + pub fn test_super_admin_dn() { + let node_config = node_config(TestConfig::default()); + + let super_admin_dn = node_config.super_admin_dn(); + let parts: Vec<&str> = super_admin_dn.split("=").collect(); + + assert_eq!( + vec![ + "CN", + "update-security-config.0b1e30e6-326e-4c1a-868d-ad6598b49e8b" + ], + parts + ); + assert!(parts[1].len() <= 64); + } + #[test] pub fn test_environment_variables() { let node_config = node_config(TestConfig { From 99bb1244c70f1ce64d6d82b377ebef3f9b700b52 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Wed, 25 Feb 2026 18:39:33 +0100 Subject: [PATCH 37/53] Remove redundant enum SecurityConfigFileType --- rust/operator-binary/src/controller.rs | 6 +- .../src/controller/build/node_config.rs | 12 +- .../src/controller/build/role_builder.rs | 2 +- .../controller/build/role_group_builder.rs | 145 +++--- .../src/controller/validate.rs | 26 +- rust/operator-binary/src/crd/mod.rs | 465 ++++++++---------- 6 files changed, 319 insertions(+), 337 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 657ea46..b3c7cab 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -187,14 +187,14 @@ pub enum ValidatedSecurity { /// At least one security setting is managed by the operator ManagedByOperator { managing_role_group: RoleGroupName, - settings: v1alpha1::SecurityConfig, + settings: v1alpha1::SecuritySettings, tls_server_secret_class: SecretClassName, tls_internal_secret_class: SecretClassName, }, /// All security settings are managed by the API ManagedByApi { - settings: v1alpha1::SecurityConfig, + settings: v1alpha1::SecuritySettings, tls_server_secret_class: Option, tls_internal_secret_class: SecretClassName, }, @@ -594,7 +594,7 @@ mod tests { ] .into(), Some(ValidatedSecurity::ManagedByApi { - settings: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecuritySettings::default(), tls_server_secret_class: None, tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 59dc786..6585b85 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -623,11 +623,11 @@ mod tests { product_specific_common_config: GenericProductSpecificCommonConfig::default(), }; - let security_settings = v1alpha1::SecurityConfig { - config: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + let security_settings = v1alpha1::SecuritySettings { + config: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Operator, + content: v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::ConfigMapKeyRef( v1alpha1::ConfigMapKeyRef { name: ConfigMapName::from_str_unsafe("security-config"), key: ConfigMapKey::from_str_unsafe("config.yml"), @@ -635,7 +635,7 @@ mod tests { ), ), }, - ..v1alpha1::SecurityConfig::default() + ..v1alpha1::SecuritySettings::default() }; let tls_server_secret_class = SecretClassName::from_str_unsafe("tls"); let tls_internal_secret_class = SecretClassName::from_str_unsafe("tls"); diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 12ffbf0..1aa9113 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -428,7 +428,7 @@ mod tests { )] .into(), Some(ValidatedSecurity::ManagedByApi { - settings: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecuritySettings::default(), tls_server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), }), 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 c131b86..96cb1a7 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -35,7 +35,6 @@ use stackable_operator::{ }, utils::COMMON_BASH_TRAP_FUNCTIONS, }; -use strum::IntoEnumIterator; use super::{ node_config::{CONFIGURATION_FILE_OPENSEARCH_YML, NodeConfig}, @@ -52,7 +51,7 @@ use crate::{ MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, }, }, - crd::{SecurityConfigFileType, v1alpha1}, + crd::{ExtendedSecuritySettingsFileType, v1alpha1}, framework::{ builder::{ meta::ownerreference_from_resource, @@ -111,7 +110,7 @@ pub enum RoleGroupSecurityMode { /// The security plugin is enabled and all settings are initialized by an arbitrary role group. /// The security settings are mounted to the main container for all role groups. Initializing { - settings: v1alpha1::SecurityConfig, + settings: v1alpha1::SecuritySettings, tls_server_secret_class: Option, tls_internal_secret_class: SecretClassName, }, @@ -122,7 +121,7 @@ pub enum RoleGroupSecurityMode { /// init container and the security settings are mounted to and updated in the /// [`v1alpha1::Container::UpdateSecurityConfig`] side-car container. Managing { - settings: v1alpha1::SecurityConfig, + settings: v1alpha1::SecuritySettings, tls_server_secret_class: SecretClassName, tls_internal_secret_class: SecretClassName, }, @@ -298,9 +297,12 @@ impl<'a> RoleGroupBuilder<'a> { if let RoleGroupSecurityMode::Initializing { settings, .. } | RoleGroupSecurityMode::Managing { settings, .. } = &self.security_mode { - for file_type in SecurityConfigFileType::iter() { - if let Some(value) = settings.value(file_type) { - data.insert(file_type.filename(), value.to_string()); + 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()); } } } @@ -722,8 +724,8 @@ impl<'a> RoleGroupBuilder<'a> { }); }; - if let RoleGroupSecurityMode::Initializing { .. } = self.security_mode { - volume_mounts.extend(self.security_config_volume_mounts()); + if let RoleGroupSecurityMode::Initializing { settings, .. } = &self.security_mode { + volume_mounts.extend(self.security_config_volume_mounts(settings)); }; if !self.cluster.keystores.is_empty() { @@ -786,21 +788,23 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the security settings volume mounts for the [`v1alpha1::Container::OpenSearch`] /// container or the [`v1alpha1::Container::UpdateSecurityConfig`] container - fn security_config_volume_mounts(&self) -> Vec { + fn security_config_volume_mounts( + &self, + settings: &v1alpha1::SecuritySettings, + ) -> Vec { let mut volume_mounts = vec![]; let opensearch_path_conf = self.node_config.opensearch_path_conf(); - for file_type in SecurityConfigFileType::iter() { - let volume_name = format!("security-config-file-{}", file_type.volume_name()); + for file_type in settings { volume_mounts.push(VolumeMount { mount_path: format!( "{opensearch_path_conf}/opensearch-security/{filename}", - filename = file_type.filename() + filename = file_type.filename.to_owned() ), - name: volume_name, + name: Self::security_settings_file_type_volume_name(&file_type).to_string(), read_only: Some(true), - sub_path: Some(file_type.filename()), + sub_path: Some(file_type.filename.to_owned()), ..VolumeMount::default() }); } @@ -808,6 +812,13 @@ impl<'a> RoleGroupBuilder<'a> { volume_mounts } + fn security_settings_file_type_volume_name( + file_type: &ExtendedSecuritySettingsFileType, + ) -> VolumeName { + VolumeName::from_str(&format!("security-config-file-{}", file_type.id)) + .expect("should be a valid VolumeName") + } + /// Builds the [`v1alpha1::Container::Vector`] container for the [`PodTemplateSpec`] if it is /// enabled fn build_maybe_vector_container(&self) -> Option { @@ -866,7 +877,7 @@ impl<'a> RoleGroupBuilder<'a> { ..VolumeMount::default() }, ]; - volume_mounts.extend(self.security_config_volume_mounts()); + volume_mounts.extend(self.security_config_volume_mounts(settings)); let mut env_vars = EnvVarSet::new() .with_value( @@ -878,15 +889,12 @@ impl<'a> RoleGroupBuilder<'a> { FieldPathEnvVar::Name, ); - for file_type in SecurityConfigFileType::iter() { - let managed_by_operator = settings.security_config(file_type).managed_by - == v1alpha1::SecurityConfigFileTypeManagedBy::Operator; + for file_type in settings { + let managed_by_operator = + *file_type.managed_by == v1alpha1::SecuritySettingsFileTypeManagedBy::Operator; env_vars = env_vars.with_value( - &EnvVarName::from_str_unsafe(&format!( - "MANAGE_{}", - file_type.volume_name().to_uppercase() - )), + &EnvVarName::from_str_unsafe(&format!("MANAGE_{}", file_type.id.to_uppercase())), managed_by_operator.to_string(), ); } @@ -1091,61 +1099,66 @@ impl<'a> RoleGroupBuilder<'a> { /// Builds the security settings volumes for the [`PodTemplateSpec`] /// It is not checked if these volumes are required in this role group. - fn build_security_settings_volumes(&self, settings: &v1alpha1::SecurityConfig) -> Vec { + fn build_security_settings_volumes( + &self, + settings: &v1alpha1::SecuritySettings, + ) -> Vec { let mut volumes = vec![]; - for file_type in SecurityConfigFileType::iter() { - let volume_name = format!("security-config-file-{}", file_type.volume_name()); - if settings.value(file_type).is_some() { - let volume = Volume { + for file_type in settings { + let volume_name = Self::security_settings_file_type_volume_name(&file_type).to_string(); + + let volume = match &file_type.content { + v1alpha1::SecuritySettingsFileTypeContent::Value(_) => Volume { name: volume_name, config_map: Some(ConfigMapVolumeSource { items: Some(vec![KeyToPath { - key: file_type.filename(), + key: file_type.filename.to_owned(), mode: Some(0o660), - path: file_type.filename(), + path: file_type.filename.to_owned(), }]), name: self.resource_names.role_group_config_map().to_string(), ..Default::default() }), ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::ConfigMapKeyRef { name, key }) = - settings.config_map_key_ref(file_type) - { - let volume = Volume { + }, + v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::ConfigMapKeyRef( + v1alpha1::ConfigMapKeyRef { name, key }, + ), + ) => Volume { name: volume_name, config_map: Some(ConfigMapVolumeSource { items: Some(vec![KeyToPath { key: key.to_string(), mode: Some(0o660), - path: file_type.filename(), + path: file_type.filename.to_owned(), }]), name: name.to_string(), ..ConfigMapVolumeSource::default() }), ..Volume::default() - }; - volumes.push(volume); - } else if let Some(v1alpha1::SecretKeyRef { name, key }) = - settings.secret_key_ref(file_type) - { - let volume = Volume { + }, + v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::SecretKeyRef( + v1alpha1::SecretKeyRef { name, key }, + ), + ) => Volume { name: volume_name, secret: Some(SecretVolumeSource { items: Some(vec![KeyToPath { key: key.to_string(), mode: Some(0o660), - path: file_type.filename(), + path: file_type.filename.to_owned(), }]), secret_name: Some(name.to_string()), ..SecretVolumeSource::default() }), ..Volume::default() - }; - volumes.push(volume); - } + }, + }; + + volumes.push(volume); } volumes @@ -1406,6 +1419,16 @@ mod tests { let _ = OPENSEARCH_KEYSTORE_VOLUME_NAME; } + #[test] + fn test_security_settings_file_type_volume_name() { + let security_settings = v1alpha1::SecuritySettings::default(); + + for file_type in &security_settings { + // Test that the function does not panic + let _ = RoleGroupBuilder::security_settings_file_type_volume_name(&file_type); + } + } + fn context_names() -> ContextNames { ContextNames { product_name: ProductName::from_str_unsafe("opensearch"), @@ -1471,11 +1494,11 @@ mod tests { product_specific_common_config: GenericProductSpecificCommonConfig::default(), }; - let security_settings = v1alpha1::SecurityConfig { - config: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, - content: v1alpha1::SecurityConfigFileTypeContent::Value( - v1alpha1::SecurityConfigFileTypeContentValue { + let security_settings = v1alpha1::SecuritySettings { + config: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Operator, + content: v1alpha1::SecuritySettingsFileTypeContent::Value( + v1alpha1::SecuritySettingsFileTypeContentValue { value: json!({ "_meta": { "type": "config", @@ -1492,10 +1515,10 @@ mod tests { }, ), }, - internal_users: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::SecretKeyRef( + internal_users: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Api, + content: v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::SecretKeyRef( v1alpha1::SecretKeyRef { name: SecretName::from_str_unsafe("opensearch-security-config"), key: SecretKey::from_str_unsafe("internal_users.yml"), @@ -1503,10 +1526,10 @@ mod tests { ), ), }, - roles: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + roles: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Api, + content: v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::ConfigMapKeyRef( v1alpha1::ConfigMapKeyRef { name: ConfigMapName::from_str_unsafe("opensearch-security-config"), key: ConfigMapKey::from_str_unsafe("roles.yml"), @@ -1514,7 +1537,7 @@ mod tests { ), ), }, - ..v1alpha1::SecurityConfig::default() + ..v1alpha1::SecuritySettings::default() }; let security = match security_mode { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 8347a66..6864a7e 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -664,11 +664,11 @@ mod tests { .into(), Some(ValidatedSecurity::ManagedByOperator { managing_role_group: RoleGroupName::from_str_unsafe("default"), - settings: v1alpha1::SecurityConfig { - config: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + settings: v1alpha1::SecuritySettings { + config: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Operator, + content: v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::ConfigMapKeyRef( v1alpha1::ConfigMapKeyRef { name: ConfigMapName::from_str_unsafe("security-config"), key: ConfigMapKey::from_str_unsafe("config.yml") @@ -676,7 +676,7 @@ mod tests { ) ) }, - ..v1alpha1::SecurityConfig::default() + ..v1alpha1::SecuritySettings::default() }, tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls") @@ -901,7 +901,7 @@ mod tests { .security .settings .config - .managed_by = v1alpha1::SecurityConfigFileTypeManagedBy::Operator; + .managed_by = v1alpha1::SecuritySettingsFileTypeManagedBy::Operator; cluster.spec.cluster_config.tls.server_secret_class = None; }, ErrorDiscriminants::CheckSecurityConfigTlsSettings, @@ -954,11 +954,11 @@ mod tests { security: v1alpha1::Security { enabled: true, managing_role_group: RoleGroupName::from_str_unsafe("default"), - settings: v1alpha1::SecurityConfig { - config: v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Operator, - content: v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( + settings: v1alpha1::SecuritySettings { + config: v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Operator, + content: v1alpha1::SecuritySettingsFileTypeContent::ValueFrom( + v1alpha1::SecuritySettingsFileTypeContentValueFrom::ConfigMapKeyRef( v1alpha1::ConfigMapKeyRef { name: ConfigMapName::from_str_unsafe("security-config"), key: ConfigMapKey::from_str_unsafe("config.yml") @@ -966,7 +966,7 @@ mod tests { ) ), }, - ..v1alpha1::SecurityConfig::default() + ..v1alpha1::SecuritySettings::default() }, }, vector_aggregator_config_map_name: Some(ConfigMapName::from_str_unsafe( diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 44ba45b..0ec235b 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,4 +1,4 @@ -use std::{slice, str::FromStr}; +use std::{array, slice, str::FromStr}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -27,7 +27,7 @@ use stackable_operator::{ utils::crds::raw_object_schema, versioned::versioned, }; -use strum::{Display, EnumIter, IntoEnumIterator}; +use strum::{Display, EnumIter}; use crate::{ attributed_string_type, constant, @@ -132,6 +132,7 @@ pub mod versioned { pub secret_key_ref: SecretKeyRef, } + /// Configuration of the OpenSearch security plugin #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Security { @@ -148,83 +149,86 @@ pub mod versioned { /// Settings for the OpenSearch security plugin #[serde(default)] - pub settings: SecurityConfig, + pub settings: SecuritySettings, } + /// Configuration files of the OpenSearch security plugin #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub struct SecurityConfig { + pub struct SecuritySettings { /// User-defined action groups /// /// see - #[serde(default = "security_config_file_type_actiongroups_default")] - pub action_groups: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_actiongroups")] + pub action_groups: SecuritySettingsFileType, /// List of allowed HTTP endpoints /// /// see - #[serde(default = "security_config_file_type_allowlist_default")] - pub allow_list: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_allowlist")] + pub allow_list: SecuritySettingsFileType, /// Settings for audit logging /// /// see /// - #[serde(default = "security_config_file_type_audit_default")] - pub audit: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_audit")] + pub audit: SecuritySettingsFileType, /// Configuration of the security backend /// /// see - #[serde(default = "security_config_file_type_config_default")] - pub config: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_config")] + pub config: SecuritySettingsFileType, /// The internal user database /// /// see - #[serde(default = "security_config_file_type_internalusers_default")] - pub internal_users: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_internalusers")] + pub internal_users: SecuritySettingsFileType, /// Distinguished names (DNs) of nodes to allow communication between nodes and clusters /// /// see - #[serde(default = "security_config_file_type_nodesdn_default")] - pub nodes_dn: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_nodesdn")] + pub nodes_dn: SecuritySettingsFileType, /// Definition of roles in the security plugin /// /// see - #[serde(default = "security_config_file_type_roles_default")] - pub roles: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_roles")] + pub roles: SecuritySettingsFileType, /// Role mappings to users or backend roles /// /// see - #[serde(default = "security_config_file_type_rolesmapping_default")] - pub roles_mapping: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_rolesmapping")] + pub roles_mapping: SecuritySettingsFileType, /// OpenSearch Dashboards tenants /// /// see - #[serde(default = "security_config_file_type_tenants_default")] - pub tenants: SecurityConfigFileType, + #[serde(default = "security_settings_file_type_default_tenants")] + pub tenants: SecuritySettingsFileType, } + /// Specific configuration file of the OpenSearch security plugin #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub struct SecurityConfigFileType { + pub struct SecuritySettingsFileType { /// Whether this configuration should only be applied initially and afterwards be managed /// via the "API", or managed all the time by the "operator". /// /// If this configuration is changed later from "API" to "operator", then the changes made /// via the API are overridden. - // No default, so that the user is aware of! - pub managed_by: SecurityConfigFileTypeManagedBy, + // There is no default, so that the user is aware of this choice. + pub managed_by: SecuritySettingsFileTypeManagedBy, /// The content of the security configuration file - pub content: SecurityConfigFileTypeContent, + pub content: SecuritySettingsFileTypeContent, } + /// Responsibility for initializing and updating the security configuration #[derive( Clone, Debug, @@ -238,7 +242,7 @@ pub mod versioned { PartialOrd, Serialize, )] - pub enum SecurityConfigFileTypeManagedBy { + pub enum SecuritySettingsFileTypeManagedBy { /// Only initially applied by the operator, but afterwards managed via the API. #[serde(rename = "API")] Api, @@ -248,26 +252,29 @@ pub mod versioned { Operator, } + /// Content of the security configuration file #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub enum SecurityConfigFileTypeContent { + pub enum SecuritySettingsFileTypeContent { /// Security configuration file content defined inline - Value(SecurityConfigFileTypeContentValue), + Value(SecuritySettingsFileTypeContentValue), /// Security configuration file content ingested from a ConfigMap or Secret - ValueFrom(SecurityConfigFileTypeContentValueFrom), + ValueFrom(SecuritySettingsFileTypeContentValueFrom), } + /// Security configuration file content defined inline #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] - pub struct SecurityConfigFileTypeContentValue { + pub struct SecuritySettingsFileTypeContentValue { #[serde(flatten)] #[schemars(schema_with = "raw_object_schema")] value: serde_json::Value, } + /// Security configuration file content ingested from a ConfigMap or Secret #[derive(Clone, Debug, Deserialize, Display, Eq, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] - pub enum SecurityConfigFileTypeContentValueFrom { + pub enum SecuritySettingsFileTypeContentValueFrom { /// Reference to a key in a ConfigMap ConfigMapKeyRef(ConfigMapKeyRef), @@ -275,6 +282,7 @@ pub mod versioned { SecretKeyRef(SecretKeyRef), } + /// Reference to a key in a ConfigMap #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] pub struct ConfigMapKeyRef { /// Name of the ConfigMap @@ -284,6 +292,7 @@ pub mod versioned { pub key: ConfigMapKey, } + /// Reference to a key in a Secret #[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] pub struct SecretKeyRef { /// Name of the Secret @@ -564,217 +573,109 @@ impl Default for v1alpha1::Security { Self { enabled: security_config_enabled_default(), managing_role_group: security_config_managing_role_group(), - settings: v1alpha1::SecurityConfig::default(), + settings: v1alpha1::SecuritySettings::default(), } } } -impl Default for v1alpha1::SecurityConfig { +impl v1alpha1::SecuritySettings { + pub fn is_only_managed_by_api(&self) -> bool { + self.into_iter() + .all(|config| *config.managed_by == v1alpha1::SecuritySettingsFileTypeManagedBy::Api) + } +} + +impl Default for v1alpha1::SecuritySettings { fn default() -> Self { Self { - action_groups: security_config_file_type_actiongroups_default(), - allow_list: security_config_file_type_allowlist_default(), - audit: security_config_file_type_audit_default(), - config: security_config_file_type_config_default(), - internal_users: security_config_file_type_internalusers_default(), - nodes_dn: security_config_file_type_nodesdn_default(), - roles_mapping: security_config_file_type_rolesmapping_default(), - roles: security_config_file_type_roles_default(), - tenants: security_config_file_type_tenants_default(), + action_groups: security_settings_file_type_default_actiongroups(), + allow_list: security_settings_file_type_default_allowlist(), + audit: security_settings_file_type_default_audit(), + config: security_settings_file_type_default_config(), + internal_users: security_settings_file_type_default_internalusers(), + nodes_dn: security_settings_file_type_default_nodesdn(), + roles_mapping: security_settings_file_type_default_rolesmapping(), + roles: security_settings_file_type_default_roles(), + tenants: security_settings_file_type_default_tenants(), } } } -#[derive(Clone, Copy, EnumIter)] -pub enum SecurityConfigFileType { - ActionGroups, - AllowList, - Audit, - Config, - InternalUsers, - NodesDn, - Roles, - RolesMapping, - Tenants, -} +/// [`v1alpha1::SecuritySettingsFileType`] extended with ID and filename +pub struct ExtendedSecuritySettingsFileType<'a> { + /// The ID of the file type as set in the `_meta.type` field; can be used to construct + /// volume names + pub id: &'static str, -impl SecurityConfigFileType { - pub fn filename(&self) -> String { - match self { - SecurityConfigFileType::ActionGroups => "action_groups.yml".to_owned(), - SecurityConfigFileType::AllowList => "allow_list.yml".to_owned(), - SecurityConfigFileType::Audit => "audit.yml".to_owned(), - SecurityConfigFileType::Config => "config.yml".to_owned(), - SecurityConfigFileType::InternalUsers => "internal_users.yml".to_owned(), - SecurityConfigFileType::NodesDn => "nodes_dn.yml".to_owned(), - SecurityConfigFileType::Roles => "roles.yml".to_owned(), - SecurityConfigFileType::RolesMapping => "roles_mapping.yml".to_owned(), - SecurityConfigFileType::Tenants => "tenants.yml".to_owned(), - } - } + /// The file name as expected by the OpenSearch security plugin + pub filename: &'static str, - pub fn volume_name(&self) -> String { - match self { - SecurityConfigFileType::ActionGroups => "actiongroups".to_owned(), - SecurityConfigFileType::AllowList => "allowlist".to_owned(), - SecurityConfigFileType::Audit => "audit".to_owned(), - SecurityConfigFileType::Config => "config".to_owned(), - SecurityConfigFileType::InternalUsers => "internalusers".to_owned(), - SecurityConfigFileType::NodesDn => "nodesdn".to_owned(), - SecurityConfigFileType::Roles => "roles".to_owned(), - SecurityConfigFileType::RolesMapping => "rolesmapping".to_owned(), - SecurityConfigFileType::Tenants => "tenants".to_owned(), - } - } + pub managed_by: &'a v1alpha1::SecuritySettingsFileTypeManagedBy, - fn default_content(&self) -> serde_json::Value { - match self { - SecurityConfigFileType::ActionGroups => json!({ - "_meta": { - "type": "actiongroups", - "config_version": 2 - } - }), - SecurityConfigFileType::AllowList => json!({ - "_meta": { - "type": "allowlist", - "config_version": 2 - }, - "config": { - "enabled": false - } - }), - SecurityConfigFileType::Audit => json!({ - "_meta": { - "type": "audit", - "config_version": 2 - }, - "config": { - "enabled": false - } - }), - SecurityConfigFileType::Config => json!({ - "_meta": { - "type": "config", - "config_version": 2 - }, - "config": { - "dynamic": { - "http": {}, - "authc": {}, - "authz": {} - } - } - }), - SecurityConfigFileType::InternalUsers => json!({ - "_meta": { - "type": "internalusers", - "config_version": 2 - } - }), - SecurityConfigFileType::NodesDn => json!({ - "_meta": { - "type": "nodesdn", - "config_version": 2 - } - }), - SecurityConfigFileType::Roles => json!({ - "_meta": { - "type": "roles", - "config_version": 2 - } - }), - SecurityConfigFileType::RolesMapping => json!({ - "_meta": { - "type": "rolesmapping", - "config_version": 2 - } - }), - SecurityConfigFileType::Tenants => json!({ - "_meta": { - "type": "tenants", - "config_version": 2 - } - }), - } - } + pub content: &'a v1alpha1::SecuritySettingsFileTypeContent, } -impl v1alpha1::SecurityConfig { - pub fn security_config( - &self, - file_type: SecurityConfigFileType, - ) -> &v1alpha1::SecurityConfigFileType { - match file_type { - SecurityConfigFileType::ActionGroups => &self.action_groups, - SecurityConfigFileType::AllowList => &self.allow_list, - SecurityConfigFileType::Audit => &self.audit, - SecurityConfigFileType::Config => &self.config, - SecurityConfigFileType::InternalUsers => &self.internal_users, - SecurityConfigFileType::NodesDn => &self.nodes_dn, - SecurityConfigFileType::Roles => &self.roles, - SecurityConfigFileType::RolesMapping => &self.roles_mapping, - SecurityConfigFileType::Tenants => &self.tenants, - } - } - - pub fn is_only_managed_by_api(&self) -> bool { - SecurityConfigFileType::iter() - .map(|file_type| self.security_config(file_type)) - .all(|config| config.managed_by == v1alpha1::SecurityConfigFileTypeManagedBy::Api) - } - - pub fn value(&self, file_type: SecurityConfigFileType) -> Option { - if let v1alpha1::SecurityConfigFileType { - content: - v1alpha1::SecurityConfigFileTypeContent::Value( - v1alpha1::SecurityConfigFileTypeContentValue { value }, - ), - .. - } = self.security_config(file_type) - { - Some(value.to_string()) - } else { - None - } - } - - pub fn config_map_key_ref( - &self, - file_type: SecurityConfigFileType, - ) -> Option<&v1alpha1::ConfigMapKeyRef> { - if let v1alpha1::SecurityConfigFileType { - content: - v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::ConfigMapKeyRef( - config_map_key_ref, - ), - ), - .. - } = self.security_config(file_type) - { - Some(config_map_key_ref) - } else { - None - } - } - - pub fn secret_key_ref( - &self, - file_type: SecurityConfigFileType, - ) -> Option<&v1alpha1::SecretKeyRef> { - if let v1alpha1::SecurityConfigFileType { - content: - v1alpha1::SecurityConfigFileTypeContent::ValueFrom( - v1alpha1::SecurityConfigFileTypeContentValueFrom::SecretKeyRef(secret_key_ref), - ), - .. - } = self.security_config(file_type) - { - Some(secret_key_ref) - } else { - None - } +impl<'a> IntoIterator for &'a v1alpha1::SecuritySettings { + type IntoIter = array::IntoIter; + type Item = ExtendedSecuritySettingsFileType<'a>; + + fn into_iter(self) -> Self::IntoIter { + IntoIterator::into_iter([ + ExtendedSecuritySettingsFileType { + id: "actiongroups", + filename: "action_groups.yml", + managed_by: &self.action_groups.managed_by, + content: &self.action_groups.content, + }, + ExtendedSecuritySettingsFileType { + id: "allowlist", + filename: "allow_list.yml", + managed_by: &self.allow_list.managed_by, + content: &self.allow_list.content, + }, + ExtendedSecuritySettingsFileType { + id: "audit", + filename: "audit.yml", + managed_by: &self.audit.managed_by, + content: &self.audit.content, + }, + ExtendedSecuritySettingsFileType { + id: "config", + filename: "config.yml", + managed_by: &self.config.managed_by, + content: &self.config.content, + }, + ExtendedSecuritySettingsFileType { + id: "internalusers", + filename: "internal_users.yml", + managed_by: &self.internal_users.managed_by, + content: &self.internal_users.content, + }, + ExtendedSecuritySettingsFileType { + id: "nodesdn", + filename: "nodes_dn.yml", + managed_by: &self.nodes_dn.managed_by, + content: &self.nodes_dn.content, + }, + ExtendedSecuritySettingsFileType { + id: "roles", + filename: "roles.yml", + managed_by: &self.roles.managed_by, + content: &self.roles.content, + }, + ExtendedSecuritySettingsFileType { + id: "rolesmapping", + filename: "roles_mapping.yml", + managed_by: &self.roles_mapping.managed_by, + content: &self.roles_mapping.content, + }, + ExtendedSecuritySettingsFileType { + id: "tenants", + filename: "tenants.yml", + managed_by: &self.tenants.managed_by, + content: &self.tenants.content, + }, + ]) } } @@ -786,49 +687,107 @@ fn security_config_managing_role_group() -> RoleGroupName { RoleGroupName::from_str("security-config").expect("should be a valid role group name") } -fn security_config_file_type_actiongroups_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::ActionGroups) +fn security_settings_file_type_default_actiongroups() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "actiongroups", + "config_version": 2 + } + })) } -fn security_config_file_type_allowlist_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::AllowList) +fn security_settings_file_type_default_allowlist() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "allowlist", + "config_version": 2 + }, + "config": { + "enabled": false + } + })) } -fn security_config_file_type_audit_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::Audit) +fn security_settings_file_type_default_audit() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "audit", + "config_version": 2 + }, + "config": { + "enabled": false + } + })) } -fn security_config_file_type_config_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::Config) +fn security_settings_file_type_default_config() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "config", + "config_version": 2 + }, + "config": { + "dynamic": { + "http": {}, + "authc": {}, + "authz": {} + } + } + })) } -fn security_config_file_type_internalusers_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::InternalUsers) +fn security_settings_file_type_default_internalusers() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "internalusers", + "config_version": 2 + } + })) } -fn security_config_file_type_nodesdn_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::NodesDn) +fn security_settings_file_type_default_nodesdn() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "nodesdn", + "config_version": 2 + } + })) } -fn security_config_file_type_roles_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::Roles) +fn security_settings_file_type_default_roles() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "roles", + "config_version": 2 + } + })) } -fn security_config_file_type_rolesmapping_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::RolesMapping) +fn security_settings_file_type_default_rolesmapping() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "rolesmapping", + "config_version": 2 + } + })) } -fn security_config_file_type_tenants_default() -> v1alpha1::SecurityConfigFileType { - crd_default(SecurityConfigFileType::Tenants) +fn security_settings_file_type_default_tenants() -> v1alpha1::SecuritySettingsFileType { + security_settings_file_type_default(json!({ + "_meta": { + "type": "tenants", + "config_version": 2 + } + })) } -fn crd_default(file_type: SecurityConfigFileType) -> v1alpha1::SecurityConfigFileType { - v1alpha1::SecurityConfigFileType { - managed_by: v1alpha1::SecurityConfigFileTypeManagedBy::Api, - content: v1alpha1::SecurityConfigFileTypeContent::Value( - v1alpha1::SecurityConfigFileTypeContentValue { - value: file_type.default_content(), - }, +fn security_settings_file_type_default( + value: serde_json::Value, +) -> v1alpha1::SecuritySettingsFileType { + v1alpha1::SecuritySettingsFileType { + managed_by: v1alpha1::SecuritySettingsFileTypeManagedBy::Api, + content: v1alpha1::SecuritySettingsFileTypeContent::Value( + v1alpha1::SecuritySettingsFileTypeContentValue { value }, ), } } From fe86178a63c74f1a1933da6d2a0f6b984883e253 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 08:37:33 +0100 Subject: [PATCH 38/53] Fix comments --- .../src/controller/build/role_group_builder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 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 96cb1a7..a1f23dc 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -554,7 +554,7 @@ impl<'a> RoleGroupBuilder<'a> { } /// Builds the [`v1alpha1::Container::CreateAdminCertificate`] init container for the - /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] + /// [`PodTemplateSpec`] if the security mode is [`RoleGroupSecurityMode::Managing`] fn build_maybe_admin_certificate_init_container(&self) -> Option { let RoleGroupSecurityMode::Managing { .. } = self.security_mode else { return None; @@ -841,7 +841,7 @@ impl<'a> RoleGroupBuilder<'a> { } /// Builds the [`v1alpha1::Container::UpdateSecurityConfig`] container for the - /// [`PodTemplateSpec`] if the security mode is [`RoleGroupBuilderSecurityMode::Managing`] + /// [`PodTemplateSpec`] if the security mode is [`RoleGroupSecurityMode::Managing`] fn build_maybe_security_config_container(&self) -> Option { let RoleGroupSecurityMode::Managing { settings, .. } = &self.security_mode else { return None; @@ -976,7 +976,7 @@ impl<'a> RoleGroupBuilder<'a> { } /// Builds the security volumes for the [`PodTemplateSpec`] depending on the - /// [`RoleGroupBuilderSecurityMode`] + /// [`RoleGroupSecurityMode`] fn build_security_volumes(&self) -> Vec { let volumes = match &self.security_mode { RoleGroupSecurityMode::Initializing { From e94a82d97d6fcb0c9a317461d5b8adcc3d027a72 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 08:52:42 +0100 Subject: [PATCH 39/53] Test RoleGroupBuilder::security_settings_file_type_managed_by_env_var --- .../controller/build/role_group_builder.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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 a1f23dc..d28cd13 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -894,7 +894,7 @@ impl<'a> RoleGroupBuilder<'a> { *file_type.managed_by == v1alpha1::SecuritySettingsFileTypeManagedBy::Operator; env_vars = env_vars.with_value( - &EnvVarName::from_str_unsafe(&format!("MANAGE_{}", file_type.id.to_uppercase())), + &Self::security_settings_file_type_managed_by_env_var(&file_type), managed_by_operator.to_string(), ); } @@ -928,6 +928,14 @@ impl<'a> RoleGroupBuilder<'a> { Some(container) } + /// Environment variable which is used in the `update-security-config.sh` script to determine + /// if a security settings file type is managed by the operator + fn security_settings_file_type_managed_by_env_var( + file_type: &ExtendedSecuritySettingsFileType, + ) -> EnvVarName { + EnvVarName::from_str_unsafe(&format!("MANAGE_{}", file_type.id.to_uppercase())) + } + /// Builds the config volumes for the [`PodTemplateSpec`] fn build_config_volumes(&self) -> Vec { vec![Volume { @@ -1429,6 +1437,16 @@ mod tests { } } + #[test] + fn test_security_settings_file_type_managed_by_env_var() { + let security_settings = v1alpha1::SecuritySettings::default(); + + for file_type in &security_settings { + // Test that the function does not panic + let _ = RoleGroupBuilder::security_settings_file_type_managed_by_env_var(&file_type); + } + } + fn context_names() -> ContextNames { ContextNames { product_name: ProductName::from_str_unsafe("opensearch"), From 4b67f0e4e333ed56964480937bc91ed36717488d Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 09:53:31 +0100 Subject: [PATCH 40/53] Add ValidatedSecurity::Disabled --- rust/operator-binary/src/controller.rs | 17 ++++++----- rust/operator-binary/src/controller/build.rs | 4 +-- .../src/controller/build/node_config.rs | 2 +- .../src/controller/build/role_builder.rs | 4 +-- .../controller/build/role_group_builder.rs | 28 +++++++++---------- .../src/controller/validate.rs | 18 ++++++------ 6 files changed, 37 insertions(+), 36 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index b3c7cab..0240767 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -198,6 +198,9 @@ pub enum ValidatedSecurity { tls_server_secret_class: Option, tls_internal_secret_class: SecretClassName, }, + + /// The OpenSearch security plugin is disabled. + Disabled, } #[derive(Clone, Debug, PartialEq)] @@ -224,7 +227,7 @@ pub struct ValidatedCluster { pub uid: Uid, pub role_config: v1alpha1::OpenSearchRoleConfig, pub role_group_configs: BTreeMap, - pub security: Option, + pub security: ValidatedSecurity, pub keystores: Vec, pub discovery_endpoint: Option, } @@ -239,7 +242,7 @@ impl ValidatedCluster { uid: impl Into, role_config: v1alpha1::OpenSearchRoleConfig, role_group_configs: BTreeMap, - security: Option, + security: ValidatedSecurity, keystores: Vec, discovery_endpoint: Option, ) -> Self { @@ -298,13 +301,13 @@ impl ValidatedCluster { pub fn is_server_tls_enabled(&self) -> bool { matches!( self.security, - Some(ValidatedSecurity::ManagedByApi { + ValidatedSecurity::ManagedByApi { tls_server_secret_class: Some(_), .. - }) | Some(ValidatedSecurity::ManagedByOperator { + } | ValidatedSecurity::ManagedByOperator { tls_server_secret_class: _, .. - }) + } ) } } @@ -593,11 +596,11 @@ mod tests { ), ] .into(), - Some(ValidatedSecurity::ManagedByApi { + ValidatedSecurity::ManagedByApi { settings: v1alpha1::SecuritySettings::default(), tls_server_secret_class: None, tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), - }), + }, vec![], None, ) diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 72826c6..23ece0a 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -82,7 +82,7 @@ mod tests { controller::{ ContextNames, OpenSearchNodeResources, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedDiscoveryEndpoint, ValidatedLogging, - ValidatedOpenSearchConfig, + ValidatedOpenSearchConfig, ValidatedSecurity, }, crd::{NodeRoles, v1alpha1}, framework::{ @@ -209,7 +209,7 @@ mod tests { ), ] .into(), - None, + ValidatedSecurity::Disabled, vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 6585b85..95c2a00 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -666,7 +666,7 @@ mod tests { role_group_config.clone(), )] .into(), - Some(validated_security), + validated_security, vec![], None, ); diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index 1aa9113..f9dac5a 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -427,11 +427,11 @@ mod tests { role_group_config.clone(), )] .into(), - Some(ValidatedSecurity::ManagedByApi { + ValidatedSecurity::ManagedByApi { settings: v1alpha1::SecuritySettings::default(), tls_server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), - }), + }, vec![], Some(ValidatedDiscoveryEndpoint { hostname: Hostname::from_str_unsafe("1.2.3.4"), 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 d28cd13..08534a6 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -211,34 +211,34 @@ impl<'a> RoleGroupBuilder<'a> { }; let security_mode = match cluster.security.clone() { - Some(ValidatedSecurity::ManagedByApi { + ValidatedSecurity::ManagedByApi { settings, tls_server_secret_class, tls_internal_secret_class, - }) => RoleGroupSecurityMode::Initializing { + } => RoleGroupSecurityMode::Initializing { settings, tls_server_secret_class, tls_internal_secret_class, }, - Some(ValidatedSecurity::ManagedByOperator { + ValidatedSecurity::ManagedByOperator { managing_role_group, settings, tls_server_secret_class, tls_internal_secret_class, - }) if managing_role_group == role_group_name => RoleGroupSecurityMode::Managing { + } if managing_role_group == role_group_name => RoleGroupSecurityMode::Managing { settings, tls_server_secret_class, tls_internal_secret_class, }, - Some(ValidatedSecurity::ManagedByOperator { + ValidatedSecurity::ManagedByOperator { tls_server_secret_class, tls_internal_secret_class, .. - }) => RoleGroupSecurityMode::Participating { + } => RoleGroupSecurityMode::Participating { tls_server_secret_class, tls_internal_secret_class, }, - None => RoleGroupSecurityMode::Disabled, + ValidatedSecurity::Disabled => RoleGroupSecurityMode::Disabled, }; RoleGroupBuilder { @@ -1559,24 +1559,24 @@ mod tests { }; let security = match security_mode { - TestSecurityMode::Initializing => Some(ValidatedSecurity::ManagedByApi { + TestSecurityMode::Initializing => ValidatedSecurity::ManagedByApi { settings: security_settings, tls_server_secret_class: Some(SecretClassName::from_str_unsafe("tls")), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), - }), - TestSecurityMode::Managing => Some(ValidatedSecurity::ManagedByOperator { + }, + TestSecurityMode::Managing => ValidatedSecurity::ManagedByOperator { managing_role_group: RoleGroupName::from_str_unsafe("default"), settings: security_settings, tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), - }), - TestSecurityMode::Participating => Some(ValidatedSecurity::ManagedByOperator { + }, + TestSecurityMode::Participating => ValidatedSecurity::ManagedByOperator { managing_role_group: RoleGroupName::from_str_unsafe("other"), settings: security_settings, tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls"), - }), - TestSecurityMode::Disabled => None, + }, + TestSecurityMode::Disabled => ValidatedSecurity::Disabled, }; ValidatedCluster::new( diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 6864a7e..89c9f40 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -279,9 +279,7 @@ fn validate_logging_configuration( }) } -fn validate_security_config( - spec: &v1alpha1::OpenSearchClusterSpec, -) -> Result> { +fn validate_security_config(spec: &v1alpha1::OpenSearchClusterSpec) -> Result { let security = if spec.cluster_config.security.enabled { if spec .cluster_config @@ -289,11 +287,11 @@ fn validate_security_config( .settings .is_only_managed_by_api() { - Some(ValidatedSecurity::ManagedByApi { + ValidatedSecurity::ManagedByApi { settings: spec.cluster_config.security.settings.clone(), tls_server_secret_class: spec.cluster_config.tls.server_secret_class.clone(), tls_internal_secret_class: spec.cluster_config.tls.internal_secret_class.clone(), - }) + } } else { let managing_role_group = spec.cluster_config.security.managing_role_group.clone(); @@ -314,15 +312,15 @@ fn validate_security_config( .clone() .context(CheckSecurityConfigTlsSettingsSnafu)?; - Some(ValidatedSecurity::ManagedByOperator { + ValidatedSecurity::ManagedByOperator { managing_role_group, settings: spec.cluster_config.security.settings.clone(), tls_server_secret_class, tls_internal_secret_class: spec.cluster_config.tls.internal_secret_class.clone(), - }) + } } } else { - None + ValidatedSecurity::Disabled }; Ok(security) @@ -662,7 +660,7 @@ mod tests { } )] .into(), - Some(ValidatedSecurity::ManagedByOperator { + ValidatedSecurity::ManagedByOperator { managing_role_group: RoleGroupName::from_str_unsafe("default"), settings: v1alpha1::SecuritySettings { config: v1alpha1::SecuritySettingsFileType { @@ -680,7 +678,7 @@ mod tests { }, tls_server_secret_class: SecretClassName::from_str_unsafe("tls"), tls_internal_secret_class: SecretClassName::from_str_unsafe("tls") - }), + }, vec![v1alpha1::OpenSearchKeystore { key: OpenSearchKeystoreKey::from_str_unsafe("Keystore1"), secret_key_ref: v1alpha1::SecretKeyRef { From c8d4e81277891e99c91fcba5dec3dd52c173b1c7 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 12:10:24 +0100 Subject: [PATCH 41/53] Test the preprocess step --- rust/operator-binary/src/controller.rs | 38 +++--- .../src/controller/preprocess.rs | 109 +++++++++++++++--- 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 0240767..28b9af3 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -123,9 +123,6 @@ pub enum Error { #[snafu(display("failed to dereference resources"))] Dereference { source: dereference::Error }, - #[snafu(display("failed to preprocess cluster"))] - Preprocess { source: preprocess::Error }, - #[snafu(display("failed to validate cluster"))] ValidateCluster { source: validate::Error }, @@ -396,10 +393,14 @@ pub fn error_policy( /// Reconcile function of the OpenSearchCluster controller /// /// The reconcile function performs the following steps: -/// 1. Validate the given cluster specification and return a [`ValidatedCluster`] if successful. -/// 2. Build Kubernetes resource specifications from the validated cluster. -/// 3. Apply the Kubernetes resource specifications -/// 4. Update the cluster status +/// 1. Dereference objects which are referenced in the OpenSearchCluster. +/// 2. Preprocess the OpenSearchCluster specification and add configurations that the user is +/// allowed to leave out. +/// 3. Validate the preprocessed cluster specification and the dereferenced objects and return a +/// [`ValidatedCluster`] if successful. +/// 4. Build Kubernetes resource specifications from the validated cluster. +/// 5. Apply the Kubernetes resource specifications +/// 6. Update the cluster status pub async fn reconcile( object: Arc>, context: Arc, @@ -410,15 +411,16 @@ pub async fn reconcile( .0 .as_ref() .map_err(stackable_operator::kube::core::error_boundary::InvalidObject::clone) - .context(DeserializeClusterDefinitionSnafu)?; + .context(DeserializeClusterDefinitionSnafu)? + .clone(); // dereference (client required) - let dereferenced_objects = dereference(&context.client, cluster) + let dereferenced_objects = dereference(&context.client, &cluster) .await .context(DereferenceSnafu)?; // preprocess (no client required) - let preprocessed_cluster = preprocess(cluster.clone()).context(PreprocessSnafu)?; + let preprocessed_cluster = preprocess(cluster); // validate (no client required) let validated_cluster = validate(&context.names, &preprocessed_cluster, &dereferenced_objects) @@ -428,7 +430,8 @@ pub async fn reconcile( let prepared_resources = build(&context.names, validated_cluster.clone()); // apply (client required) - let apply_strategy = ClusterResourceApplyStrategy::from(&cluster.spec.cluster_operation); + let apply_strategy = + ClusterResourceApplyStrategy::from(&preprocessed_cluster.spec.cluster_operation); let applied_resources = Applier::new( &context.client, &context.names, @@ -436,7 +439,7 @@ pub async fn reconcile( &validated_cluster.namespace, &validated_cluster.uid, apply_strategy, - &cluster.spec.object_overrides, + &preprocessed_cluster.spec.object_overrides, ) .apply(prepared_resources) .await @@ -445,9 +448,14 @@ pub async fn reconcile( // not necessary in this controller: create discovery ConfigMap based on the applied resources (client required) // update status (client required) - update_status(&context.client, &context.names, cluster, applied_resources) - .await - .context(UpdateStatusSnafu)?; + update_status( + &context.client, + &context.names, + &preprocessed_cluster, + applied_resources, + ) + .await + .context(UpdateStatusSnafu)?; Ok(Action::await_change()) } diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs index 95d360f..654c2f1 100644 --- a/rust/operator-binary/src/controller/preprocess.rs +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -1,10 +1,10 @@ -use snafu::Snafu; +//! The preprocess step in the OpenSearchCluster controller + use stackable_operator::{ commons::resources::{PvcConfigFragment, ResourcesFragment}, k8s_openapi::apimachinery::pkg::api::resource::Quantity, role_utils::{CommonConfiguration, RoleGroup}, }; -use strum::{EnumDiscriminants, IntoStaticStr}; use tracing::info; use crate::{ @@ -12,18 +12,18 @@ use crate::{ framework::role_utils::GenericProductSpecificCommonConfig, }; -#[derive(Snafu, Debug, EnumDiscriminants)] -#[strum_discriminants(derive(IntoStaticStr))] -pub enum Error { - #[snafu(display("failed to get the cluster name"))] - GetClusterName { - source: crate::framework::controller_utils::Error, - }, +/// Preprocesses the OpenSearchCluster and adds configurations that the user is allowed to leave +/// out +pub fn preprocess(cluster: v1alpha1::OpenSearchCluster) -> v1alpha1::OpenSearchCluster { + preprocess_security_managing_role_group(cluster) } -type Result = std::result::Result; - -pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result { +/// Adds the role group defined in [`v1alpha1::Security::managing_role_group`] if the OpenSearch +/// security plugin is enabled, some security settings are managed by the operator and the defined +/// role group does not exist yet +pub fn preprocess_security_managing_role_group( + mut cluster: v1alpha1::OpenSearchCluster, +) -> v1alpha1::OpenSearchCluster { let security = &cluster.spec.cluster_config.security; if security.enabled && !security.settings.is_only_managed_by_api() @@ -68,5 +68,88 @@ pub fn preprocess(mut cluster: v1alpha1::OpenSearchCluster) -> Result = + serde_json::from_value(expected_role_groups_spec).expect("should be deserializable"); + + assert_eq!( + expected_role_groups, + prepocessed_cluster.spec.nodes.role_groups + ); + } } From 521ca8d11e283f75f5a0d70efe360d6a2fe5858e Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 15:22:27 +0100 Subject: [PATCH 42/53] Rename security_config_managing_role_group to security_config_managing_role_group_default --- rust/operator-binary/src/crd/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 0ec235b..3e6b1b7 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -144,7 +144,7 @@ pub mod versioned { pub enabled: bool, /// The role group that updates the security index if any setting is managed by the operator. - #[serde(default = "security_config_managing_role_group")] + #[serde(default = "security_config_managing_role_group_default")] pub managing_role_group: RoleGroupName, /// Settings for the OpenSearch security plugin @@ -572,7 +572,7 @@ impl Default for v1alpha1::Security { fn default() -> Self { Self { enabled: security_config_enabled_default(), - managing_role_group: security_config_managing_role_group(), + managing_role_group: security_config_managing_role_group_default(), settings: v1alpha1::SecuritySettings::default(), } } @@ -683,7 +683,7 @@ fn security_config_enabled_default() -> bool { true } -fn security_config_managing_role_group() -> RoleGroupName { +fn security_config_managing_role_group_default() -> RoleGroupName { RoleGroupName::from_str("security-config").expect("should be a valid role group name") } @@ -867,7 +867,7 @@ attributed_string_type! { mod tests { use strum::IntoEnumIterator; - use crate::crd::v1alpha1; + use crate::crd::{security_config_managing_role_group_default, v1alpha1}; #[test] fn test_node_role() { @@ -897,4 +897,10 @@ mod tests { container.to_container_name(); } } + + #[test] + fn test_security_config_managing_role_group_default() { + // Test that the function does not panic + security_config_managing_role_group_default(); + } } From 03de85b1833c27f4e3026f9cc246fbf194caf52b Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 15:24:55 +0100 Subject: [PATCH 43/53] Upgrade opensearch-py to version 3.1.0 --- tests/templates/kuttl/backup-restore/22-create-testuser.yaml | 2 +- tests/templates/kuttl/backup-restore/23-create-data.yaml | 2 +- tests/templates/kuttl/backup-restore/30-create-snapshot.yaml | 2 +- tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml | 2 +- tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml | 2 +- tests/templates/kuttl/ldap/30-test-opensearch.yaml | 2 +- tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/templates/kuttl/backup-restore/22-create-testuser.yaml b/tests/templates/kuttl/backup-restore/22-create-testuser.yaml index 6d171a0..5fd5f6c 100644 --- a/tests/templates/kuttl/backup-restore/22-create-testuser.yaml +++ b/tests/templates/kuttl/backup-restore/22-create-testuser.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/create-testuser.py env: # required for pip install diff --git a/tests/templates/kuttl/backup-restore/23-create-data.yaml b/tests/templates/kuttl/backup-restore/23-create-data.yaml index 24b9ebe..2daf051 100644 --- a/tests/templates/kuttl/backup-restore/23-create-data.yaml +++ b/tests/templates/kuttl/backup-restore/23-create-data.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/create-data.py env: # required for pip install diff --git a/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml b/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml index ff2c654..fc8e86a 100644 --- a/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml +++ b/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/create-snapshot.py env: # required for pip install diff --git a/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml b/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml index 870792a..32e4b62 100644 --- a/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml +++ b/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/restore-snapshot.py env: # required for pip install diff --git a/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml b/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml index 0414a6f..8b76d53 100644 --- a/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml +++ b/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/test-opensearch-2.py env: # required for pip install diff --git a/tests/templates/kuttl/ldap/30-test-opensearch.yaml b/tests/templates/kuttl/ldap/30-test-opensearch.yaml index c14b926..596f5b8 100644 --- a/tests/templates/kuttl/ldap/30-test-opensearch.yaml +++ b/tests/templates/kuttl/ldap/30-test-opensearch.yaml @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/test.py env: # required for pip install diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 index 1fc0950..a015635 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 @@ -16,7 +16,7 @@ spec: - -c args: - | - pip install opensearch-py==3.0.0 + pip install opensearch-py==3.1.0 python scripts/test.py env: # required for pip install From e517f53782df8e8a56e59ef440dd194faefa226a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 16:12:21 +0100 Subject: [PATCH 44/53] Fix tests --- rust/operator-binary/src/controller/preprocess.rs | 2 +- tests/templates/kuttl/logging/30-test-opensearch.yaml | 2 +- tests/test-definition.yaml | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs index 654c2f1..9346205 100644 --- a/rust/operator-binary/src/controller/preprocess.rs +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -19,7 +19,7 @@ pub fn preprocess(cluster: v1alpha1::OpenSearchCluster) -> v1alpha1::OpenSearchC } /// Adds the role group defined in [`v1alpha1::Security::managing_role_group`] if the OpenSearch -/// security plugin is enabled, some security settings are managed by the operator and the defined +/// security plugin is enabled, any security settings are managed by the operator and the defined /// role group does not exist yet pub fn preprocess_security_managing_role_group( mut cluster: v1alpha1::OpenSearchCluster, diff --git a/tests/templates/kuttl/logging/30-test-opensearch.yaml b/tests/templates/kuttl/logging/30-test-opensearch.yaml index 342af13..441efb7 100644 --- a/tests/templates/kuttl/logging/30-test-opensearch.yaml +++ b/tests/templates/kuttl/logging/30-test-opensearch.yaml @@ -37,7 +37,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure - backoffLimit: 10 + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 662da82..daffd46 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -59,12 +59,10 @@ tests: dimensions: - opensearch - opensearch_home - - release - name: security-disabled dimensions: - opensearch - opensearch_home - - release suites: - name: nightly patch: From 7e987a36e22fb554a195ff33c471cb900dfcd9ee Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Thu, 26 Feb 2026 17:43:13 +0100 Subject: [PATCH 45/53] Fix the test cases that work with the original image --- .../kuttl/ldap/21-install-opensearch.yaml.j2 | 2 +- .../security-config/11-install-opensearch.yaml.j2 | 6 ------ tests/test-definition.yaml | 12 +++++++----- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 index 944262a..cb8a248 100644 --- a/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/ldap/21-install-opensearch.yaml.j2 @@ -14,7 +14,7 @@ spec: security: settings: config: - managedBy: operator + managedBy: API content: valueFrom: secretKeyRef: diff --git a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 index b3f4842..fb035ed 100644 --- a/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 +++ b/tests/templates/kuttl/security-config/11-install-opensearch.yaml.j2 @@ -89,12 +89,6 @@ spec: data: capacity: 2Gi replicas: 2 - envOverrides: - # Only required for the official image - # The official image (built with https://github.com/opensearch-project/opensearch-build) - # installs a demo configuration if not disabled explicitly. - DISABLE_INSTALL_DEMO_CONFIG: "true" - OPENSEARCH_HOME: {{ test_scenario['values']['opensearch_home'] }} configOverrides: opensearch.yml: # Disable memory mapping in this test; If memory mapping were activated, the kernel setting diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index daffd46..927c35d 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -31,7 +31,8 @@ tests: dimensions: - opensearch - opensearch_home - # requires an image with the OpenSearch Prometheus exporter + # The test case "metrics" does not work with the original image because it requires the OpenSearch + # Prometheus exporter. - name: metrics dimensions: - opensearch @@ -39,7 +40,7 @@ tests: dimensions: - opensearch - opensearch_home - # requires an image with Vector + # The test case "logging" does not work with the original image because it requires Vector. - name: logging dimensions: - opensearch @@ -49,16 +50,18 @@ tests: - opensearch_home - server-use-tls - release - # requires the repository-s3 plugin + # The test case "backup-restore" does not work with the original image because it requires the + # repository-s3 plugin. - name: backup-restore dimensions: - opensearch - release - s3-use-tls + # The test case "security-config" does not work with the original image because it requires + # openssl. - name: security-config dimensions: - opensearch - - opensearch_home - name: security-disabled dimensions: - opensearch @@ -89,7 +92,6 @@ suites: - external-access - ldap - opensearch-dashboards - - security-config - security-disabled patch: - dimensions: From 8397acca211d83d19044b22947400f8c3a695675 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 27 Feb 2026 09:51:04 +0100 Subject: [PATCH 46/53] Add support for DEPRECATION log level --- .../build/product_logging/vector-test.yaml | 31 +++++++++++++++++++ .../build/product_logging/vector.yaml | 8 +++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/rust/operator-binary/src/controller/build/product_logging/vector-test.yaml b/rust/operator-binary/src/controller/build/product_logging/vector-test.yaml index 0468f60..9325f90 100644 --- a/rust/operator-binary/src/controller/build/product_logging/vector-test.yaml +++ b/rust/operator-binary/src/controller/build/product_logging/vector-test.yaml @@ -297,6 +297,37 @@ tests: "timestamp": t'2025-10-01T10:47:28.582Z' } + assert_eq!(expected_log_event, .) + - name: Test opensearch_server log entry with DEPRECATION level + inputs: + - type: log + insert_at: processed_files_opensearch_server + log_fields: + file: /stackable/log/opensearch/opensearch_server.json + message: | + {"type": "server", "timestamp": "2025-10-01T12:47:28,582Z", "level": "DEPRECATION", "component": "o.o.d.t.TransportInfo", "cluster.name": "opensearch", "node.name": "opensearch-nodes-cluster-manager-0", "message": "transport.publish_address was printed as [ip:port] instead of [hostname/ip:port]. This format is deprecated and will change to [hostname/ip:port] in a future version. Use -Dopensearch.transport.cname_in_publish_address=true to enforce non-deprecated formatting.", "cluster.uuid": "Jh1D6YAhTmyzkHI7vM1WWw", "node.id": "sk-r0P_TTYuPqaamTFbjKg" } + pod: opensearch-nodes-cluster-manager-0 + source_type: file + timestamp: 2025-10-01T12:47:29.473487331Z + outputs: + - extract_from: extended_logs + conditions: + - type: vrl + source: | + expected_log_event = { + "cluster": "opensearch", + "container": "opensearch", + "file": "opensearch_server.json", + "level": "WARN", + "logger": "o.o.d.t.TransportInfo", + "message": "transport.publish_address was printed as [ip:port] instead of [hostname/ip:port]. This format is deprecated and will change to [hostname/ip:port] in a future version. Use -Dopensearch.transport.cname_in_publish_address=true to enforce non-deprecated formatting.", + "namespace": "default", + "pod": "opensearch-nodes-cluster-manager-0", + "role": "nodes", + "roleGroup": "cluster-manager", + "timestamp": t'2025-10-01T10:47:28.582Z' + } + assert_eq!(expected_log_event, .) - name: Test opensearch_server log entry with unknown level inputs: diff --git a/rust/operator-binary/src/controller/build/product_logging/vector.yaml b/rust/operator-binary/src/controller/build/product_logging/vector.yaml index c01ac7c..205ed08 100644 --- a/rust/operator-binary/src/controller/build/product_logging/vector.yaml +++ b/rust/operator-binary/src/controller/build/product_logging/vector.yaml @@ -68,10 +68,12 @@ transforms: level, err = string(event.level) if err != null { .errors = push(.errors, "Level not found, using \"" + .level + "\" instead.") - } else if !includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { - .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") - } else { + } else if level == "DEPRECATION" { + .level = "WARN" + } else if includes(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"], level) { .level = level + } else { + .errors = push(.errors, "Level \"" + level + "\" unknown, using \"" + .level + "\" instead.") } .message, err = string(event.message) From 540c3734d036d651052099f81499c3c0eab35e3c Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Fri, 27 Feb 2026 12:23:46 +0100 Subject: [PATCH 47/53] test: Set backoffLimit for all jobs --- .../20-create-opensearch-1-admin-certificate.yaml | 1 + tests/templates/kuttl/backup-restore/22-create-testuser.yaml | 1 + tests/templates/kuttl/backup-restore/23-create-data.yaml | 1 + tests/templates/kuttl/backup-restore/30-create-snapshot.yaml | 1 + .../kuttl/backup-restore/31-backup-security-indices.yaml.j2 | 1 + .../50-create-opensearch-2-admin-certificate.yaml | 1 + .../kuttl/backup-restore/60-restore-security-indices.yaml.j2 | 1 + tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml | 1 + tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml | 1 + tests/templates/kuttl/ldap/30-test-opensearch.yaml | 1 + tests/templates/kuttl/metrics/30-check-metrics.yaml | 1 + .../opensearch-dashboards/30-test-opensearch-dashboards.yaml | 1 + tests/templates/kuttl/security-config/20-assert.yaml | 2 +- .../kuttl/security-config/20-test-initial-security-config.yaml | 1 + tests/templates/kuttl/security-config/22-assert.yaml | 2 +- .../kuttl/security-config/22-test-updated-security-config.yaml | 1 + tests/templates/kuttl/security-disabled/20-test-opensearch.yaml | 1 + tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 | 1 + 18 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/templates/kuttl/backup-restore/20-create-opensearch-1-admin-certificate.yaml b/tests/templates/kuttl/backup-restore/20-create-opensearch-1-admin-certificate.yaml index 9e5592f..3cdbf52 100644 --- a/tests/templates/kuttl/backup-restore/20-create-opensearch-1-admin-certificate.yaml +++ b/tests/templates/kuttl/backup-restore/20-create-opensearch-1-admin-certificate.yaml @@ -36,6 +36,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/22-create-testuser.yaml b/tests/templates/kuttl/backup-restore/22-create-testuser.yaml index 5fd5f6c..8284902 100644 --- a/tests/templates/kuttl/backup-restore/22-create-testuser.yaml +++ b/tests/templates/kuttl/backup-restore/22-create-testuser.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/23-create-data.yaml b/tests/templates/kuttl/backup-restore/23-create-data.yaml index 2daf051..33f4500 100644 --- a/tests/templates/kuttl/backup-restore/23-create-data.yaml +++ b/tests/templates/kuttl/backup-restore/23-create-data.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml b/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml index fc8e86a..b38c851 100644 --- a/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml +++ b/tests/templates/kuttl/backup-restore/30-create-snapshot.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/31-backup-security-indices.yaml.j2 b/tests/templates/kuttl/backup-restore/31-backup-security-indices.yaml.j2 index 8da84ca..a4f3817 100644 --- a/tests/templates/kuttl/backup-restore/31-backup-security-indices.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/31-backup-security-indices.yaml.j2 @@ -116,6 +116,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/50-create-opensearch-2-admin-certificate.yaml b/tests/templates/kuttl/backup-restore/50-create-opensearch-2-admin-certificate.yaml index d66ef1b..102edab 100644 --- a/tests/templates/kuttl/backup-restore/50-create-opensearch-2-admin-certificate.yaml +++ b/tests/templates/kuttl/backup-restore/50-create-opensearch-2-admin-certificate.yaml @@ -36,6 +36,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/60-restore-security-indices.yaml.j2 b/tests/templates/kuttl/backup-restore/60-restore-security-indices.yaml.j2 index d2c41fd..be6d766 100644 --- a/tests/templates/kuttl/backup-restore/60-restore-security-indices.yaml.j2 +++ b/tests/templates/kuttl/backup-restore/60-restore-security-indices.yaml.j2 @@ -116,6 +116,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml b/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml index 32e4b62..d436b66 100644 --- a/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml +++ b/tests/templates/kuttl/backup-restore/61-restore-snapshot.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml b/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml index 8b76d53..37a3f02 100644 --- a/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml +++ b/tests/templates/kuttl/backup-restore/70-test-opensearch-2.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/ldap/30-test-opensearch.yaml b/tests/templates/kuttl/ldap/30-test-opensearch.yaml index 596f5b8..a720d96 100644 --- a/tests/templates/kuttl/ldap/30-test-opensearch.yaml +++ b/tests/templates/kuttl/ldap/30-test-opensearch.yaml @@ -64,6 +64,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/metrics/30-check-metrics.yaml b/tests/templates/kuttl/metrics/30-check-metrics.yaml index da80a13..ddd90c2 100644 --- a/tests/templates/kuttl/metrics/30-check-metrics.yaml +++ b/tests/templates/kuttl/metrics/30-check-metrics.yaml @@ -35,3 +35,4 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 diff --git a/tests/templates/kuttl/opensearch-dashboards/30-test-opensearch-dashboards.yaml b/tests/templates/kuttl/opensearch-dashboards/30-test-opensearch-dashboards.yaml index 05b6880..0bb8681 100644 --- a/tests/templates/kuttl/opensearch-dashboards/30-test-opensearch-dashboards.yaml +++ b/tests/templates/kuttl/opensearch-dashboards/30-test-opensearch-dashboards.yaml @@ -56,6 +56,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: secrets.stackable.tech/v1alpha1 kind: TrustStore diff --git a/tests/templates/kuttl/security-config/20-assert.yaml b/tests/templates/kuttl/security-config/20-assert.yaml index ce5e8a8..d526d49 100644 --- a/tests/templates/kuttl/security-config/20-assert.yaml +++ b/tests/templates/kuttl/security-config/20-assert.yaml @@ -1,7 +1,7 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 300 +timeout: 600 --- apiVersion: batch/v1 kind: Job diff --git a/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml b/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml index 87b9c8f..c41667b 100644 --- a/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml +++ b/tests/templates/kuttl/security-config/20-test-initial-security-config.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/security-config/22-assert.yaml b/tests/templates/kuttl/security-config/22-assert.yaml index 990c02e..83cbd2b 100644 --- a/tests/templates/kuttl/security-config/22-assert.yaml +++ b/tests/templates/kuttl/security-config/22-assert.yaml @@ -1,7 +1,7 @@ --- apiVersion: kuttl.dev/v1beta1 kind: TestAssert -timeout: 300 +timeout: 600 --- apiVersion: batch/v1 kind: Job diff --git a/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml b/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml index f7db842..de8e9fd 100644 --- a/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml +++ b/tests/templates/kuttl/security-config/22-test-updated-security-config.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml b/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml index 00497b3..d878a17 100644 --- a/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml +++ b/tests/templates/kuttl/security-disabled/20-test-opensearch.yaml @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: v1 kind: ConfigMap diff --git a/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 index a015635..f76d04f 100644 --- a/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 +++ b/tests/templates/kuttl/smoke/20-test-opensearch.yaml.j2 @@ -54,6 +54,7 @@ spec: securityContext: fsGroup: 1000 restartPolicy: OnFailure + backoffLimit: 10 --- apiVersion: secrets.stackable.tech/v1alpha1 kind: TrustStore From 7f21cefff83c39640b24c05f94a3d71304cf3734 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 2 Mar 2026 14:24:54 +0100 Subject: [PATCH 48/53] Validate node roles; Fix coordinating_only node role --- rust/operator-binary/src/controller.rs | 81 +++++++++++------- rust/operator-binary/src/controller/build.rs | 20 ++--- .../src/controller/build/node_config.rs | 83 ++++++++++++++----- .../src/controller/build/role_builder.rs | 17 ++-- .../controller/build/role_group_builder.rs | 29 +++---- .../src/controller/preprocess.rs | 6 +- .../src/controller/validate.rs | 78 ++++++++++++++--- rust/operator-binary/src/crd/mod.rs | 12 +-- 8 files changed, 219 insertions(+), 107 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 28b9af3..3859380 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -3,7 +3,12 @@ //! The cluster specification is validated, Kubernetes resource specifications are created and //! applied and the cluster status is updated. -use std::{collections::BTreeMap, marker::PhantomData, str::FromStr, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + marker::PhantomData, + str::FromStr, + sync::Arc, +}; use apply::Applier; use build::build; @@ -26,13 +31,13 @@ use stackable_operator::{ logging::controller::ReconcilerError, shared::time::Duration, }; -use strum::{EnumDiscriminants, IntoStaticStr}; +use strum::{Display, EnumDiscriminants, EnumIter, IntoStaticStr}; use update_status::update_status; use validate::validate; use crate::{ controller::preprocess::preprocess, - crd::{NodeRoles, v1alpha1}, + crd::v1alpha1, framework::{ HasName, HasUid, NameIsValidLabelValue, product_logging::framework::{ValidatedContainerLogConfigChoice, VectorContainerLogConfig}, @@ -159,7 +164,7 @@ pub struct ValidatedOpenSearchConfig { pub discovery_service_exposed: bool, pub listener_class: ListenerClassName, pub logging: ValidatedLogging, - pub node_roles: NodeRoles, + pub node_roles: ValidatedNodeRoles, pub requested_secret_lifetime: Duration, pub resources: OpenSearchNodeResources, pub termination_grace_period_seconds: i64, @@ -178,6 +183,23 @@ impl ValidatedLogging { } } +/// Set of validated node roles +/// +/// An empty set specifies a coordinating only node. +type ValidatedNodeRoles = BTreeSet; + +/// Validated node role +#[derive(Clone, Copy, Debug, Display, EnumIter, Eq, PartialEq, PartialOrd, Ord)] +#[strum(serialize_all = "snake_case")] +pub enum ValidatedNodeRole { + ClusterManager, + Data, + Ingest, + RemoteClusterClient, + Search, + Warm, +} + /// Validated security configuration #[derive(Clone, Debug, PartialEq)] pub enum ValidatedSecurity { @@ -285,7 +307,7 @@ impl ValidatedCluster { /// Returns all role-group configurations which contain the given node role pub fn role_group_configs_filtered_by_node_role( &self, - node_role: &v1alpha1::NodeRole, + node_role: &ValidatedNodeRole, ) -> BTreeMap { self.role_group_configs .clone() @@ -481,8 +503,11 @@ mod tests { use super::{Context, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedLogging}; use crate::{ - controller::{OpenSearchNodeResources, ValidatedOpenSearchConfig, ValidatedSecurity}, - crd::{NodeRoles, v1alpha1}, + controller::{ + OpenSearchNodeResources, ValidatedNodeRole, ValidatedNodeRoles, + ValidatedOpenSearchConfig, ValidatedSecurity, + }, + crd::v1alpha1, framework::{ builder::pod::container::EnvVarSet, product_logging::framework::ValidatedContainerLogConfigChoice, @@ -533,10 +558,10 @@ mod tests { RoleGroupName::from_str_unsafe("data1"), role_group_config( 4, - &[ - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, + [ + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient, ], ), ), @@ -544,15 +569,15 @@ mod tests { RoleGroupName::from_str_unsafe("data2"), role_group_config( 6, - &[ - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, + [ + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient, ], ), ), ]), - validated_cluster.role_group_configs_filtered_by_node_role(&v1alpha1::NodeRole::Data) + validated_cluster.role_group_configs_filtered_by_node_role(&ValidatedNodeRole::Data) ); } @@ -574,20 +599,20 @@ mod tests { [ ( RoleGroupName::from_str_unsafe("coordinating"), - role_group_config(5, &[v1alpha1::NodeRole::CoordinatingOnly]), + role_group_config(5, []), ), ( RoleGroupName::from_str_unsafe("cluster-manager"), - role_group_config(3, &[v1alpha1::NodeRole::ClusterManager]), + role_group_config(3, [ValidatedNodeRole::ClusterManager]), ), ( RoleGroupName::from_str_unsafe("data1"), role_group_config( 4, - &[ - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, + [ + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient, ], ), ), @@ -595,10 +620,10 @@ mod tests { RoleGroupName::from_str_unsafe("data2"), role_group_config( 6, - &[ - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, + [ + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient, ], ), ), @@ -616,7 +641,7 @@ mod tests { fn role_group_config( replicas: u16, - node_roles: &[v1alpha1::NodeRole], + node_roles: impl Into, ) -> OpenSearchRoleGroupConfig { OpenSearchRoleGroupConfig { replicas, @@ -630,7 +655,7 @@ mod tests { ), vector_container: None, }, - node_roles: NodeRoles(node_roles.to_vec()), + node_roles: node_roles.into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), diff --git a/rust/operator-binary/src/controller/build.rs b/rust/operator-binary/src/controller/build.rs index 23ece0a..5be50f3 100644 --- a/rust/operator-binary/src/controller/build.rs +++ b/rust/operator-binary/src/controller/build.rs @@ -82,9 +82,9 @@ mod tests { controller::{ ContextNames, OpenSearchNodeResources, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedDiscoveryEndpoint, ValidatedLogging, - ValidatedOpenSearchConfig, ValidatedSecurity, + ValidatedNodeRole, ValidatedNodeRoles, ValidatedOpenSearchConfig, ValidatedSecurity, }, - crd::{NodeRoles, v1alpha1}, + crd::v1alpha1, framework::{ builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, @@ -190,20 +190,20 @@ mod tests { [ ( RoleGroupName::from_str_unsafe("coordinating"), - role_group_config(5, &[v1alpha1::NodeRole::CoordinatingOnly]), + role_group_config(5, []), ), ( RoleGroupName::from_str_unsafe("cluster-manager"), - role_group_config(3, &[v1alpha1::NodeRole::ClusterManager]), + role_group_config(3, [ValidatedNodeRole::ClusterManager]), ), ( RoleGroupName::from_str_unsafe("data"), role_group_config( 8, - &[ - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient, + [ + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient, ], ), ), @@ -220,7 +220,7 @@ mod tests { fn role_group_config( replicas: u16, - node_roles: &[v1alpha1::NodeRole], + node_roles: impl Into, ) -> OpenSearchRoleGroupConfig { OpenSearchRoleGroupConfig { replicas, @@ -234,7 +234,7 @@ mod tests { ), vector_container: None, }, - node_roles: NodeRoles(node_roles.to_vec()), + node_roles: node_roles.into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: OpenSearchNodeResources::default(), diff --git a/rust/operator-binary/src/controller/build/node_config.rs b/rust/operator-binary/src/controller/build/node_config.rs index 95c2a00..26591fd 100644 --- a/rust/operator-binary/src/controller/build/node_config.rs +++ b/rust/operator-binary/src/controller/build/node_config.rs @@ -8,7 +8,10 @@ use tracing::warn; use super::ValidatedCluster; use crate::{ - controller::{OpenSearchRoleGroupConfig, build::role_group_builder::RoleGroupSecurityMode}, + controller::{ + OpenSearchRoleGroupConfig, ValidatedNodeRole, + build::role_group_builder::RoleGroupSecurityMode, + }, crd::v1alpha1, framework::{ builder::pod::container::{EnvVarName, EnvVarSet}, @@ -372,15 +375,16 @@ impl NodeConfig { ) .with_value( &EnvVarName::from_str_unsafe(CONFIG_OPTION_NODE_ROLES), - self.role_group_config - .config - .node_roles - .iter() - .map(|node_role| format!("{node_role}")) - .collect::>() - // Node roles cannot contain commas, therefore creating a comma-separated list - // is safe. - .join(","), + Self::to_comma_separated_list( + &self + .role_group_config + .config + .node_roles + .iter() + .map(|node_role| format!("{node_role}")) + .collect::>(), + ) + .expect("Node roles cannot contain commas, therefore creating a comma-separated list is safe."), ); if let Some(initial_cluster_manager_nodes) = self.initial_cluster_manager_nodes() { @@ -476,13 +480,13 @@ impl NodeConfig { .role_group_config .config .node_roles - .contains(&v1alpha1::NodeRole::ClusterManager) + .contains(&ValidatedNodeRole::ClusterManager) { None } else { let cluster_manager_configs = self .cluster - .role_group_configs_filtered_by_node_role(&v1alpha1::NodeRole::ClusterManager); + .role_group_configs_filtered_by_node_role(&ValidatedNodeRole::ClusterManager); // This setting requires node names as set in NODE_NAME. // The node names are set to the pod names with @@ -501,8 +505,8 @@ impl NodeConfig { .map(|i| format!("{}-{i}", role_group_resource_names.stateful_set_name())), ); } - // Pod names cannot contain commas, therefore creating a comma-separated list is safe. - Some(pod_names.join(",")) + + Some(Self::to_comma_separated_list(&pod_names).expect("Pod names cannot contain commas, therefore creating a comma-separated list is safe.")) } } @@ -522,6 +526,16 @@ impl NodeConfig { .and_then(|env_var| env_var.value.clone()) .unwrap_or(format!("{opensearch_home}/config")) } + + fn to_comma_separated_list(values: &[String]) -> Option { + if values.iter().any(|value| value.contains(",")) { + None + } else if values.is_empty() { + Some("[]".to_owned()) + } else { + Some(values.join(",")) + } + } } #[cfg(test)] @@ -545,7 +559,7 @@ mod tests { use super::*; use crate::{ controller::{ValidatedLogging, ValidatedOpenSearchConfig, ValidatedSecurity}, - crd::{NodeRoles, v1alpha1}, + crd::v1alpha1, framework::{ product_logging::framework::ValidatedContainerLogConfigChoice, role_utils::GenericProductSpecificCommonConfig, @@ -592,12 +606,13 @@ mod tests { ), vector_container: None, }, - node_roles: NodeRoles(vec![ - v1alpha1::NodeRole::ClusterManager, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::RemoteClusterClient, - ]), + node_roles: [ + ValidatedNodeRole::ClusterManager, + ValidatedNodeRole::Data, + ValidatedNodeRole::Ingest, + ValidatedNodeRole::RemoteClusterClient, + ] + .into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), @@ -826,4 +841,30 @@ mod tests { node_config_multiple_nodes.initial_cluster_manager_nodes() ); } + + #[test] + pub fn test_to_comma_separated_list() { + assert_eq!( + None, + NodeConfig::to_comma_separated_list(&[ + "one".to_owned(), + "two,three".to_owned(), + "four".to_owned() + ]) + ); + + assert_eq!( + Some("[]".to_owned()), + NodeConfig::to_comma_separated_list(&[]) + ); + + assert_eq!( + Some("one,two,three".to_owned()), + NodeConfig::to_comma_separated_list(&[ + "one".to_owned(), + "two".to_owned(), + "three".to_owned() + ]) + ); + } } diff --git a/rust/operator-binary/src/controller/build/role_builder.rs b/rust/operator-binary/src/controller/build/role_builder.rs index f9dac5a..385e0fc 100644 --- a/rust/operator-binary/src/controller/build/role_builder.rs +++ b/rust/operator-binary/src/controller/build/role_builder.rs @@ -336,12 +336,12 @@ mod tests { controller::{ ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, ValidatedContainerLogConfigChoice, ValidatedDiscoveryEndpoint, ValidatedLogging, - ValidatedOpenSearchConfig, ValidatedSecurity, + ValidatedNodeRole, ValidatedOpenSearchConfig, ValidatedSecurity, build::role_builder::{ discovery_config_map_name, discovery_service_listener_name, seed_nodes_service_name, }, }, - crd::{NodeRoles, v1alpha1}, + crd::v1alpha1, framework::{ builder::pod::container::EnvVarSet, role_utils::GenericProductSpecificCommonConfig, @@ -385,12 +385,13 @@ mod tests { ), vector_container: None, }, - node_roles: NodeRoles(vec![ - v1alpha1::NodeRole::ClusterManager, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::RemoteClusterClient, - ]), + node_roles: [ + ValidatedNodeRole::ClusterManager, + ValidatedNodeRole::Data, + ValidatedNodeRole::Ingest, + ValidatedNodeRole::RemoteClusterClient, + ] + .into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), 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 08534a6..7ea7519 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -46,7 +46,7 @@ use crate::{ constant, controller::{ ContextNames, HTTP_PORT, HTTP_PORT_NAME, OpenSearchRoleGroupConfig, TRANSPORT_PORT, - TRANSPORT_PORT_NAME, ValidatedCluster, ValidatedSecurity, + TRANSPORT_PORT_NAME, ValidatedCluster, ValidatedNodeRole, ValidatedSecurity, build::product_logging::config::{ MAX_OPENSEARCH_SERVER_LOG_FILES_SIZE, vector_config_file_extra_env_vars, }, @@ -494,14 +494,14 @@ impl<'a> RoleGroupBuilder<'a> { ); labels.insert(Self::build_node_role_label( - &v1alpha1::NodeRole::ClusterManager, + &ValidatedNodeRole::ClusterManager, )); labels } /// Builds a label indicating the role of the OpenSearch node - fn build_node_role_label(node_role: &v1alpha1::NodeRole) -> Label { + fn build_node_role_label(node_role: &ValidatedNodeRole) -> Label { // It is not possible to check the infallibility of the following statement at // compile-time. Instead, it is tested in `tests::test_build_node_role_label`. Label::try_from(( @@ -1045,7 +1045,7 @@ impl<'a> RoleGroupBuilder<'a> { .role_group_config .config .node_roles - .contains(&v1alpha1::NodeRole::ClusterManager) + .contains(&ValidatedNodeRole::ClusterManager) { volume_source_builder.with_service_scope(&self.node_config.seed_nodes_service_name); } @@ -1387,14 +1387,14 @@ mod tests { use crate::{ controller::{ ContextNames, OpenSearchRoleGroupConfig, ValidatedCluster, - ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedOpenSearchConfig, - ValidatedSecurity, + ValidatedContainerLogConfigChoice, ValidatedLogging, ValidatedNodeRole, + ValidatedOpenSearchConfig, ValidatedSecurity, build::role_group_builder::{ DISCOVERY_SERVICE_LISTENER_VOLUME_NAME, OPENSEARCH_KEYSTORE_VOLUME_NAME, TLS_INTERNAL_VOLUME_NAME, TLS_SERVER_CA_VOLUME_NAME, TLS_SERVER_VOLUME_NAME, }, }, - crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, + crd::{OpenSearchKeystoreKey, v1alpha1}, framework::{ builder::pod::container::EnvVarSet, product_logging::framework::VectorContainerLogConfig, @@ -1494,12 +1494,13 @@ mod tests { ), }), }, - node_roles: NodeRoles(vec![ - v1alpha1::NodeRole::ClusterManager, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::RemoteClusterClient, - ]), + node_roles: [ + ValidatedNodeRole::ClusterManager, + ValidatedNodeRole::Data, + ValidatedNodeRole::Ingest, + ValidatedNodeRole::RemoteClusterClient, + ] + .into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources::default(), @@ -3342,7 +3343,7 @@ mod tests { #[test] fn test_build_node_role_label() { // Test that the function does not panic on all possible inputs - for node_role in v1alpha1::NodeRole::iter() { + for node_role in ValidatedNodeRole::iter() { RoleGroupBuilder::build_node_role_label(&node_role); } } diff --git a/rust/operator-binary/src/controller/preprocess.rs b/rust/operator-binary/src/controller/preprocess.rs index 9346205..4965eb0 100644 --- a/rust/operator-binary/src/controller/preprocess.rs +++ b/rust/operator-binary/src/controller/preprocess.rs @@ -44,7 +44,7 @@ pub fn preprocess_security_managing_role_group( config: CommonConfiguration { config: v1alpha1::OpenSearchConfigFragment { discovery_service_exposed: Some(false), - node_roles: Some(NodeRoles(vec![])), + node_roles: Some(NodeRoles(vec![v1alpha1::NodeRole::CoordinatingOnly])), resources: ResourcesFragment { storage: v1alpha1::StorageConfigFragment { data: PvcConfigFragment { @@ -132,7 +132,9 @@ mod tests { "security-manager": { "config" : { "discoveryServiceExposed": false, - "nodeRoles": [], + "nodeRoles": [ + "coordinating_only" + ], "resources": { "storage": { "data": { diff --git a/rust/operator-binary/src/controller/validate.rs b/rust/operator-binary/src/controller/validate.rs index 89c9f40..62bbaf1 100644 --- a/rust/operator-binary/src/controller/validate.rs +++ b/rust/operator-binary/src/controller/validate.rs @@ -15,9 +15,10 @@ use super::{ }; use crate::{ controller::{ - DereferencedObjects, HTTP_PORT_NAME, ValidatedDiscoveryEndpoint, ValidatedSecurity, + DereferencedObjects, HTTP_PORT_NAME, ValidatedDiscoveryEndpoint, ValidatedNodeRole, + ValidatedNodeRoles, ValidatedSecurity, }, - crd::v1alpha1::{self}, + crd::{NodeRoles, v1alpha1}, framework::{ builder::pod::container::{EnvVarName, EnvVarSet}, controller_utils::{get_cluster_name, get_namespace, get_uid}, @@ -48,6 +49,9 @@ pub enum Error { ))] CheckSecurityConfigTlsSettings {}, + #[snafu(display("node role \"coordinating_only\" not compatible with other node roles"))] + ConflictingNodeRoles {}, + #[snafu(display("failed to get the cluster name"))] GetClusterName { source: crate::framework::controller_utils::Error, @@ -210,6 +214,8 @@ fn validate_role_group_config( .vector_aggregator_config_map_name, )?; + let node_roles = validate_node_roles(&merged_role_group.config.config.node_roles)?; + let graceful_shutdown_timeout = merged_role_group.config.config.graceful_shutdown_timeout; let termination_grace_period_seconds = graceful_shutdown_timeout.as_secs().try_into().context( TerminationGracePeriodTooLongSnafu { @@ -222,7 +228,7 @@ fn validate_role_group_config( discovery_service_exposed: merged_role_group.config.config.discovery_service_exposed, listener_class: merged_role_group.config.config.listener_class, logging, - node_roles: merged_role_group.config.config.node_roles, + node_roles, requested_secret_lifetime: merged_role_group.config.config.requested_secret_lifetime, resources: merged_role_group.config.config.resources, termination_grace_period_seconds, @@ -279,6 +285,30 @@ fn validate_logging_configuration( }) } +fn validate_node_roles(node_roles: &NodeRoles) -> Result { + if node_roles.0.is_empty() || node_roles.0 == vec![v1alpha1::NodeRole::CoordinatingOnly] { + Ok(ValidatedNodeRoles::new()) + } else { + let validated_node_roles = node_roles + .0 + .iter() + .map(|node_role| match node_role { + v1alpha1::NodeRole::ClusterManager => Ok(ValidatedNodeRole::ClusterManager), + v1alpha1::NodeRole::CoordinatingOnly => ConflictingNodeRolesSnafu.fail(), + v1alpha1::NodeRole::Data => Ok(ValidatedNodeRole::Data), + v1alpha1::NodeRole::Ingest => Ok(ValidatedNodeRole::Ingest), + v1alpha1::NodeRole::RemoteClusterClient => { + Ok(ValidatedNodeRole::RemoteClusterClient) + } + v1alpha1::NodeRole::Warm => Ok(ValidatedNodeRole::Warm), + v1alpha1::NodeRole::Search => Ok(ValidatedNodeRole::Search), + }) + .collect::>()?; + + Ok(validated_node_roles) + } +} + fn validate_security_config(spec: &v1alpha1::OpenSearchClusterSpec) -> Result { let security = if spec.cluster_config.security.enabled { if spec @@ -429,7 +459,7 @@ mod tests { built_info, controller::{ ContextNames, DereferencedObjects, ValidatedCluster, ValidatedDiscoveryEndpoint, - ValidatedLogging, ValidatedOpenSearchConfig, ValidatedSecurity, + ValidatedLogging, ValidatedNodeRole, ValidatedOpenSearchConfig, ValidatedSecurity, }, crd::{NodeRoles, OpenSearchKeystoreKey, v1alpha1}, framework::{ @@ -564,15 +594,13 @@ mod tests { ConfigMapName::from_str_unsafe("vector-aggregator"), }), }, - node_roles: NodeRoles( - [ - v1alpha1::NodeRole::ClusterManager, - v1alpha1::NodeRole::Ingest, - v1alpha1::NodeRole::Data, - v1alpha1::NodeRole::RemoteClusterClient - ] - .into() - ), + node_roles: [ + ValidatedNodeRole::ClusterManager, + ValidatedNodeRole::Ingest, + ValidatedNodeRole::Data, + ValidatedNodeRole::RemoteClusterClient + ] + .into(), requested_secret_lifetime: Duration::from_str("1d") .expect("should be a valid duration"), resources: Resources { @@ -786,6 +814,30 @@ mod tests { ); } + #[test] + fn test_validate_err_conflicting_node_roles() { + test_validate_err( + |cluster, _| { + cluster + .spec + .nodes + .role_groups + .get_mut("default") + .expect("should be defined in the test cluster spec") + .config + .config + .node_roles = Some(NodeRoles(vec![ + v1alpha1::NodeRole::ClusterManager, + v1alpha1::NodeRole::CoordinatingOnly, + v1alpha1::NodeRole::Ingest, + v1alpha1::NodeRole::Data, + v1alpha1::NodeRole::RemoteClusterClient, + ])); + }, + ErrorDiscriminants::ConflictingNodeRoles, + ); + } + #[test] fn test_validate_err_termination_grace_period_too_long() { test_validate_err( diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 3e6b1b7..56bd2ae 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -1,4 +1,4 @@ -use std::{array, slice, str::FromStr}; +use std::{array, str::FromStr}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -825,16 +825,6 @@ fn discovery_service_listener_class_default() -> ListenerClassName { #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct NodeRoles(pub Vec); -impl NodeRoles { - pub fn contains(&self, node_role: &v1alpha1::NodeRole) -> bool { - self.0.contains(node_role) - } - - pub fn iter(&self) -> slice::Iter<'_, v1alpha1::NodeRole> { - self.0.iter() - } -} - impl Atomic for NodeRoles {} impl v1alpha1::Container { From a45edb7af861af68f456935d2e2e9e7622272a15 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 2 Mar 2026 14:35:22 +0100 Subject: [PATCH 49/53] doc: Document the security plugin configuration --- .../getting_started/getting_started.sh | 33 +++- .../getting_started/getting_started.sh.j2 | 33 +++- .../initial-opensearch-security-config.yaml | 34 ++++ .../opensearch-dashboards-values.yaml | 60 +++--- .../opensearch-dashboards-values.yaml.j2 | 60 +++--- .../examples/getting_started/opensearch.yaml | 52 +++-- .../pages/getting_started/first_steps.adoc | 27 ++- .../opensearch/pages/reference/discovery.adoc | 8 +- .../pages/usage-guide/node-roles.adoc | 25 ++- .../usage-guide/opensearch-dashboards.adoc | 60 +++--- .../usage-guide/opensearch-dashboards.adoc.j2 | 60 +++--- .../pages/usage-guide/security.adoc | 177 +++++++++++++++++- 12 files changed, 457 insertions(+), 172 deletions(-) create mode 100644 docs/modules/opensearch/examples/getting_started/initial-opensearch-security-config.yaml diff --git a/docs/modules/opensearch/examples/getting_started/getting_started.sh b/docs/modules/opensearch/examples/getting_started/getting_started.sh index de50009..986b58e 100755 --- a/docs/modules/opensearch/examples/getting_started/getting_started.sh +++ b/docs/modules/opensearch/examples/getting_started/getting_started.sh @@ -47,7 +47,7 @@ esac echo "Creating OpenSearch security plugin configuration" # tag::apply-security-config[] -kubectl apply -f opensearch-security-config.yaml +kubectl apply -f initial-opensearch-security-config.yaml # end::apply-security-config[] echo "Creating OpenSearch cluster" @@ -91,8 +91,21 @@ curl \ --json '{"name": "Stackable"}' \ "$OPENSEARCH_HOST/sample_index/_doc/1" -# Output: -# {"_index":"sample_index","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1} +# Formatted output: +# { +# "_index": "sample_index", +# "_id": "1", +# "_version": 1, +# "result": "created", +# "_shards": { +# "total": 2, +# "successful": 1, +# "failed": 0 +# }, +# "_seq_no": 0, +# "_primary_term": 1 +# } + curl \ --insecure \ @@ -100,8 +113,18 @@ curl \ --request GET \ "$OPENSEARCH_HOST/sample_index/_doc/1" -# Output: -# {"_index":"sample_index","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"name": "Stackable"}} +# Formatted output: +# { +# "_index": "sample_index", +# "_id": "1", +# "_version": 1, +# "_seq_no": 0, +# "_primary_term": 1, +# "found": true, +# "_source": { +# "name": "Stackable" +# } +# } # end::rest-api[] echo diff --git a/docs/modules/opensearch/examples/getting_started/getting_started.sh.j2 b/docs/modules/opensearch/examples/getting_started/getting_started.sh.j2 index 51246e8..c23b438 100755 --- a/docs/modules/opensearch/examples/getting_started/getting_started.sh.j2 +++ b/docs/modules/opensearch/examples/getting_started/getting_started.sh.j2 @@ -47,7 +47,7 @@ esac echo "Creating OpenSearch security plugin configuration" # tag::apply-security-config[] -kubectl apply -f opensearch-security-config.yaml +kubectl apply -f initial-opensearch-security-config.yaml # end::apply-security-config[] echo "Creating OpenSearch cluster" @@ -91,8 +91,21 @@ curl \ --json '{"name": "Stackable"}' \ "$OPENSEARCH_HOST/sample_index/_doc/1" -# Output: -# {"_index":"sample_index","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1} +# Formatted output: +# { +# "_index": "sample_index", +# "_id": "1", +# "_version": 1, +# "result": "created", +# "_shards": { +# "total": 2, +# "successful": 1, +# "failed": 0 +# }, +# "_seq_no": 0, +# "_primary_term": 1 +# } + curl \ --insecure \ @@ -100,8 +113,18 @@ curl \ --request GET \ "$OPENSEARCH_HOST/sample_index/_doc/1" -# Output: -# {"_index":"sample_index","_id":"1","_version":1,"_seq_no":0,"_primary_term":1,"found":true,"_source":{"name": "Stackable"}} +# Formatted output: +# { +# "_index": "sample_index", +# "_id": "1", +# "_version": 1, +# "_seq_no": 0, +# "_primary_term": 1, +# "found": true, +# "_source": { +# "name": "Stackable" +# } +# } # end::rest-api[] echo diff --git a/docs/modules/opensearch/examples/getting_started/initial-opensearch-security-config.yaml b/docs/modules/opensearch/examples/getting_started/initial-opensearch-security-config.yaml new file mode 100644 index 0000000..486f58f --- /dev/null +++ b/docs/modules/opensearch/examples/getting_started/initial-opensearch-security-config.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: initial-opensearch-security-config +stringData: + internal_users.yml: | + --- + _meta: + type: internalusers + config_version: 2 + admin: + hash: $2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e + reserved: true + backend_roles: + - admin + description: OpenSearch admin user + kibanaserver: + hash: $2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS + reserved: true + description: OpenSearch Dashboards user + roles_mapping.yml: | + --- + _meta: + type: rolesmapping + config_version: 2 + all_access: + reserved: false + backend_roles: + - admin + kibana_server: + reserved: true + users: + - kibanaserver diff --git a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml index 1073c1a..5565626 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml +++ b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml @@ -17,41 +17,41 @@ config: ssl: verificationMode: full certificateAuthorities: - - /stackable/opensearch-dashboards/config/tls/ca.crt + - /stackable/opensearch-dashboards/config/tls/ca.crt opensearch_security: cookie: secure: true extraEnvs: - - name: OPENSEARCH_HOSTS - valueFrom: - configMapKeyRef: - name: simple-opensearch - key: OPENSEARCH_HOSTS - - name: OPENSEARCH_PASSWORD - valueFrom: - secretKeyRef: - name: opensearch-credentials - key: kibanaserver +- name: OPENSEARCH_HOSTS + valueFrom: + configMapKeyRef: + name: simple-opensearch + key: OPENSEARCH_HOSTS +- name: OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: opensearch-credentials + key: kibanaserver extraVolumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch-dashboards - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" +- name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=opensearch-dashboards + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" extraVolumeMounts: - - mountPath: /stackable/opensearch-dashboards/config/tls - name: tls - - mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml - name: config - subPath: opensearch_dashboards.yml +- mountPath: /stackable/opensearch-dashboards/config/tls + name: tls +- mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml + name: config + subPath: opensearch_dashboards.yml podSecurityContext: fsGroup: 1000 diff --git a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 index 0b3977f..07da0ac 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 +++ b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 @@ -17,41 +17,41 @@ config: ssl: verificationMode: full certificateAuthorities: - - /stackable/opensearch-dashboards/config/tls/ca.crt + - /stackable/opensearch-dashboards/config/tls/ca.crt opensearch_security: cookie: secure: true extraEnvs: - - name: OPENSEARCH_HOSTS - valueFrom: - configMapKeyRef: - name: simple-opensearch - key: OPENSEARCH_HOSTS - - name: OPENSEARCH_PASSWORD - valueFrom: - secretKeyRef: - name: opensearch-credentials - key: kibanaserver +- name: OPENSEARCH_HOSTS + valueFrom: + configMapKeyRef: + name: simple-opensearch + key: OPENSEARCH_HOSTS +- name: OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: opensearch-credentials + key: kibanaserver extraVolumes: - - name: tls - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch-dashboards - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" +- name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=opensearch-dashboards + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" extraVolumeMounts: - - mountPath: /stackable/opensearch-dashboards/config/tls - name: tls - - mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml - name: config - subPath: opensearch_dashboards.yml +- mountPath: /stackable/opensearch-dashboards/config/tls + name: tls +- mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml + name: config + subPath: opensearch_dashboards.yml podSecurityContext: fsGroup: 1000 diff --git a/docs/modules/opensearch/examples/getting_started/opensearch.yaml b/docs/modules/opensearch/examples/getting_started/opensearch.yaml index 4790255..58ea87b 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch.yaml +++ b/docs/modules/opensearch/examples/getting_started/opensearch.yaml @@ -6,6 +6,44 @@ metadata: spec: image: productVersion: 3.4.0 + clusterConfig: + security: + settings: + config: + managedBy: operator + content: + value: + _meta: + type: config + config_version: 2 + config: + dynamic: + authc: + basic_internal_auth_domain: + description: Authenticate via HTTP Basic against internal users database + http_enabled: true + transport_enabled: true + order: 1 + http_authenticator: + type: basic + challenge: true + authentication_backend: + type: intern + authz: {} + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: initial-opensearch-security-config + key: internal_users.yml + rolesMapping: + managedBy: API + content: + valueFrom: + secretKeyRef: + name: initial-opensearch-security-config + key: roles_mapping.yml nodes: roleConfig: discoveryServiceListenerClass: external-stable @@ -14,18 +52,4 @@ spec: replicas: 3 configOverrides: opensearch.yml: - plugins.security.allow_default_init_securityindex: "true" plugins.security.restapi.roles_enabled: all_access - podOverrides: - spec: - containers: - - name: opensearch - volumeMounts: - - name: security-config - mountPath: /stackable/opensearch/config/opensearch-security - readOnly: true - volumes: - - name: security-config - secret: - secretName: opensearch-security-config - defaultMode: 0o660 diff --git a/docs/modules/opensearch/pages/getting_started/first_steps.adoc b/docs/modules/opensearch/pages/getting_started/first_steps.adoc index 0119d9a..6e72ccf 100644 --- a/docs/modules/opensearch/pages/getting_started/first_steps.adoc +++ b/docs/modules/opensearch/pages/getting_started/first_steps.adoc @@ -2,20 +2,13 @@ Once you have followed the steps in xref:getting_started/installation.adoc[] for the operator and its dependencies, you will now go through the steps to set up and connect to an OpenSearch instance. -== Security plugin configuration +== User configuration -The configuration for the OpenSearch security plugin must be provided in a separate resource, e.g. a Secret: +Let's begin by defining the users with passwords and their corresponding mappings to back-end roles within a Secret: [source,yaml] ---- -include::example$getting_started/opensearch-security-config.yaml[] ----- - -Apply the Secret: - -[source,bash] ----- -include::example$getting_started/getting_started.sh[tag=apply-security-config] +include::example$getting_started/initial-opensearch-security-config.yaml[] ---- The passwords in `internal_users.yml` are hashes using the bcrypt algorithm. @@ -30,9 +23,17 @@ $ htpasswd -nbBC 10 kibanaserver E4kENuEmkqH3jyHC kibanaserver:$2y$10$vPgQ/6ilKDM5utawBqxoR.7euhVQ0qeGl8mPTeKhmFT475WUDrfQS ---- -== Creation of OpenSearch nodes +Now apply the Secret: -OpenSearch nodes must be created as a custom resource; Create a file called `opensearch.yaml`: +[source,bash] +---- +include::example$getting_started/getting_started.sh[tag=apply-security-config] +---- + +== OpenSearch cluster definition + +OpenSearch nodes must be created as a custom resource; +Create a file called `opensearch.yaml`: [source,yaml] ---- @@ -48,8 +49,6 @@ include::example$getting_started/getting_started.sh[tag=apply-cluster] `metadata.name` contains the name of the OpenSearch cluster. -The previously created security plugin configuration must be referenced via `podOverrides`. - You need to wait for the OpenSearch nodes to finish deploying. You can do so with this command: diff --git a/docs/modules/opensearch/pages/reference/discovery.adoc b/docs/modules/opensearch/pages/reference/discovery.adoc index ad57c0f..cde3685 100644 --- a/docs/modules/opensearch/pages/reference/discovery.adoc +++ b/docs/modules/opensearch/pages/reference/discovery.adoc @@ -36,14 +36,14 @@ spec: config: discoveryServiceExposed: true # <5> nodeRoles: - - cluster_manager + - cluster_manager data: config: discoveryServiceExposed: false # <6> nodeRoles: - - ingest - - data - - remote_cluster_client + - ingest + - data + - remote_cluster_client ---- <1> The name of the OpenSearch cluster which is also the name of the created discovery ConfigMap. <2> The namespace of the cluster and the discovery ConfigMap. diff --git a/docs/modules/opensearch/pages/usage-guide/node-roles.adoc b/docs/modules/opensearch/pages/usage-guide/node-roles.adoc index fa215af..c3a0813 100644 --- a/docs/modules/opensearch/pages/usage-guide/node-roles.adoc +++ b/docs/modules/opensearch/pages/usage-guide/node-roles.adoc @@ -11,10 +11,10 @@ The role configuration already defaults to a set of node roles: nodes: config: nodeRoles: - - cluster_manager - - data - - ingest - - remote_cluster_client + - cluster_manager + - data + - ingest + - remote_cluster_client ---- If you deploy a cluster with the following specification, then 3 replicas with the roles `cluster_manager`, `data`, `ingest` and `remote_cluster_client` are deployed: @@ -40,18 +40,18 @@ nodes: cluster-manager: config: nodeRoles: - - cluster_manager + - cluster_manager replicas: 1 coordinating: config: nodeRoles: - - coordinating_only + - coordinating_only replicas: 1 data: config: nodeRoles: - - data - - ingest + - data + - ingest replicas: 2 ---- @@ -65,4 +65,13 @@ The following roles are currently supported by the operator: * `search` * `warm` +The node role `coordinating_only` cannot be combined with other roles. +`nodeRoles: []` also defines a `coordinating_only` node. + +[WARNING] +==== +Do not remove the `data` node role from an existing role group! +Otherwise you have to repurpose the node to another role manually. +==== + We refer to https://docs.opensearch.org/docs/latest/install-and-configure/configuring-opensearch/configuration-system/[the OpenSearch documentation{external-link-icon}^] for an explanation of the roles. diff --git a/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc b/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc index 080405b..eb467ea 100644 --- a/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc +++ b/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc @@ -29,42 +29,42 @@ config: ssl: verificationMode: full # <8> certificateAuthorities: - - /stackable/opensearch-dashboards/config/tls/ca.crt # <9> + - /stackable/opensearch-dashboards/config/tls/ca.crt # <9> opensearch_security: cookie: secure: true # <10> extraEnvs: - - name: OPENSEARCH_HOSTS - valueFrom: - configMapKeyRef: - name: opensearch # <11> - key: OPENSEARCH_HOSTS - - name: OPENSEARCH_PASSWORD - valueFrom: - secretKeyRef: - name: opensearch-credentials - key: kibanaserver # <12> +- name: OPENSEARCH_HOSTS + valueFrom: + configMapKeyRef: + name: opensearch # <11> + key: OPENSEARCH_HOSTS +- name: OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: opensearch-credentials + key: kibanaserver # <12> extraVolumes: - - name: tls # <13> - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch-dashboards - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" +- name: tls # <13> + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=opensearch-dashboards + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" extraVolumeMounts: - - mountPath: /stackable/opensearch-dashboards/config/tls - name: tls - - mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml - name: config # <14> - subPath: opensearch_dashboards.yml +- mountPath: /stackable/opensearch-dashboards/config/tls + name: tls +- mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml + name: config # <14> + subPath: opensearch_dashboards.yml podSecurityContext: fsGroup: 1000 # <15> ---- diff --git a/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc.j2 b/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc.j2 index da964d3..99e6d7f 100644 --- a/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc.j2 +++ b/docs/modules/opensearch/pages/usage-guide/opensearch-dashboards.adoc.j2 @@ -29,42 +29,42 @@ config: ssl: verificationMode: full # <8> certificateAuthorities: - - /stackable/opensearch-dashboards/config/tls/ca.crt # <9> + - /stackable/opensearch-dashboards/config/tls/ca.crt # <9> opensearch_security: cookie: secure: true # <10> extraEnvs: - - name: OPENSEARCH_HOSTS - valueFrom: - configMapKeyRef: - name: opensearch # <11> - key: OPENSEARCH_HOSTS - - name: OPENSEARCH_PASSWORD - valueFrom: - secretKeyRef: - name: opensearch-credentials - key: kibanaserver # <12> +- name: OPENSEARCH_HOSTS + valueFrom: + configMapKeyRef: + name: opensearch # <11> + key: OPENSEARCH_HOSTS +- name: OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: opensearch-credentials + key: kibanaserver # <12> extraVolumes: - - name: tls # <13> - ephemeral: - volumeClaimTemplate: - metadata: - annotations: - secrets.stackable.tech/class: tls - secrets.stackable.tech/scope: service=opensearch-dashboards - spec: - storageClassName: secrets.stackable.tech - accessModes: - - ReadWriteOnce - resources: - requests: - storage: "1" +- name: tls # <13> + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=opensearch-dashboards + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" extraVolumeMounts: - - mountPath: /stackable/opensearch-dashboards/config/tls - name: tls - - mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml - name: config # <14> - subPath: opensearch_dashboards.yml +- mountPath: /stackable/opensearch-dashboards/config/tls + name: tls +- mountPath: /stackable/opensearch-dashboards/config/opensearch_dashboards.yml + name: config # <14> + subPath: opensearch_dashboards.yml podSecurityContext: fsGroup: 1000 # <15> ---- diff --git a/docs/modules/opensearch/pages/usage-guide/security.adoc b/docs/modules/opensearch/pages/usage-guide/security.adoc index d928d13..6ce055f 100644 --- a/docs/modules/opensearch/pages/usage-guide/security.adoc +++ b/docs/modules/opensearch/pages/usage-guide/security.adoc @@ -1,8 +1,152 @@ = Security -:description: Configure TLS encryption for OpenSearch with the Stackable Operator. +:description: Configure the OpenSearch security plugin with the Stackable Operator. + +Security in OpenSearch is managed by the OpenSearch security plugin. +The security plugin can be configured in `spec.clusterConfig.security` and is enabled by default: + +[source,yaml] +---- +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + clusterConfig: + security: + enabled: true +---- + +== Settings + +The configuration of the security plugin is stored in the security index. +When a new cluster is created, the security index is initialized from the following configuration files: + +* https://docs.opensearch.org/latest/security/configuration/yaml/#action_groupsyml[action_groups.yml{external-link-icon}^]: + user-defined action groups +* https://docs.opensearch.org/latest/security/configuration/yaml/#allowlistyml[allowlist.yml{external-link-icon}^]: + list of allowed HTTP endpoints +* https://docs.opensearch.org/latest/security/audit-logs/index/#settings-in-audityml[audit.yml{external-link-icon}^]: + settings for audit logging +* https://docs.opensearch.org/latest/security/configuration/configuration/[config.yml{external-link-icon}^]: + configuration of the security backend +* https://docs.opensearch.org/latest/security/configuration/yaml/#internal_usersyml[internal_users.yml{external-link-icon}^]: + the internal users database +* https://docs.opensearch.org/latest/security/configuration/yaml/#nodes_dnyml[nodes_dn.yml{external-link-icon}^]: + distinguished names (DNs) of nodes to allow communication between nodes and clusters +* https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml[roles.yml{external-link-icon}^]: + definition of roles in the security plugin +* https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml[roles_mapping.yml{external-link-icon}^]: + Role mappings to users or backend roles +* https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml[tenants.yml{external-link-icon}^]: + OpenSearch Dashboards tenants + +These configuration files can be specified in `spec.clusterConfig.security.settings`: + +[source,yaml] +---- +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + clusterConfig: + security: + settings: + actionGroups: ... + allowList: ... + audit: ... + config: ... + internalUsers: ... + nodesDn: ... + roles: ... + rolesMapping: ... + tenants: ... +---- + +If a setting is undefined, then a default configuration is deployed with no permissions. +Therefore, it is okay to only define some settings and leave the others unspecified. + +A setting can be defined either inline, via Secret or ConfigMap: + +[source,yaml] +---- +spec: + clusterConfig: + security: + settings: + config: + managedBy: API + content: + value: # defined inline + _meta: + type: config + config_version: 2 + ... + internalUsers: + managedBy: API + content: + valueFrom: + secretKeyRef: # defined via Secret + name: opensearch-security-config-secret + key: internal_users.yml + rolesMapping: + managedBy: API + content: + valueFrom: + configMapKeyRef: # defined via ConfigMap + name: opensearch-security-config-configmap + key: roles_mapping.yml +---- + +By default, the security settings are only used to initialize the security index: + +[source,yaml] +---- +spec: + clusterConfig: + security: + settings: + config: + managedBy: API + ... +---- + +Later changes are ignored, because usually, the index is managed via the https://docs.opensearch.org/latest/api-reference/security/configuration/index/[security configuration API{external-link-icon}^] and it should not be overridden by the operator. +However, if you prefer to manage some settings in the OpenSearchCluster specification, you can set `managedBy` to `operator`: + +[source,yaml] +---- +spec: + clusterConfig: + security: + settings: + config: + managedBy: operator + ... +---- + +[WARNING] +==== +It is possible to change `managedBy` from `API` to `operator` after the cluster was created, but be aware that all changes made via the API are lost. +==== + +All settings managed by the operator are updated by the role group defined in `spec.clusterConfig.security.managingRoleGroup` which defaults to `security-config`: + +[source,yaml] +---- +spec: + clusterConfig: + security: + managingRoleGroup: security-config +---- + +If this role group is not defined, it will be created by the operator. == TLS +TLS is also managed by the OpenSearch security plugin, therefore TLS is only available if the security plugin was not disabled. 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 and are configured using xref:secret-operator:secretclass.adoc[SecretClasses]. @@ -36,5 +180,34 @@ 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. -Important: The operator sets the configuration `plugins.security.nodes_dn` to `["CN=generated certificate for pod"]` which provides weak authentication between nodes. +[WARNING] +==== +The operator sets the configuration `plugins.security.nodes_dn` to `["CN=generated certificate for pod"]` which provides weak authentication between nodes. If you want to increase security and use certificates which identify the OpenSearch nodes specifically, you must also adapt the `plugins.security.nodes_dn` setting via configOverrides. +==== + +== Disabling security + +The OpenSearch security plugin can be disabled as follows: + +[source,yaml] +---- +--- +apiVersion: opensearch.stackable.tech/v1alpha1 +kind: OpenSearchCluster +metadata: + name: opensearch +spec: + clusterConfig: + security: + enabled: false +---- + +All other security settings as well as the TLS settings are then ignored. + +[WARNING] +==== +If the security plugin was enabled before disabling it, then the security index is exposed to the public. +==== + +OpenSearch Dashboards require an enabled security plugin. From 00b3594ebf131aee87babe71d0baec003bf40e4a Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 2 Mar 2026 16:36:42 +0100 Subject: [PATCH 50/53] Rename allow_list.yml to allowlist.yml --- .../controller/build/role_group_builder.rs | 18 ++++++++--------- rust/operator-binary/src/crd/mod.rs | 2 +- .../kuttl/security-config/11-assert.yaml | 2 +- .../kuttl/security-config/21-assert.yaml | 2 +- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 20 +++++++++---------- 5 files changed, 22 insertions(+), 22 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 7ea7519..4eb332e 100644 --- a/rust/operator-binary/src/controller/build/role_group_builder.rs +++ b/rust/operator-binary/src/controller/build/role_group_builder.rs @@ -1652,7 +1652,7 @@ mod tests { let expected_data = match security_mode { TestSecurityMode::Initializing | TestSecurityMode::Managing => json!({ "action_groups.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"actiongroups\"}}", - "allow_list.yml": "{\"_meta\":{\"config_version\":2,\"type\":\"allowlist\"},\"config\":{\"enabled\":false}}", + "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\":{}}}}", "log4j2.properties": null, @@ -1771,10 +1771,10 @@ mod tests { "subPath": "action_groups.yml" }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/allowlist.yml", "name": "security-config-file-allowlist", "readOnly": true, - "subPath": "allow_list.yml" + "subPath": "allowlist.yml" }, { "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", @@ -2298,10 +2298,10 @@ mod tests { "subPath": "action_groups.yml", }, { - "mountPath": "/stackable/opensearch/config/opensearch-security/allow_list.yml", + "mountPath": "/stackable/opensearch/config/opensearch-security/allowlist.yml", "name": "security-config-file-allowlist", "readOnly": true, - "subPath": "allow_list.yml", + "subPath": "allowlist.yml", }, { "mountPath": "/stackable/opensearch/config/opensearch-security/audit.yml", @@ -2549,9 +2549,9 @@ mod tests { "configMap": { "items": [ { - "key": "allow_list.yml", + "key": "allowlist.yml", "mode": 0o660, - "path": "allow_list.yml" + "path": "allowlist.yml" } ], "name": "my-opensearch-cluster-nodes-default" @@ -2759,9 +2759,9 @@ mod tests { "configMap": { "items": [ { - "key": "allow_list.yml", + "key": "allowlist.yml", "mode": 0o660, - "path": "allow_list.yml" + "path": "allowlist.yml" } ], "name": "my-opensearch-cluster-nodes-default" diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 56bd2ae..dde1eb6 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -629,7 +629,7 @@ impl<'a> IntoIterator for &'a v1alpha1::SecuritySettings { }, ExtendedSecuritySettingsFileType { id: "allowlist", - filename: "allow_list.yml", + filename: "allowlist.yml", managed_by: &self.allow_list.managed_by, content: &self.allow_list.content, }, diff --git a/tests/templates/kuttl/security-config/11-assert.yaml b/tests/templates/kuttl/security-config/11-assert.yaml index 4f0fc5a..11166f9 100644 --- a/tests/templates/kuttl/security-config/11-assert.yaml +++ b/tests/templates/kuttl/security-config/11-assert.yaml @@ -33,7 +33,7 @@ metadata: name: opensearch-nodes-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' + 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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":true}}}}' diff --git a/tests/templates/kuttl/security-config/21-assert.yaml b/tests/templates/kuttl/security-config/21-assert.yaml index 3779548..bffbbbc 100644 --- a/tests/templates/kuttl/security-config/21-assert.yaml +++ b/tests/templates/kuttl/security-config/21-assert.yaml @@ -9,7 +9,7 @@ metadata: name: opensearch-nodes-security-config data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' + 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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{},"http":{"anonymous_auth_enabled":false}}}}' diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 1ab23c9..5e753b0 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -201,10 +201,10 @@ spec: name: security-config-file-actiongroups readOnly: true subPath: action_groups.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allowlist.yml name: security-config-file-allowlist readOnly: true - subPath: allow_list.yml + subPath: allowlist.yml - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml name: security-config-file-audit readOnly: true @@ -360,9 +360,9 @@ spec: - configMap: defaultMode: 420 items: - - key: allow_list.yml + - key: allowlist.yml mode: 432 - path: allow_list.yml + path: allowlist.yml name: opensearch-nodes-cluster-manager name: security-config-file-allowlist - configMap: @@ -674,10 +674,10 @@ spec: name: security-config-file-actiongroups readOnly: true subPath: action_groups.yml - - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allow_list.yml + - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/allowlist.yml name: security-config-file-allowlist readOnly: true - subPath: allow_list.yml + subPath: allowlist.yml - mountPath: {{ test_scenario['values']['opensearch_home'] }}/config/opensearch-security/audit.yml name: security-config-file-audit readOnly: true @@ -833,9 +833,9 @@ spec: - configMap: defaultMode: 420 items: - - key: allow_list.yml + - key: allowlist.yml mode: 432 - path: allow_list.yml + path: allowlist.yml name: opensearch-nodes-data name: security-config-file-allowlist - configMap: @@ -951,7 +951,7 @@ metadata: name: opensearch data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' + 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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{}}}}' internal_users.yml: '{"_meta":{"config_version":2,"type":"internalusers"},"admin":{"backend_roles":["admin"],"description":"OpenSearch admin user","hash":"$2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e","reserved":true}}' @@ -1001,7 +1001,7 @@ metadata: name: opensearch data: action_groups.yml: '{"_meta":{"config_version":2,"type":"actiongroups"}}' - allow_list.yml: '{"_meta":{"config_version":2,"type":"allowlist"},"config":{"enabled":false}}' + 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":{"basic_internal_auth_domain":{"authentication_backend":{"type":"intern"},"description":"Authenticate via HTTP Basic against internal users database","http_authenticator":{"challenge":true,"type":"basic"},"http_enabled":true,"order":1,"transport_enabled":true}},"authz":{}}}}' internal_users.yml: '{"_meta":{"config_version":2,"type":"internalusers"},"admin":{"backend_roles":["admin"],"description":"OpenSearch admin user","hash":"$2y$10$xRtHZFJ9QhG9GcYhRpAGpufCZYsk//nxsuel5URh0GWEBgmiI4Q/e","reserved":true}}' From a16b282c580b0518900775e00a78c926da9496d3 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Mon, 2 Mar 2026 16:58:14 +0100 Subject: [PATCH 51/53] test(smoke): Fix test assertion --- .../examples/getting_started/opensearch-dashboards-values.yaml | 2 +- .../getting_started/opensearch-dashboards-values.yaml.j2 | 2 +- tests/templates/kuttl/smoke/10-assert.yaml.j2 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml index 5565626..9dcac61 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml +++ b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml @@ -43,7 +43,7 @@ extraVolumes: spec: storageClassName: secrets.stackable.tech accessModes: - - ReadWriteOnce + - ReadWriteOnce resources: requests: storage: "1" diff --git a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 index 07da0ac..d4e8abe 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 +++ b/docs/modules/opensearch/examples/getting_started/opensearch-dashboards-values.yaml.j2 @@ -43,7 +43,7 @@ extraVolumes: spec: storageClassName: secrets.stackable.tech accessModes: - - ReadWriteOnce + - ReadWriteOnce resources: requests: storage: "1" diff --git a/tests/templates/kuttl/smoke/10-assert.yaml.j2 b/tests/templates/kuttl/smoke/10-assert.yaml.j2 index 5e753b0..64feb74 100644 --- a/tests/templates/kuttl/smoke/10-assert.yaml.j2 +++ b/tests/templates/kuttl/smoke/10-assert.yaml.j2 @@ -608,7 +608,7 @@ spec: apiVersion: v1 fieldPath: metadata.name - name: node.roles - value: ingest,data,remote_cluster_client + value: data,ingest,remote_cluster_client - name: transport.publish_host # value: $(_POD_NAME).opensearch-nodes-data-headless.$NAMESPACE.svc.cluster.local imagePullPolicy: IfNotPresent From 2bbcfc89060138ea0630ef969d30f669cc2b2d3c Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 3 Mar 2026 09:37:08 +0100 Subject: [PATCH 52/53] doc: Improve the security documentation --- .../opensearch/pages/usage-guide/security.adoc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/modules/opensearch/pages/usage-guide/security.adoc b/docs/modules/opensearch/pages/usage-guide/security.adoc index 6ce055f..75ec9ca 100644 --- a/docs/modules/opensearch/pages/usage-guide/security.adoc +++ b/docs/modules/opensearch/pages/usage-guide/security.adoc @@ -37,7 +37,7 @@ When a new cluster is created, the security index is initialized from the follow * https://docs.opensearch.org/latest/security/configuration/yaml/#rolesyml[roles.yml{external-link-icon}^]: definition of roles in the security plugin * https://docs.opensearch.org/latest/security/configuration/yaml/#roles_mappingyml[roles_mapping.yml{external-link-icon}^]: - Role mappings to users or backend roles + role mappings to users or backend roles * https://docs.opensearch.org/latest/security/configuration/yaml/#tenantsyml[tenants.yml{external-link-icon}^]: OpenSearch Dashboards tenants @@ -65,7 +65,7 @@ spec: tenants: ... ---- -If a setting is undefined, then a default configuration is deployed with no permissions. +If any setting remains undefined, a default configuration will be deployed with no permissions. Therefore, it is okay to only define some settings and leave the others unspecified. A setting can be defined either inline, via Secret or ConfigMap: @@ -129,7 +129,7 @@ spec: [WARNING] ==== -It is possible to change `managedBy` from `API` to `operator` after the cluster was created, but be aware that all changes made via the API are lost. +While it is possible to change `managedBy` from `API` to `operator` after cluster creation, be cautious as this will discard all API-made changes. ==== All settings managed by the operator are updated by the role group defined in `spec.clusterConfig.security.managingRoleGroup` which defaults to `security-config`: @@ -180,10 +180,11 @@ 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. -[WARNING] +[IMPORTANT] ==== The operator sets the configuration `plugins.security.nodes_dn` to `["CN=generated certificate for pod"]` which provides weak authentication between nodes. -If you want to increase security and use certificates which identify the OpenSearch nodes specifically, you must also adapt the `plugins.security.nodes_dn` setting via configOverrides. +For enhanced security, use certificates that uniquely identify the OpenSearch nodes. +In this case, you must also adapt the `plugins.security.nodes_dn` setting via configOverrides. ==== == Disabling security @@ -203,11 +204,11 @@ spec: enabled: false ---- -All other security settings as well as the TLS settings are then ignored. +Once disabled, all other security and TLS settings will be disregarded. [WARNING] ==== -If the security plugin was enabled before disabling it, then the security index is exposed to the public. +If the security plugin was previously enabled, the security index will become accessible like any other indices. ==== -OpenSearch Dashboards require an enabled security plugin. +OpenSearch Dashboards require the security plugin to be enabled. From 539bee3f54a5dc4aa241186afe00e205f2cdb023 Mon Sep 17 00:00:00 2001 From: Siegfried Weber Date: Tue, 3 Mar 2026 14:36:27 +0100 Subject: [PATCH 53/53] doc: Remove deprecation warning for the opensearch-operator --- .../opensearch/examples/getting_started/opensearch.yaml | 2 +- docs/modules/opensearch/pages/index.adoc | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/modules/opensearch/examples/getting_started/opensearch.yaml b/docs/modules/opensearch/examples/getting_started/opensearch.yaml index 58ea87b..e77e9d7 100644 --- a/docs/modules/opensearch/examples/getting_started/opensearch.yaml +++ b/docs/modules/opensearch/examples/getting_started/opensearch.yaml @@ -10,7 +10,7 @@ spec: security: settings: config: - managedBy: operator + managedBy: API content: value: _meta: diff --git a/docs/modules/opensearch/pages/index.adoc b/docs/modules/opensearch/pages/index.adoc index 8292090..6355221 100644 --- a/docs/modules/opensearch/pages/index.adoc +++ b/docs/modules/opensearch/pages/index.adoc @@ -13,11 +13,6 @@ * {feature-tracker}[Feature Tracker{external-link-icon}^] * {crd}[CRD documentation{external-link-icon}^] -[WARNING] -==== -This operator is experimental, and subject to change. -==== - The Stackable operator for {opensearch}[OpenSearch{external-link-icon}^] deploys and manages OpenSearch clusters on Kubernetes. OpenSearch is a powerful search and analytics engine built on Apache Lucene. This operator helps you manage your OpenSearch instances on Kubernetes efficiently.