Skip to content

Commit 4d81a5a

Browse files
committed
feat: support OPA with TLS
1 parent 480fd9f commit 4d81a5a

5 files changed

Lines changed: 158 additions & 27 deletions

File tree

rust/operator-binary/src/config/jvm.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ use stackable_operator::{
77
use crate::{
88
config::{JVM_SECURITY_PROPERTIES_FILE, NIFI_CONFIG_DIRECTORY},
99
crd::{NifiConfig, NifiConfigFragment, NifiNodeRoleConfig},
10+
security::{
11+
authentication::{STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD},
12+
authorization::NifiAuthorizationConfig,
13+
},
1014
};
1115

1216
// Part of memory resources allocated for Java heap
@@ -31,6 +35,7 @@ pub fn build_merged_jvm_config(
3135
merged_config: &NifiConfig,
3236
role: &Role<NifiConfigFragment, NifiNodeRoleConfig, JavaCommonConfig>,
3337
role_group: &str,
38+
authorization_config: Option<&NifiAuthorizationConfig>,
3439
) -> Result<JvmArgumentOverrides, Error> {
3540
let heap_size = MemoryQuantity::try_from(
3641
merged_config
@@ -47,7 +52,7 @@ pub fn build_merged_jvm_config(
4752
.format_for_java()
4853
.context(InvalidMemoryConfigSnafu)?;
4954

50-
let jvm_args = vec![
55+
let mut jvm_args = vec![
5156
// Heap settings
5257
format!("-Xmx{java_heap}"),
5358
format!("-Xms{java_heap}"),
@@ -79,6 +84,23 @@ pub fn build_merged_jvm_config(
7984
),
8085
];
8186

87+
// Add JVM truststore properties when OPA TLS is enabled
88+
// This ensures that the OPA authorizer can verify the OPA server's TLS certificate
89+
//
90+
// Note: JVM system properties are currently the correct way to configure TLS for the OPA
91+
// plugin. The NiFi OPA authorizer uses the Styra OPA Java SDK, which internally creates a
92+
// standard Java HttpClient without exposed SSL configuration options, but the HttpClient
93+
// respects these JVM-wide SSL system properties. So there is no plugin-level configuration
94+
// available for truststore settings. This was last checked for version 1.7.0 of the Styra
95+
// OPA Java SDK.
96+
if let Some(authz_config) = authorization_config {
97+
if authz_config.has_opa_tls() {
98+
jvm_args.push(format!("-Djavax.net.ssl.trustStore={STACKABLE_SERVER_TLS_DIR}/truststore.p12"));
99+
jvm_args.push(format!("-Djavax.net.ssl.trustStorePassword={STACKABLE_TLS_STORE_PASSWORD}"));
100+
jvm_args.push("-Djavax.net.ssl.trustStoreType=pkcs12".to_owned());
101+
}
102+
}
103+
82104
let operator_generated = JvmArgumentOverrides::new_with_only_additions(jvm_args);
83105
role.get_merged_jvm_argument_overrides(role_group, &operator_generated)
84106
.context(MergeJvmArgumentOverridesSnafu)

rust/operator-binary/src/config/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ pub fn build_bootstrap_conf(
115115
overrides: BTreeMap<String, String>,
116116
role: &Role<NifiConfigFragment, NifiNodeRoleConfig, JavaCommonConfig>,
117117
role_group: &str,
118+
authorization_config: Option<&crate::security::authorization::NifiAuthorizationConfig>,
118119
) -> Result<String, Error> {
119120
let mut bootstrap = BTreeMap::new();
120121
// Java command to use when running NiFi
@@ -129,7 +130,7 @@ pub fn build_bootstrap_conf(
129130
bootstrap.extend(graceful_shutdown_config_properties(merged_config));
130131

131132
let merged_jvm_config =
132-
build_merged_jvm_config(merged_config, role, role_group).context(InvalidJVMConfigSnafu)?;
133+
build_merged_jvm_config(merged_config, role, role_group, authorization_config).context(InvalidJVMConfigSnafu)?;
133134

134135
for (index, argument) in merged_jvm_config
135136
.effective_jvm_config_after_merging()
@@ -917,6 +918,6 @@ mod tests {
917918
let role = nifi.spec.nodes.as_ref().unwrap();
918919
let merged_config = nifi.merged_config(&nifi_role, "default").unwrap();
919920

920-
build_bootstrap_conf(&merged_config, BTreeMap::new(), role, "default").unwrap()
921+
build_bootstrap_conf(&merged_config, BTreeMap::new(), role, "default", None).unwrap()
921922
}
922923
}

rust/operator-binary/src/controller.rs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use stackable_operator::{
2424
container::ContainerBuilder,
2525
resources::ResourceRequirementsBuilder,
2626
security::PodSecurityContextBuilder,
27-
volume::{ListenerOperatorVolumeSourceBuilderError, SecretFormat},
27+
volume::{
28+
ListenerOperatorVolumeSourceBuilderError, SecretFormat,
29+
SecretOperatorVolumeSourceBuilder, VolumeBuilder,
30+
},
2831
},
2932
},
3033
client::Client,
@@ -106,7 +109,7 @@ use crate::{
106109
AUTHORIZERS_XML_FILE_NAME, LOGIN_IDENTITY_PROVIDERS_XML_FILE_NAME,
107110
NifiAuthenticationConfig, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD,
108111
},
109-
authorization::NifiAuthorizationConfig,
112+
authorization::{NifiAuthorizationConfig, OPA_TLS_MOUNT_PATH, OPA_TLS_VOLUME_NAME},
110113
build_tls_volume, check_or_generate_oidc_admin_password, check_or_generate_sensitive_key,
111114
tls::{KEYSTORE_NIFI_CONTAINER_MOUNT, KEYSTORE_VOLUME_NAME, TRUSTSTORE_VOLUME_NAME},
112115
},
@@ -343,6 +346,12 @@ pub enum Error {
343346
BuildListenerVolume {
344347
source: ListenerOperatorVolumeSourceBuilderError,
345348
},
349+
350+
#[snafu(display("failed to build OPA TLS certificate volume"))]
351+
OpaTlsCertSecretClassVolumeBuild {
352+
source: stackable_operator::builder::pod::volume::SecretOperatorVolumeSourceBuilderError,
353+
},
354+
346355
#[snafu(display("failed to apply group listener"))]
347356
ApplyGroupListener {
348357
source: stackable_operator::cluster_resources::Error,
@@ -455,8 +464,16 @@ pub async fn reconcile_nifi(
455464
.context(SecuritySnafu)?;
456465
}
457466

458-
let authorization_config =
459-
NifiAuthorizationConfig::from(&nifi.spec.cluster_config.authorization);
467+
let authorization_config = NifiAuthorizationConfig::from(
468+
&nifi.spec.cluster_config.authorization,
469+
client,
470+
nifi.metadata
471+
.namespace
472+
.as_deref()
473+
.context(ObjectHasNoNamespaceSnafu)?,
474+
)
475+
.await
476+
.context(InvalidNifiAuthorizationConfigSnafu)?;
460477

461478
let (rbac_sa, rbac_rolebinding) = build_rbac_resources(
462479
nifi,
@@ -770,6 +787,7 @@ async fn build_node_rolegroup_config_map(
770787
.clone(),
771788
role,
772789
&rolegroup.role_group,
790+
Some(authorization_config),
773791
)
774792
.context(BootstrapConfigSnafu)?,
775793
)
@@ -978,6 +996,14 @@ async fn build_node_rolegroup_statefulset(
978996
.as_slice(),
979997
);
980998

999+
// Add OPA certificate to truststore if OPA TLS is enabled
1000+
if authorization_config.has_opa_tls() {
1001+
prepare_args.extend(vec![
1002+
"echo Importing OPA CA certificate to truststore".to_string(),
1003+
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"),
1004+
]);
1005+
}
1006+
9811007
prepare_args.extend(vec![
9821008
"export LISTENER_DEFAULT_ADDRESS=$(cat /stackable/listener/default-address/address)"
9831009
.to_string(),
@@ -1051,15 +1077,22 @@ async fn build_node_rolegroup_statefulset(
10511077
.add_volume_mount(TRUSTSTORE_VOLUME_NAME, STACKABLE_SERVER_TLS_DIR)
10521078
.context(AddVolumeMountSnafu)?
10531079
.add_volume_mount(LISTENER_VOLUME_NAME, LISTENER_VOLUME_DIR)
1054-
.context(AddVolumeMountSnafu)?
1055-
.resources(
1056-
ResourceRequirementsBuilder::new()
1057-
.with_cpu_request("500m")
1058-
.with_cpu_limit("2000m")
1059-
.with_memory_request("4096Mi")
1060-
.with_memory_limit("4096Mi")
1061-
.build(),
1062-
);
1080+
.context(AddVolumeMountSnafu)?;
1081+
1082+
if authorization_config.has_opa_tls() {
1083+
container_prepare
1084+
.add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH)
1085+
.context(AddVolumeMountSnafu)?;
1086+
}
1087+
1088+
container_prepare.resources(
1089+
ResourceRequirementsBuilder::new()
1090+
.with_cpu_request("500m")
1091+
.with_cpu_limit("2000m")
1092+
.with_memory_request("4096Mi")
1093+
.with_memory_limit("4096Mi")
1094+
.build(),
1095+
);
10631096

10641097
let nifi_container_name = Container::Nifi.to_string();
10651098
let mut container_nifi_builder =
@@ -1083,6 +1116,13 @@ async fn build_node_rolegroup_statefulset(
10831116
create_vector_shutdown_file_command =
10841117
create_vector_shutdown_file_command(STACKABLE_LOG_DIR),
10851118
}];
1119+
1120+
if authorization_config.has_opa_tls() {
1121+
container_nifi_builder
1122+
.add_volume_mount(OPA_TLS_VOLUME_NAME, OPA_TLS_MOUNT_PATH)
1123+
.context(AddVolumeMountSnafu)?;
1124+
}
1125+
10861126
let container_nifi = container_nifi_builder
10871127
.image_from_product_image(resolved_product_image)
10881128
.command(vec![
@@ -1366,7 +1406,22 @@ async fn build_node_rolegroup_statefulset(
13661406
)
13671407
.context(AddVolumeSnafu)?
13681408
.add_empty_dir_volume(TRUSTSTORE_VOLUME_NAME, None)
1369-
.context(AddVolumeSnafu)?
1409+
.context(AddVolumeSnafu)?;
1410+
1411+
if let NifiAuthorizationConfig::Opa { secret_class: Some(secret_class), .. } = authorization_config
1412+
{
1413+
pod_builder
1414+
.add_volume(VolumeBuilder::new(OPA_TLS_VOLUME_NAME)
1415+
.ephemeral(
1416+
SecretOperatorVolumeSourceBuilder::new(secret_class)
1417+
.build()
1418+
.context(OpaTlsCertSecretClassVolumeBuildSnafu)?,
1419+
)
1420+
.build())
1421+
.context(AddVolumeSnafu)?;
1422+
}
1423+
1424+
pod_builder
13701425
.add_volume(Volume {
13711426
name: "sensitiveproperty".to_string(),
13721427
secret: Some(SecretVolumeSource {

rust/operator-binary/src/security/authorization.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,77 @@
11
use indoc::{formatdoc, indoc};
2-
use snafu::{OptionExt, Snafu};
2+
use snafu::{OptionExt, ResultExt, Snafu};
33
use stackable_operator::{
4+
client::Client,
45
crd::authentication::ldap,
5-
k8s_openapi::api::core::v1::{ConfigMapKeySelector, EnvVar, EnvVarSource},
6+
k8s_openapi::api::core::v1::{ConfigMap, ConfigMapKeySelector, EnvVar, EnvVarSource},
67
};
78

89
use super::authentication::NifiAuthenticationConfig;
910
use crate::crd::NifiAuthorization;
1011

12+
pub const OPA_TLS_VOLUME_NAME: &str = "opa-tls";
13+
pub const OPA_TLS_MOUNT_PATH: &str = "/stackable/opa_tls";
14+
1115
#[derive(Snafu, Debug)]
1216
pub enum Error {
1317
#[snafu(display(
1418
"The LDAP AuthenticationClass is missing the bind credentials. Currently the NiFi operator only supports connecting to LDAP servers using bind credentials"
1519
))]
1620
LdapAuthenticationClassMissingBindCredentials {},
21+
22+
#[snafu(display("Failed to fetch OPA ConfigMap {configmap_name}"))]
23+
FetchOpaConfigMap {
24+
source: stackable_operator::client::Error,
25+
configmap_name: String,
26+
namespace: String,
27+
},
1728
}
1829

1930
pub enum NifiAuthorizationConfig {
2031
Opa {
2132
configmap_name: String,
2233
cache_entry_time_to_live_secs: u64,
2334
cache_max_entries: u32,
35+
secret_class: Option<String>,
2436
},
2537
Default,
2638
}
2739

2840
impl NifiAuthorizationConfig {
29-
pub fn from(nifi_authorization: &Option<NifiAuthorization>) -> Self {
30-
match nifi_authorization {
41+
pub async fn from(
42+
nifi_authorization: &Option<NifiAuthorization>,
43+
client: &Client,
44+
namespace: &str,
45+
) -> Result<Self, Error> {
46+
let config = match nifi_authorization {
3147
Some(authorization_config) => match authorization_config.opa.clone() {
32-
Some(opa_config) => NifiAuthorizationConfig::Opa {
33-
configmap_name: opa_config.opa.config_map_name,
34-
cache_entry_time_to_live_secs: opa_config.cache.entry_time_to_live.as_secs(),
35-
cache_max_entries: opa_config.cache.max_entries,
36-
},
48+
Some(opa_config) => {
49+
let configmap_name = opa_config.opa.config_map_name.clone();
50+
51+
// Resolve the secret class from the ConfigMap
52+
let secret_class = client
53+
.get::<ConfigMap>(&configmap_name, namespace)
54+
.await
55+
.with_context(|_| FetchOpaConfigMapSnafu {
56+
configmap_name: configmap_name.clone(),
57+
namespace: namespace.to_string(),
58+
})?
59+
.data
60+
.and_then(|mut data| data.remove("OPA_SECRET_CLASS"));
61+
62+
NifiAuthorizationConfig::Opa {
63+
configmap_name,
64+
cache_entry_time_to_live_secs: opa_config.cache.entry_time_to_live.as_secs(),
65+
cache_max_entries: opa_config.cache.max_entries,
66+
secret_class,
67+
}
68+
}
3769
None => NifiAuthorizationConfig::Default,
3870
},
3971
None => NifiAuthorizationConfig::Default,
40-
}
72+
};
73+
74+
Ok(config)
4175
}
4276

4377
pub fn get_authorizers_config(
@@ -152,4 +186,8 @@ impl NifiAuthorizationConfig {
152186
NifiAuthorizationConfig::Default => vec![],
153187
}
154188
}
189+
190+
pub fn has_opa_tls(&self) -> bool {
191+
matches!(self, NifiAuthorizationConfig::Opa { secret_class: Some(_), .. })
192+
}
155193
}

tests/templates/kuttl/oidc-opa/20-install-opa.yaml.j2

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ commands:
55
- script: |
66
kubectl apply -n $NAMESPACE -f - <<EOF
77
---
8+
apiVersion: secrets.stackable.tech/v1alpha1
9+
kind: SecretClass
10+
metadata:
11+
name: opa-tls-$NAMESPACE
12+
spec:
13+
backend:
14+
autoTls:
15+
ca:
16+
autoGenerate: true
17+
secret:
18+
name: opa-tls-ca
19+
namespace: $NAMESPACE
20+
---
821
apiVersion: opa.stackable.tech/v1alpha1
922
kind: OpaCluster
1023
metadata:
@@ -14,6 +27,8 @@ commands:
1427
productVersion: "{{ test_scenario['values']['opa-l'] }}"
1528
pullPolicy: IfNotPresent
1629
clusterConfig:
30+
tls:
31+
serverSecretClass: opa-tls-$NAMESPACE
1732
userInfo:
1833
backend:
1934
keycloak:

0 commit comments

Comments
 (0)