diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3047cf..f5750da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. - `EOS_CHECK_MODE` (`--eos-check-mode`) to set the EoS check mode. Currently, only "offline" is supported. - `EOS_INTERVAL` (`--eos-interval`) to set the interval in which the operator checks if it is EoS. - `EOS_DISABLED` (`--eos-disabled`) to disable the EoS checker completely. +- Add support for OPA with TLS enabled ([#863]). ### Changed @@ -33,6 +34,7 @@ All notable changes to this project will be documented in this file. [#855]: https://github.com/stackabletech/nifi-operator/pull/855 [#859]: https://github.com/stackabletech/nifi-operator/pull/859 [#860]: https://github.com/stackabletech/nifi-operator/pull/860 +[#863]: https://github.com/stackabletech/nifi-operator/pull/863 ## [25.7.0] - 2025-07-23 diff --git a/rust/operator-binary/src/config/jvm.rs b/rust/operator-binary/src/config/jvm.rs index 7960a2e9..a570db6d 100644 --- a/rust/operator-binary/src/config/jvm.rs +++ b/rust/operator-binary/src/config/jvm.rs @@ -7,6 +7,10 @@ use stackable_operator::{ use crate::{ config::{JVM_SECURITY_PROPERTIES_FILE, NIFI_CONFIG_DIRECTORY}, crd::{NifiConfig, NifiConfigFragment, NifiNodeRoleConfig}, + security::{ + authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD}, + authorization::NifiAuthorizationConfig, + }, }; // Part of memory resources allocated for Java heap @@ -31,6 +35,7 @@ pub fn build_merged_jvm_config( merged_config: &NifiConfig, role: &Role, role_group: &str, + authorization_config: Option<&NifiAuthorizationConfig>, ) -> Result { let heap_size = MemoryQuantity::try_from( merged_config @@ -47,7 +52,7 @@ pub fn build_merged_jvm_config( .format_for_java() .context(InvalidMemoryConfigSnafu)?; - let jvm_args = vec![ + let mut jvm_args = vec![ // Heap settings format!("-Xmx{java_heap}"), format!("-Xms{java_heap}"), @@ -79,6 +84,27 @@ pub fn build_merged_jvm_config( ), ]; + // Add JVM truststore properties when OPA TLS is enabled + // This ensures that the OPA authorizer can verify the OPA server's TLS certificate + // + // Note: JVM system properties are currently the correct way to configure TLS for the OPA + // plugin. The NiFi OPA authorizer uses the Styra OPA Java SDK, which internally creates a + // standard Java HttpClient without exposed SSL configuration options, but the HttpClient + // respects these JVM-wide SSL system properties. So there is no plugin-level configuration + // available for truststore settings. This was last checked for version 1.7.0 of the Styra + // OPA Java SDK. + if let Some(authz_config) = authorization_config { + if authz_config.has_opa_tls() { + jvm_args.push(format!( + "-Djavax.net.ssl.trustStore={STACKABLE_SERVER_TLS_DIR}/truststore.p12" + )); + jvm_args.push(format!( + "-Djavax.net.ssl.trustStorePassword={STACKABLE_TLS_STORE_PASSWORD}" + )); + jvm_args.push("-Djavax.net.ssl.trustStoreType=pkcs12".to_owned()); + } + } + let operator_generated = JvmArgumentOverrides::new_with_only_additions(jvm_args); role.get_merged_jvm_argument_overrides(role_group, &operator_generated) .context(MergeJvmArgumentOverridesSnafu) diff --git a/rust/operator-binary/src/config/mod.rs b/rust/operator-binary/src/config/mod.rs index 3ddfa6da..1c3ed4be 100644 --- a/rust/operator-binary/src/config/mod.rs +++ b/rust/operator-binary/src/config/mod.rs @@ -115,6 +115,7 @@ pub fn build_bootstrap_conf( overrides: BTreeMap, role: &Role, role_group: &str, + authorization_config: Option<&crate::security::authorization::NifiAuthorizationConfig>, ) -> Result { let mut bootstrap = BTreeMap::new(); // Java command to use when running NiFi @@ -129,7 +130,8 @@ pub fn build_bootstrap_conf( bootstrap.extend(graceful_shutdown_config_properties(merged_config)); let merged_jvm_config = - build_merged_jvm_config(merged_config, role, role_group).context(InvalidJVMConfigSnafu)?; + build_merged_jvm_config(merged_config, role, role_group, authorization_config) + .context(InvalidJVMConfigSnafu)?; for (index, argument) in merged_jvm_config .effective_jvm_config_after_merging() @@ -917,6 +919,6 @@ mod tests { let role = nifi.spec.nodes.as_ref().unwrap(); let merged_config = nifi.merged_config(&nifi_role, "default").unwrap(); - build_bootstrap_conf(&merged_config, BTreeMap::new(), role, "default").unwrap() + build_bootstrap_conf(&merged_config, BTreeMap::new(), role, "default", None).unwrap() } } diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index c7039148..124f83db 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -24,7 +24,7 @@ use stackable_operator::{ container::ContainerBuilder, resources::ResourceRequirementsBuilder, security::PodSecurityContextBuilder, - volume::{ListenerOperatorVolumeSourceBuilderError, SecretFormat}, + volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder}, }, }, client::Client, @@ -106,7 +106,7 @@ use crate::{ AUTHORIZERS_XML_FILE_NAME, LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME, NifiAuthenticationConfig, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, }, - authorization::NifiAuthorizationConfig, + authorization::{NifiAuthorizationConfig, OPA_TLS_MOUNT_PATH, OPA_TLS_VOLUME_NAME}, build_tls_volume, check_or_generate_oidc_admin_password, check_or_generate_sensitive_key, tls::{KEYSTORE_NIFI_CONTAINER_MOUNT, KEYSTORE_VOLUME_NAME, TRUSTSTORE_VOLUME_NAME}, }, @@ -152,11 +152,6 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to fetch deployed StatefulSets"))] - FetchStatefulsets { - source: stackable_operator::client::Error, - }, - #[snafu(display("failed to update status"))] StatusUpdate { source: stackable_operator::client::Error, @@ -339,10 +334,11 @@ pub enum Error { #[snafu(display("Failed to determine the state of the version upgrade procedure"))] ClusterVersionUpdateState { source: upgrade::Error }, - #[snafu(display("failed to build listener volume"))] - BuildListenerVolume { - source: ListenerOperatorVolumeSourceBuilderError, + #[snafu(display("failed to build OPA TLS certificate volume"))] + OpaTlsCertSecretClassVolumeBuild { + source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError, }, + #[snafu(display("failed to apply group listener"))] ApplyGroupListener { source: stackable_operator::cluster_resources::Error, @@ -455,8 +451,16 @@ pub async fn reconcile_nifi( .context(SecuritySnafu)?; } - let authorization_config = - NifiAuthorizationConfig::from(&nifi.spec.cluster_config.authorization); + let authorization_config = NifiAuthorizationConfig::from( + &nifi.spec.cluster_config.authorization, + client, + nifi.metadata + .namespace + .as_deref() + .context(ObjectHasNoNamespaceSnafu)?, + ) + .await + .context(InvalidNifiAuthorizationConfigSnafu)?; let (rbac_sa, rbac_rolebinding) = build_rbac_resources( nifi, @@ -770,6 +774,7 @@ async fn build_node_rolegroup_config_map( .clone(), role, &rolegroup.role_group, + Some(authorization_config), ) .context(BootstrapConfigSnafu)?, ) @@ -978,6 +983,14 @@ async fn build_node_rolegroup_statefulset( .as_slice(), ); + // Add OPA certificate to truststore if OPA TLS is enabled + if authorization_config.has_opa_tls() { + prepare_args.extend(vec![ + "echo Importing OPA CA certificate to truststore".to_string(), + format!("keytool -importcert -file {OPA_TLS_MOUNT_PATH}/ca.crt -keystore {STACKABLE_SERVER_TLS_DIR}/truststore.p12 -storepass {STACKABLE_TLS_STORE_PASSWORD} -alias opa-ca -noprompt"), + ]); + } + prepare_args.extend(vec![ "export LISTENER_DEFAULT_ADDRESS=$(cat /stackable/listener/default-address/address)" .to_string(), @@ -1061,6 +1074,12 @@ async fn build_node_rolegroup_statefulset( .build(), ); + if authorization_config.has_opa_tls() { + container_prepare + .add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH) + .context(AddVolumeMountSnafu)?; + } + let nifi_container_name = Container::Nifi.to_string(); let mut container_nifi_builder = ContainerBuilder::new(&nifi_container_name).with_context(|_| { @@ -1083,6 +1102,13 @@ async fn build_node_rolegroup_statefulset( create_vector_shutdown_file_command = create_vector_shutdown_file_command(STACKABLE_LOG_DIR), }]; + + if authorization_config.has_opa_tls() { + container_nifi_builder + .add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH) + .context(AddVolumeMountSnafu)?; + } + let container_nifi = container_nifi_builder .image_from_product_image(resolved_product_image) .command(vec![ @@ -1366,7 +1392,27 @@ async fn build_node_rolegroup_statefulset( ) .context(AddVolumeSnafu)? .add_empty_dir_volume(TRUSTSTORE_VOLUME_NAME, None) - .context(AddVolumeSnafu)? + .context(AddVolumeSnafu)?; + + if let NifiAuthorizationConfig::Opa { + secret_class: Some(secret_class), + .. + } = authorization_config + { + pod_builder + .add_volume( + VolumeBuilder::new(OPA_TLS_VOLUME_NAME) + .ephemeral( + SecretOperatorVolumeSourceBuilder::new(secret_class) + .build() + .context(OpaTlsCertSecretClassVolumeBuildSnafu)?, + ) + .build(), + ) + .context(AddVolumeSnafu)?; + } + + pod_builder .add_volume(Volume { name: "sensitiveproperty".to_string(), secret: Some(SecretVolumeSource { diff --git a/rust/operator-binary/src/security/authorization.rs b/rust/operator-binary/src/security/authorization.rs index 8f0575c7..6c46f942 100644 --- a/rust/operator-binary/src/security/authorization.rs +++ b/rust/operator-binary/src/security/authorization.rs @@ -1,19 +1,30 @@ use indoc::{formatdoc, indoc}; -use snafu::{OptionExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ + client::Client, crd::authentication::ldap, - k8s_openapi::api::core::v1::{ConfigMapKeySelector, EnvVar, EnvVarSource}, + k8s_openapi::api::core::v1::{ConfigMap, ConfigMapKeySelector, EnvVar, EnvVarSource}, }; use super::authentication::NifiAuthenticationConfig; use crate::crd::NifiAuthorization; +pub const OPA_TLS_VOLUME_NAME: &str = "opa-tls"; +pub const OPA_TLS_MOUNT_PATH: &str = "/stackable/opa_tls"; + #[derive(Snafu, Debug)] pub enum Error { #[snafu(display( "The LDAP AuthenticationClass is missing the bind credentials. Currently the NiFi operator only supports connecting to LDAP servers using bind credentials" ))] LdapAuthenticationClassMissingBindCredentials {}, + + #[snafu(display("Failed to fetch OPA ConfigMap {configmap_name}"))] + FetchOpaConfigMap { + source: stackable_operator::client::Error, + configmap_name: String, + namespace: String, + }, } pub enum NifiAuthorizationConfig { @@ -21,23 +32,49 @@ pub enum NifiAuthorizationConfig { configmap_name: String, cache_entry_time_to_live_secs: u64, cache_max_entries: u32, + secret_class: Option, }, Default, } impl NifiAuthorizationConfig { - pub fn from(nifi_authorization: &Option) -> Self { - match nifi_authorization { + pub async fn from( + nifi_authorization: &Option, + client: &Client, + namespace: &str, + ) -> Result { + let config = match nifi_authorization { Some(authorization_config) => match authorization_config.opa.clone() { - Some(opa_config) => NifiAuthorizationConfig::Opa { - configmap_name: opa_config.opa.config_map_name, - cache_entry_time_to_live_secs: opa_config.cache.entry_time_to_live.as_secs(), - cache_max_entries: opa_config.cache.max_entries, - }, + Some(opa_config) => { + let configmap_name = opa_config.opa.config_map_name.clone(); + + // Resolve the secret class from the ConfigMap + let secret_class = client + .get::(&configmap_name, namespace) + .await + .with_context(|_| FetchOpaConfigMapSnafu { + configmap_name: configmap_name.clone(), + namespace: namespace.to_string(), + })? + .data + .and_then(|mut data| data.remove("OPA_SECRET_CLASS")); + + NifiAuthorizationConfig::Opa { + configmap_name, + cache_entry_time_to_live_secs: opa_config + .cache + .entry_time_to_live + .as_secs(), + cache_max_entries: opa_config.cache.max_entries, + secret_class, + } + } None => NifiAuthorizationConfig::Default, }, None => NifiAuthorizationConfig::Default, - } + }; + + Ok(config) } pub fn get_authorizers_config( @@ -152,4 +189,14 @@ impl NifiAuthorizationConfig { NifiAuthorizationConfig::Default => vec![], } } + + pub fn has_opa_tls(&self) -> bool { + matches!( + self, + NifiAuthorizationConfig::Opa { + secret_class: Some(_), + .. + } + ) + } } diff --git a/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 b/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 index 8b8f9cf5..5febeb1d 100644 --- a/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 +++ b/tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2 @@ -5,6 +5,19 @@ commands: - script: | kubectl apply -n $NAMESPACE -f - <