From 8a68412070cb288b4605c3fd0a24afd9b39a4c6b Mon Sep 17 00:00:00 2001 From: dervoeti Date: Fri, 10 Apr 2026 19:47:41 +0200 Subject: [PATCH 1/2] fix: Manage OIDC admin password secret via cluster_resources --- rust/operator-binary/src/controller.rs | 26 ++++++-- rust/operator-binary/src/security/mod.rs | 7 +- rust/operator-binary/src/security/oidc.rs | 79 ++++++++++++----------- 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index e8b6402a..cb421933 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -106,7 +106,7 @@ use crate::{ NifiAuthenticationConfig, STACKABLE_SERVER_TLS_DIR, STACKABLE_TLS_STORE_PASSWORD, }, authorization::{self, OPA_TLS_MOUNT_PATH, ResolvedNifiAuthorizationConfig}, - build_tls_volume, check_or_generate_oidc_admin_password, check_or_generate_sensitive_key, + build_oidc_admin_password_secret, build_tls_volume, check_or_generate_sensitive_key, tls::{KEYSTORE_NIFI_CONTAINER_MOUNT, KEYSTORE_VOLUME_NAME, TRUSTSTORE_VOLUME_NAME}, }, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, @@ -241,6 +241,11 @@ pub enum Error { cm_name: String, }, + #[snafu(display("failed to apply OIDC admin password secret"))] + ApplyOidcAdminPasswordSecret { + source: stackable_operator::cluster_resources::Error, + }, + #[snafu(display("failed to patch service account"))] ApplyServiceAccount { source: stackable_operator::cluster_resources::Error, @@ -443,10 +448,23 @@ pub async fn reconcile_nifi( ) .context(InvalidNifiAuthenticationConfigSnafu)?; - if let NifiAuthenticationConfig::Oidc { .. } = authentication_config { - check_or_generate_oidc_admin_password(client, nifi) + if let NifiAuthenticationConfig::Oidc { .. } = &authentication_config { + let oidc_admin_password_secret = build_oidc_admin_password_secret( + client, + nifi, + build_recommended_labels( + nifi, + &resolved_product_image.app_version_label_value, + "node", + "oidc", + ), + ) + .await + .context(SecuritySnafu)?; + cluster_resources + .add(client, oidc_admin_password_secret) .await - .context(SecuritySnafu)?; + .context(ApplyOidcAdminPasswordSecretSnafu)?; } let authorization_config = ResolvedNifiAuthorizationConfig::from( diff --git a/rust/operator-binary/src/security/mod.rs b/rust/operator-binary/src/security/mod.rs index 3a5c6d9e..d0a94a3a 100644 --- a/rust/operator-binary/src/security/mod.rs +++ b/rust/operator-binary/src/security/mod.rs @@ -35,11 +35,12 @@ pub async fn check_or_generate_sensitive_key( .context(SensitiveKeySnafu) } -pub async fn check_or_generate_oidc_admin_password( +pub async fn build_oidc_admin_password_secret( client: &Client, nifi: &v1alpha1::NifiCluster, -) -> Result { - oidc::check_or_generate_oidc_admin_password(client, nifi) + labels: stackable_operator::kvp::ObjectLabels<'_, v1alpha1::NifiCluster>, +) -> Result { + oidc::build_oidc_admin_password_secret(client, nifi, labels) .await .context(OidcAdminPasswordSnafu) } diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index 0d93d942..2c3568e4 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -9,6 +9,7 @@ use stackable_operator::{ crd::authentication::oidc, k8s_openapi::api::core::v1::Secret, kube::{ResourceExt, runtime::reflector::ObjectRef}, + kvp::ObjectLabels, }; use crate::{crd::v1alpha1, security::authentication::STACKABLE_ADMIN_USERNAME}; @@ -37,64 +38,70 @@ pub enum Error { #[snafu(display("Nifi doesn't support skipping the OIDC TLS verification"))] SkippingTlsVerificationNotSupported {}, + + #[snafu(display("failed to build OIDC admin password secret metadata"))] + BuildOidcAdminPasswordSecretMetadata { + source: stackable_operator::builder::meta::Error, + }, } -/// Generate a secret containing the password for the admin user that can access the API. +/// Build a Secret containing the OIDC admin password. /// -/// This admin user is the same as for SingleUser authentication. -pub(crate) async fn check_or_generate_oidc_admin_password( +/// If the secret already exists, the existing password is preserved. +/// Otherwise a new random password is generated. +pub(crate) async fn build_oidc_admin_password_secret( client: &Client, nifi: &v1alpha1::NifiCluster, -) -> Result { + labels: ObjectLabels<'_, v1alpha1::NifiCluster>, +) -> Result { let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; tracing::debug!("Checking for OIDC admin password configuration"); - match client + + let password = match client .get_opt::(&build_oidc_admin_password_secret_name(nifi), namespace) .await .context(OidcAdminPasswordSecretSnafu)? { Some(secret) => { - let admin_password_present = secret + let existing_password = secret .data - .iter() - .flat_map(|data| data.keys()) - .any(|key| key == STACKABLE_ADMIN_USERNAME); - - if admin_password_present { - Ok(false) - } else { - MissingAdminPasswordKeySnafu { + .as_ref() + .and_then(|data| data.get(STACKABLE_ADMIN_USERNAME)) + .map(|bytes| String::from_utf8_lossy(&bytes.0).into_owned()); + + match existing_password { + Some(password) => password, + None => MissingAdminPasswordKeySnafu { secret: ObjectRef::from_obj(&secret), } - .fail()? + .fail()?, } } None => { - tracing::info!("No existing oidc admin password secret found, generating new one"); - let password: String = rand::rng() + tracing::info!("No existing OIDC admin password secret found, generating new one"); + rand::rng() .sample_iter(&Alphanumeric) .take(15) .map(char::from) - .collect(); - - let mut secret_data = BTreeMap::new(); - secret_data.insert("admin".to_string(), password); - - let new_secret = Secret { - metadata: ObjectMetaBuilder::new() - .namespace(namespace) - .name(build_oidc_admin_password_secret_name(nifi)) - .build(), - string_data: Some(secret_data), - ..Secret::default() - }; - client - .create(&new_secret) - .await - .context(OidcAdminPasswordSecretSnafu)?; - Ok(true) + .collect() } - } + }; + + Ok(Secret { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(nifi) + .name(build_oidc_admin_password_secret_name(nifi)) + .ownerreference_from_resource(nifi, None, Some(true)) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .with_recommended_labels(labels) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .build(), + string_data: Some(BTreeMap::from([( + STACKABLE_ADMIN_USERNAME.to_string(), + password, + )])), + ..Secret::default() + }) } pub fn build_oidc_admin_password_secret_name(nifi: &v1alpha1::NifiCluster) -> String { From 66a8d967285278966959a9b3122f21a1268877af Mon Sep 17 00:00:00 2001 From: Razvan-Daniel Mihai <84674+razvan@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:57:58 +0200 Subject: [PATCH 2/2] refactor to also repair possible broken existing Secret objects --- rust/operator-binary/src/security/mod.rs | 63 +++++++++++++-- rust/operator-binary/src/security/oidc.rs | 98 +++++++---------------- 2 files changed, 87 insertions(+), 74 deletions(-) diff --git a/rust/operator-binary/src/security/mod.rs b/rust/operator-binary/src/security/mod.rs index d0a94a3a..963a9251 100644 --- a/rust/operator-binary/src/security/mod.rs +++ b/rust/operator-binary/src/security/mod.rs @@ -1,10 +1,20 @@ -use snafu::{ResultExt, Snafu}; +use std::collections::BTreeMap; + +use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ - builder::pod::volume::SecretFormat, client::Client, k8s_openapi::api::core::v1::Volume, + builder::{meta::ObjectMetaBuilder, pod::volume::SecretFormat}, + client::Client, + k8s_openapi::api::core::v1::{Secret, Volume}, + kube::ResourceExt, shared::time::Duration, }; -use crate::crd::v1alpha1; +use crate::{ + crd::v1alpha1, + security::{ + authentication::STACKABLE_ADMIN_USERNAME, oidc::build_oidc_admin_password_secret_name, + }, +}; pub mod authentication; pub mod authorization; @@ -16,14 +26,24 @@ type Result = std::result::Result; #[derive(Snafu, Debug)] pub enum Error { + #[snafu(display("the NiFi object defines no namespace"))] + ObjectHasNoNamespace, + #[snafu(display("tls failure"))] Tls { source: tls::Error }, #[snafu(display("sensistive key failure"))] SensitiveKey { source: sensitive_key::Error }, - #[snafu(display("failed to ensure OIDC admin password exists"))] - OidcAdminPassword { source: oidc::Error }, + #[snafu(display("failed to fetch or create OIDC admin password secret"))] + OidcAdminPasswordSecret { + source: stackable_operator::client::Error, + }, + + #[snafu(display("failed to build OIDC admin password secret metadata"))] + BuildOidcAdminPasswordSecretMetadata { + source: stackable_operator::builder::meta::Error, + }, } pub async fn check_or_generate_sensitive_key( @@ -35,14 +55,43 @@ pub async fn check_or_generate_sensitive_key( .context(SensitiveKeySnafu) } +/// Build a Secret containing the OIDC admin password. +/// +/// If the Secret object already exists and contains the expected key, the existing password is preserved. +/// Otherwise a new Secret object is created with a random password. +/// pub async fn build_oidc_admin_password_secret( client: &Client, nifi: &v1alpha1::NifiCluster, labels: stackable_operator::kvp::ObjectLabels<'_, v1alpha1::NifiCluster>, ) -> Result { - oidc::build_oidc_admin_password_secret(client, nifi, labels) + tracing::debug!("Checking for OIDC admin password configuration"); + + let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; + let kubernetes_secret_name = build_oidc_admin_password_secret_name(nifi); + + let oidc_admin_pass_secret = client + .get_opt::(&kubernetes_secret_name, namespace) .await - .context(OidcAdminPasswordSnafu) + .context(OidcAdminPasswordSecretSnafu)?; + + let password = oidc::build_oidc_admin_password_secret(oidc_admin_pass_secret); + + Ok(Secret { + metadata: ObjectMetaBuilder::new() + .name_and_namespace(nifi) + .name(build_oidc_admin_password_secret_name(nifi)) + .ownerreference_from_resource(nifi, None, Some(true)) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .with_recommended_labels(labels) + .context(BuildOidcAdminPasswordSecretMetadataSnafu)? + .build(), + string_data: Some(BTreeMap::from([( + STACKABLE_ADMIN_USERNAME.to_string(), + password, + )])), + ..Secret::default() + }) } pub fn build_tls_volume( diff --git a/rust/operator-binary/src/security/oidc.rs b/rust/operator-binary/src/security/oidc.rs index 2c3568e4..c0274c13 100644 --- a/rust/operator-binary/src/security/oidc.rs +++ b/rust/operator-binary/src/security/oidc.rs @@ -1,15 +1,12 @@ use std::collections::BTreeMap; use rand::{RngExt, distr::Alphanumeric}; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - client::Client, commons::tls_verification::{CaCert, TlsServerVerification, TlsVerification}, crd::authentication::oidc, - k8s_openapi::api::core::v1::Secret, - kube::{ResourceExt, runtime::reflector::ObjectRef}, - kvp::ObjectLabels, + k8s_openapi::{ByteString, api::core::v1::Secret}, + kube::ResourceExt, }; use crate::{crd::v1alpha1, security::authentication::STACKABLE_ADMIN_USERNAME}; @@ -18,19 +15,6 @@ type Result = std::result::Result; #[derive(Snafu, Debug)] pub enum Error { - #[snafu(display("the NiFi object defines no namespace"))] - ObjectHasNoNamespace, - - #[snafu(display("failed to fetch or create OIDC admin password secret"))] - OidcAdminPasswordSecret { - source: stackable_operator::client::Error, - }, - - #[snafu(display( - "found existing admin password secret {secret:?}, but the key {STACKABLE_ADMIN_USERNAME} is missing", - ))] - MissingAdminPasswordKey { secret: ObjectRef }, - #[snafu(display("invalid well-known OIDC configuration URL"))] InvalidWellKnownConfigUrl { source: stackable_operator::crd::authentication::oidc::v1alpha1::Error, @@ -38,73 +22,53 @@ pub enum Error { #[snafu(display("Nifi doesn't support skipping the OIDC TLS verification"))] SkippingTlsVerificationNotSupported {}, - - #[snafu(display("failed to build OIDC admin password secret metadata"))] - BuildOidcAdminPasswordSecretMetadata { - source: stackable_operator::builder::meta::Error, - }, } -/// Build a Secret containing the OIDC admin password. -/// -/// If the secret already exists, the existing password is preserved. +/// Returns a password to be used by the OIDC admin user. +/// If the Secret containing the password already exists and contains the expected key, the existing password is returned. /// Otherwise a new random password is generated. -pub(crate) async fn build_oidc_admin_password_secret( - client: &Client, - nifi: &v1alpha1::NifiCluster, - labels: ObjectLabels<'_, v1alpha1::NifiCluster>, -) -> Result { - let namespace: &str = &nifi.namespace().context(ObjectHasNoNamespaceSnafu)?; - tracing::debug!("Checking for OIDC admin password configuration"); - - let password = match client - .get_opt::(&build_oidc_admin_password_secret_name(nifi), namespace) - .await - .context(OidcAdminPasswordSecretSnafu)? - { +pub(crate) fn build_oidc_admin_password_secret(oidc_admin_secret: Option) -> String { + match oidc_admin_secret { Some(secret) => { let existing_password = secret .data .as_ref() .and_then(|data| data.get(STACKABLE_ADMIN_USERNAME)) - .map(|bytes| String::from_utf8_lossy(&bytes.0).into_owned()); + .map(decode_admin_password); match existing_password { Some(password) => password, - None => MissingAdminPasswordKeySnafu { - secret: ObjectRef::from_obj(&secret), + None => { + tracing::info!( + expected_key = STACKABLE_ADMIN_USERNAME, + "Found existing OIDC admin password secret, but it doesn't contain the expected key, generating new password" + ); + encode_admin_password(15) } - .fail()?, } } None => { tracing::info!("No existing OIDC admin password secret found, generating new one"); - rand::rng() - .sample_iter(&Alphanumeric) - .take(15) - .map(char::from) - .collect() + encode_admin_password(15) } - }; - - Ok(Secret { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(nifi) - .name(build_oidc_admin_password_secret_name(nifi)) - .ownerreference_from_resource(nifi, None, Some(true)) - .context(BuildOidcAdminPasswordSecretMetadataSnafu)? - .with_recommended_labels(labels) - .context(BuildOidcAdminPasswordSecretMetadataSnafu)? - .build(), - string_data: Some(BTreeMap::from([( - STACKABLE_ADMIN_USERNAME.to_string(), - password, - )])), - ..Secret::default() - }) + } +} + +// TODO: maybe switch to get_random_base64() (not public atm) from op-rs which is ASCII clean and thus more suitable for passwords: +// https://github.com/stackabletech/operator-rs/blob/main/crates/stackable-operator/src/commons/random_secret_creation.rs#L127-L127 +fn encode_admin_password(size_bytes: usize) -> String { + rand::rng() + .sample_iter(&Alphanumeric) + .take(size_bytes) + .map(char::from) + .collect() +} + +fn decode_admin_password(encoded: &ByteString) -> String { + String::from_utf8_lossy(&encoded.0).into_owned() } -pub fn build_oidc_admin_password_secret_name(nifi: &v1alpha1::NifiCluster) -> String { +pub(crate) fn build_oidc_admin_password_secret_name(nifi: &v1alpha1::NifiCluster) -> String { format!("{}-oidc-admin-password", nifi.name_any()) }