Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
28 changes: 27 additions & 1 deletion rust/operator-binary/src/config/jvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +35,7 @@ pub fn build_merged_jvm_config(
merged_config: &NifiConfig,
role: &Role<NifiConfigFragment, NifiNodeRoleConfig, JavaCommonConfig>,
role_group: &str,
authorization_config: Option<&NifiAuthorizationConfig>,
) -> Result<JvmArgumentOverrides, Error> {
let heap_size = MemoryQuantity::try_from(
merged_config
Expand All @@ -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}"),
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions rust/operator-binary/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub fn build_bootstrap_conf(
overrides: BTreeMap<String, String>,
role: &Role<NifiConfigFragment, NifiNodeRoleConfig, JavaCommonConfig>,
role_group: &str,
authorization_config: Option<&crate::security::authorization::NifiAuthorizationConfig>,
) -> Result<String, Error> {
let mut bootstrap = BTreeMap::new();
// Java command to use when running NiFi
Expand All @@ -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()
Expand Down Expand Up @@ -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()
}
}
72 changes: 59 additions & 13 deletions rust/operator-binary/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use stackable_operator::{
container::ContainerBuilder,
resources::ResourceRequirementsBuilder,
security::PodSecurityContextBuilder,
volume::{ListenerOperatorVolumeSourceBuilderError, SecretFormat},
volume::{SecretFormat, SecretOperatorVolumeSourceBuilder, VolumeBuilder},
},
},
client::Client,
Expand Down Expand Up @@ -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},
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -770,6 +774,7 @@ async fn build_node_rolegroup_config_map(
.clone(),
role,
&rolegroup.role_group,
Some(authorization_config),
)
.context(BootstrapConfigSnafu)?,
)
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(|_| {
Expand All @@ -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![
Expand Down Expand Up @@ -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 {
Expand Down
67 changes: 57 additions & 10 deletions rust/operator-binary/src/security/authorization.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,80 @@
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 {
Opa {
configmap_name: String,
cache_entry_time_to_live_secs: u64,
cache_max_entries: u32,
secret_class: Option<String>,
},
Default,
}

impl NifiAuthorizationConfig {
pub fn from(nifi_authorization: &Option<NifiAuthorization>) -> Self {
match nifi_authorization {
pub async fn from(
nifi_authorization: &Option<NifiAuthorization>,
client: &Client,
namespace: &str,
) -> Result<Self, Error> {
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>(&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(
Expand Down Expand Up @@ -152,4 +189,14 @@ impl NifiAuthorizationConfig {
NifiAuthorizationConfig::Default => vec![],
}
}

pub fn has_opa_tls(&self) -> bool {
matches!(
self,
NifiAuthorizationConfig::Opa {
secret_class: Some(_),
..
}
)
}
}
15 changes: 15 additions & 0 deletions tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ commands:
- script: |
kubectl apply -n $NAMESPACE -f - <<EOF
---
apiVersion: secrets.stackable.tech/v1alpha1
kind: SecretClass
metadata:
name: opa-tls-$NAMESPACE
spec:
backend:
autoTls:
ca:
autoGenerate: true
secret:
name: opa-tls-ca
namespace: $NAMESPACE
---
apiVersion: opa.stackable.tech/v1alpha1
kind: OpaCluster
metadata:
Expand All @@ -14,6 +27,8 @@ commands:
productVersion: "{{ test_scenario['values']['opa-l'] }}"
pullPolicy: IfNotPresent
clusterConfig:
tls:
serverSecretClass: opa-tls-$NAMESPACE
userInfo:
backend:
keycloak:
Expand Down
Loading