From 81bec4dab415b5dad98755381c1d000a8da23be9 Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 6 Feb 2026 15:11:53 +0100 Subject: [PATCH 1/4] feat: Add annotation to provision public secret data only --- rust/operator-binary/src/backend/mod.rs | 26 ++++++++++ rust/operator-binary/src/csi_server/node.rs | 25 ++++----- rust/operator-binary/src/format/mod.rs | 19 ++++--- rust/operator-binary/src/format/well_known.rs | 52 +++++++++++-------- .../src/truststore_controller.rs | 1 + 5 files changed, 83 insertions(+), 40 deletions(-) diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 2c346056..f4a12c2a 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -73,6 +73,7 @@ pub struct SecretVolumeSelector { /// The desired format of the mounted secrets /// /// Currently supported formats: + /// /// - `tls-pem` - A Kubernetes-style triple of PEM-encoded certificate files (`tls.crt`, `tls.key`, `ca.crt`). /// - `tls-pkcs12` - A PKCS#12 key store named `keystore.p12` and truststore named `truststore.p12`. /// - `kerberos` - A Kerberos keytab named `keytab`, along with a `krb5.conf`. @@ -138,6 +139,21 @@ pub struct SecretVolumeSelector { default )] pub cert_manager_cert_lifetime: Option, + + // TODO (@Techassi): Name to be decided. Will potentially be renamed. + /// Only provision non-sensitive secret data. + /// + /// - TLS (PEM): Only provision the `ca.crt` file + /// - TLS (PKCS#12): Only provision the `truststore.p12` file + /// - Kerberos: Only provision the `krb5.conf` file + /// + /// This defaults to `false` to be backwords compatible with behaviour before SDP 26.3.0. + #[serde( + rename = "secrets.stackable.tech/only-provision-identity", + deserialize_with = "SecretVolumeSelector::deserialize_str_as_bool", + default + )] + pub only_provision_identity: bool, } /// Configuration provided by the [`TrustStore`] selecting what trust data should be provided. @@ -270,6 +286,16 @@ impl SecretVolumeSelector { ) }) } + + fn deserialize_str_as_bool<'de, D: Deserializer<'de>>(de: D) -> Result { + let str = String::deserialize(de)?; + str.parse().map_err(|_| { + ::invalid_value( + Unexpected::Str(&str), + &"a string containing a boolean", + ) + }) + } } #[derive(Debug)] diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index c6c9d422..3980c98d 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -26,10 +26,7 @@ use crate::{ self, SecretBackendError, SecretContents, SecretVolumeSelector, pod_info::{self, PodInfo}, }, - format::{ - self, SecretFormat, - well_known::{CompatibilityOptions, NamingOptions}, - }, + format, grpc::csi::v1::{ NodeExpandVolumeRequest, NodeExpandVolumeResponse, NodeGetCapabilitiesRequest, NodeGetCapabilitiesResponse, NodeGetInfoRequest, NodeGetInfoResponse, @@ -220,10 +217,16 @@ impl SecretProvisionerNode { &self, target_path: &Path, data: SecretContents, - format: Option, - names: NamingOptions, - compat: CompatibilityOptions, + selector: SecretVolumeSelector, ) -> Result<(), PublishError> { + let SecretVolumeSelector { + only_provision_identity, + format, + compat, + names, + .. + } = selector; + let create_secret = { let mut opts = OpenOptions::new(); opts.create(true) @@ -234,9 +237,10 @@ impl SecretProvisionerNode { .mode(0o640); opts }; + for (k, v) in data .data - .into_files(format, names, compat) + .into_files(format, names, compat, only_provision_identity) .context(publish_error::FormatDataSnafu)? { // The following few lines of code do some basic checks against @@ -423,10 +427,7 @@ impl Node for SecretProvisionerNode { self.save_secret_data( &target_path, data, - // NOTE (@Techassi): At this point, we might want to pass the whole selector instead - selector.format, - selector.names, - selector.compat, + selector ) .await?; Ok(Response::new(NodePublishVolumeResponse {})) diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index 7a268b9c..1c192cb4 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -22,7 +22,7 @@ pub enum SecretData { impl SecretData { pub fn parse(self) -> Result { match self { - Self::WellKnown(x) => Ok(x), + Self::WellKnown(data) => Ok(data), Self::Unknown(files) => WellKnownSecretData::from_files(files), } } @@ -32,15 +32,20 @@ impl SecretData { format: Option, names: NamingOptions, compat: CompatibilityOptions, + only_identity: bool, ) -> Result { - if let Some(format) = format { - Ok(self.parse()?.convert_to(format, compat)?.into_files(names)) + let files = if let Some(format) = format { + self.parse()? + .convert_to(format, compat)? + .into_files(names, only_identity) } else { - Ok(match self { - SecretData::WellKnown(data) => data.into_files(names), + match self { + SecretData::WellKnown(data) => data.into_files(names, only_identity), SecretData::Unknown(files) => files, - }) - } + } + }; + + Ok(files) } } diff --git a/rust/operator-binary/src/format/well_known.rs b/rust/operator-binary/src/format/well_known.rs index ce513058..74cc8444 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -47,35 +47,45 @@ pub enum WellKnownSecretData { } impl WellKnownSecretData { - pub fn into_files(self, names: NamingOptions) -> SecretFiles { + pub fn into_files(self, names: NamingOptions, only_identity: bool) -> SecretFiles { match self { WellKnownSecretData::TlsPem(TlsPem { certificate_pem, key_pem, ca_pem, - }) => [ - Some(names.tls_pem_cert_name).zip(certificate_pem), - Some(names.tls_pem_key_name).zip(key_pem), - Some((names.tls_pem_ca_name, ca_pem)), - ] - .into_iter() - .flatten() - .collect(), + }) => { + let mut files = vec![Some((names.tls_pem_ca_name, ca_pem))]; + + if !only_identity { + files.extend([ + Some(names.tls_pem_cert_name).zip(certificate_pem), + Some(names.tls_pem_key_name).zip(key_pem), + ]); + } + + files.into_iter().flatten().collect() + } WellKnownSecretData::TlsPkcs12(TlsPkcs12 { keystore, truststore, - }) => [ - Some(names.tls_pkcs12_keystore_name).zip(keystore), - Some((names.tls_pkcs12_truststore_name, truststore)), - ] - .into_iter() - .flatten() - .collect(), - WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => [ - (FILE_KERBEROS_KEYTAB_KEYTAB.to_string(), keytab), - (FILE_KERBEROS_KEYTAB_KRB5_CONF.to_string(), krb5_conf), - ] - .into(), + }) => { + let mut files = vec![Some((names.tls_pkcs12_truststore_name, truststore))]; + + if !only_identity { + files.push(Some(names.tls_pkcs12_keystore_name).zip(keystore)); + } + + files.into_iter().flatten().collect() + } + WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => { + let mut files = vec![(FILE_KERBEROS_KEYTAB_KRB5_CONF.to_string(), krb5_conf)]; + + if !only_identity { + files.push((FILE_KERBEROS_KEYTAB_KEYTAB.to_string(), keytab)); + } + + SecretFiles::from_iter(files) + } } } diff --git a/rust/operator-binary/src/truststore_controller.rs b/rust/operator-binary/src/truststore_controller.rs index 4346646c..46c2a1c5 100644 --- a/rust/operator-binary/src/truststore_controller.rs +++ b/rust/operator-binary/src/truststore_controller.rs @@ -291,6 +291,7 @@ async fn reconcile( truststore.spec.format, NamingOptions::default(), CompatibilityOptions::default(), + false, ) .context(FormatDataSnafu { secret_class: secret_class_ref, From 07c3ad74fd5c73f56090ab3f000c7c1732ac9021 Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 12 Feb 2026 14:36:58 +0100 Subject: [PATCH 2/4] chore: Rename tls module to auto_tls --- .../src/backend/{tls => auto_tls}/ca.rs | 0 .../src/backend/{tls => auto_tls}/mod.rs | 242 ++++++++++-------- rust/operator-binary/src/backend/dynamic.rs | 5 +- rust/operator-binary/src/backend/mod.rs | 10 +- .../src/crd/secret_class/mod.rs | 2 +- .../src/crd/secret_class/v1alpha2_impl.rs | 6 +- 6 files changed, 147 insertions(+), 118 deletions(-) rename rust/operator-binary/src/backend/{tls => auto_tls}/ca.rs (100%) rename rust/operator-binary/src/backend/{tls => auto_tls}/mod.rs (73%) diff --git a/rust/operator-binary/src/backend/tls/ca.rs b/rust/operator-binary/src/backend/auto_tls/ca.rs similarity index 100% rename from rust/operator-binary/src/backend/tls/ca.rs rename to rust/operator-binary/src/backend/auto_tls/ca.rs diff --git a/rust/operator-binary/src/backend/tls/mod.rs b/rust/operator-binary/src/backend/auto_tls/mod.rs similarity index 73% rename from rust/operator-binary/src/backend/tls/mod.rs rename to rust/operator-binary/src/backend/auto_tls/mod.rs index 86a7bcaf..6f40e31b 100644 --- a/rust/operator-binary/src/backend/tls/mod.rs +++ b/rust/operator-binary/src/backend/auto_tls/mod.rs @@ -27,12 +27,13 @@ use stackable_operator::{ }; use time::OffsetDateTime; -use super::{ - ScopeAddressesError, SecretBackend, SecretBackendError, SecretContents, - pod_info::{Address, PodInfo}, - scope::SecretScope, -}; use crate::{ + backend::{ + ScopeAddressesError, SecretBackend, SecretBackendError, SecretContents, + SecretVolumeSelector, + pod_info::{Address, PodInfo}, + scope::SecretScope, + }, crd::v1alpha2, format::{SecretData, WellKnownSecretData, well_known}, utils::iterator_try_concat_bytes, @@ -262,7 +263,7 @@ impl SecretBackend for TlsGenerate { /// Then add the ca certificate and return these files for provisioning to the volume. async fn get_secret_data( &self, - selector: &super::SecretVolumeSelector, + selector: &SecretVolumeSelector, pod_info: PodInfo, ) -> Result { let now = OffsetDateTime::now_utc(); @@ -271,6 +272,7 @@ impl SecretBackend for TlsGenerate { // Extract and convert consumer input from the Volume annotations. let cert_lifetime = selector.autotls_cert_lifetime; let cert_restart_buffer = selector.autotls_cert_restart_buffer; + let provision_cert = !selector.only_provision_identity; // We need to check that the cert lifetime it is not longer than allowed, // by capping it to the maximum configured at the SecretClass. @@ -300,6 +302,7 @@ impl SecretBackend for TlsGenerate { let jitter_amount = Duration::from(cert_lifetime.mul_f64(jitter_factor)); let unjittered_cert_lifetime = cert_lifetime; let cert_lifetime = cert_lifetime - jitter_amount; + tracing::info!( certificate.lifetime.requested = %unjittered_cert_lifetime, certificate.lifetime.jitter = %jitter_amount, @@ -319,113 +322,140 @@ impl SecretBackend for TlsGenerate { .fail()?; } - let conf = - Conf::new(ConfMethod::default()).expect("failed to initialize OpenSSL configuration"); - - let pod_key_length = match self.key_generation { - v1alpha2::CertificateKeyGeneration::Rsa { length } => length, - }; - - let pod_key = Rsa::generate(pod_key_length) - .and_then(PKey::try_from) - .context(GenerateKeySnafu)?; - let mut addresses = Vec::new(); - for scope in &selector.scope { - addresses.extend( - selector - .scope_addresses(&pod_info, scope) - .context(ScopeAddressesSnafu { scope })?, - ); - } - for address in &mut addresses { - if let Address::Dns(dns) = address { - // Turn FQDNs into bare domain names by removing the trailing dot - if dns.ends_with('.') { - dns.pop(); - } - } - } let ca = self .ca_manager .find_certificate_authority_for_signing(not_after) .context(PickCaSnafu)?; - let pod_cert = X509Builder::new() - .and_then(|mut x509| { - let subject_name = X509NameBuilder::new() - .and_then(|mut name| { - name.append_entry_by_nid(Nid::COMMONNAME, "generated certificate for pod")?; - Ok(name) - })? - .build(); - x509.set_subject_name(&subject_name)?; - x509.set_issuer_name(ca.certificate.subject_name())?; - x509.set_not_before(Asn1Time::from_unix(not_before.unix_timestamp())?.as_ref())?; - x509.set_not_after(Asn1Time::from_unix(not_after.unix_timestamp())?.as_ref())?; - x509.set_pubkey(&pod_key)?; - x509.set_version( - 3 - 1, // zero-indexed - )?; - let mut serial = BigNum::new()?; - serial.rand(64, MsbOption::MAYBE_ZERO, false)?; - x509.set_serial_number(Asn1Integer::from_bn(&serial)?.as_ref())?; - let ctx = x509.x509v3_context(Some(&ca.certificate), Some(&conf)); - let mut exts = vec![ - BasicConstraints::new().critical().build()?, - KeyUsage::new() - .key_encipherment() - .digital_signature() - .build()?, - ExtendedKeyUsage::new() - .server_auth() - .client_auth() - .build()?, - SubjectKeyIdentifier::new().build(&ctx)?, - AuthorityKeyIdentifier::new() - .issuer(true) - .keyid(true) - .build(&ctx)?, - ]; - let mut san_ext = SubjectAlternativeName::new(); - san_ext.critical(); - let mut has_san = false; - for addr in addresses { - has_san = true; - match addr { - Address::Dns(dns) => san_ext.dns(&dns), - Address::Ip(ip) => san_ext.ip(&ip.to_string()), - }; - } - if has_san { - exts.push(san_ext.build(&ctx)?); - } - for ext in exts { - x509.append_extension(ext)?; + + // Only run leaf certificate generation if it was requested based on the + // secret volume selector. Otherwise only a ca.crt file as a PEM envelope + // will be available (to be mounted). + let tls_secret_data = if provision_cert { + let conf = Conf::new(ConfMethod::default()) + .expect("failed to initialize OpenSSL configuration"); + + let pod_key_length = match self.key_generation { + v1alpha2::CertificateKeyGeneration::Rsa { length } => length, + }; + + let pod_key = Rsa::generate(pod_key_length) + .and_then(PKey::try_from) + .context(GenerateKeySnafu)?; + + let mut addresses = Vec::new(); + for scope in &selector.scope { + addresses.extend( + selector + .scope_addresses(&pod_info, scope) + .context(ScopeAddressesSnafu { scope })?, + ); + } + for address in &mut addresses { + if let Address::Dns(dns) = address { + // Turn FQDNs into bare domain names by removing the trailing dot + if dns.ends_with('.') { + dns.pop(); + } } - x509.sign(&ca.private_key, MessageDigest::sha256())?; - Ok(x509) - }) - .context(BuildCertificateSnafu)? - .build(); + } + + let pod_cert = X509Builder::new() + .and_then(|mut x509| { + let subject_name = X509NameBuilder::new() + .and_then(|mut name| { + name.append_entry_by_nid( + Nid::COMMONNAME, + "generated certificate for pod", + )?; + Ok(name) + })? + .build(); + x509.set_subject_name(&subject_name)?; + x509.set_issuer_name(ca.certificate.subject_name())?; + x509.set_not_before( + Asn1Time::from_unix(not_before.unix_timestamp())?.as_ref(), + )?; + x509.set_not_after(Asn1Time::from_unix(not_after.unix_timestamp())?.as_ref())?; + x509.set_pubkey(&pod_key)?; + x509.set_version( + 3 - 1, // zero-indexed + )?; + let mut serial = BigNum::new()?; + serial.rand(64, MsbOption::MAYBE_ZERO, false)?; + x509.set_serial_number(Asn1Integer::from_bn(&serial)?.as_ref())?; + let ctx = x509.x509v3_context(Some(&ca.certificate), Some(&conf)); + let mut exts = vec![ + BasicConstraints::new().critical().build()?, + KeyUsage::new() + .key_encipherment() + .digital_signature() + .build()?, + ExtendedKeyUsage::new() + .server_auth() + .client_auth() + .build()?, + SubjectKeyIdentifier::new().build(&ctx)?, + AuthorityKeyIdentifier::new() + .issuer(true) + .keyid(true) + .build(&ctx)?, + ]; + let mut san_ext = SubjectAlternativeName::new(); + san_ext.critical(); + let mut has_san = false; + for addr in addresses { + has_san = true; + match addr { + Address::Dns(dns) => san_ext.dns(&dns), + Address::Ip(ip) => san_ext.ip(&ip.to_string()), + }; + } + if has_san { + exts.push(san_ext.build(&ctx)?); + } + for ext in exts { + x509.append_extension(ext)?; + } + x509.sign(&ca.private_key, MessageDigest::sha256())?; + Ok(x509) + }) + .context(BuildCertificateSnafu)? + .build(); + + well_known::TlsPem { + ca_pem: iterator_try_concat_bytes( + self.ca_manager.trust_roots(now).into_iter().map(|ca| { + ca.to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Ca }) + }), + )?, + certificate_pem: Some( + pod_cert + .to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, + ), + key_pem: Some( + pod_key + .private_key_to_pem_pkcs8() + .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, + ), + } + } else { + well_known::TlsPem { + ca_pem: iterator_try_concat_bytes( + self.ca_manager.trust_roots(now).into_iter().map(|ca| { + ca.to_pem() + .context(SerializeCertificateSnafu { tpe: CertType::Ca }) + }), + )?, + certificate_pem: None, + key_pem: None, + } + }; + Ok( SecretContents::new(SecretData::WellKnown(WellKnownSecretData::TlsPem( - well_known::TlsPem { - ca_pem: iterator_try_concat_bytes( - self.ca_manager.trust_roots(now).into_iter().map(|ca| { - ca.to_pem() - .context(SerializeCertificateSnafu { tpe: CertType::Ca }) - }), - )?, - certificate_pem: Some( - pod_cert - .to_pem() - .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, - ), - key_pem: Some( - pod_key - .private_key_to_pem_pkcs8() - .context(SerializeCertificateSnafu { tpe: CertType::Pod })?, - ), - }, + tls_secret_data, ))) .expires_after( time_datetime_to_chrono(expire_pod_after).context(InvalidCertLifetimeSnafu)?, diff --git a/rust/operator-binary/src/backend/dynamic.rs b/rust/operator-binary/src/backend/dynamic.rs index 5cf3b0e2..1579b666 100644 --- a/rust/operator-binary/src/backend/dynamic.rs +++ b/rust/operator-binary/src/backend/dynamic.rs @@ -10,10 +10,9 @@ use snafu::{ResultExt, Snafu}; use stackable_operator::kube::runtime::reflector::ObjectRef; use super::{ - SecretBackend, SecretBackendError, SecretVolumeSelector, + SecretBackend, SecretBackendError, SecretVolumeSelector, auto_tls, kerberos_keytab::{self, KerberosProfile}, pod_info::{PodInfo, SchedulingPodInfo}, - tls, }; use crate::{crd::v1alpha2, utils::Unloggable}; @@ -99,7 +98,7 @@ pub fn from(backend: impl SecretBackend + 'static) -> Box { #[snafu(module)] pub enum FromClassError { #[snafu(display("failed to initialize TLS backend"), context(false))] - Tls { source: tls::Error }, + Tls { source: auto_tls::Error }, #[snafu( display("failed to initialize Kerberos Keytab backend"), diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index f4a12c2a..d1cd5724 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -1,16 +1,17 @@ //! Collects or generates secret data based on the request in the Kubernetes `Volume` definition +pub mod auto_tls; pub mod cert_manager; pub mod dynamic; pub mod k8s_search; pub mod kerberos_keytab; pub mod pod_info; pub mod scope; -pub mod tls; use std::{collections::HashSet, convert::Infallible, fmt::Debug}; use async_trait::async_trait; +pub use auto_tls::TlsGenerate; pub use cert_manager::CertManager; pub use k8s_search::K8sSearch; pub use kerberos_keytab::KerberosKeytab; @@ -25,7 +26,6 @@ use stackable_operator::{ kube::api::DynamicObject, shared::time::Duration, }; -pub use tls::TlsGenerate; use self::pod_info::SchedulingPodInfo; #[cfg(doc)] @@ -180,15 +180,15 @@ pub struct InternalSecretVolumeSelectorParams { } fn default_cert_restart_buffer() -> Duration { - tls::DEFAULT_CERT_RESTART_BUFFER + auto_tls::DEFAULT_CERT_RESTART_BUFFER } fn default_cert_lifetime() -> Duration { - tls::DEFAULT_CERT_LIFETIME + auto_tls::DEFAULT_CERT_LIFETIME } fn default_cert_jitter_factor() -> f64 { - tls::DEFAULT_CERT_JITTER_FACTOR + auto_tls::DEFAULT_CERT_JITTER_FACTOR } #[derive(Snafu, Debug)] diff --git a/rust/operator-binary/src/crd/secret_class/mod.rs b/rust/operator-binary/src/crd/secret_class/mod.rs index f45da86d..b890b17f 100644 --- a/rust/operator-binary/src/crd/secret_class/mod.rs +++ b/rust/operator-binary/src/crd/secret_class/mod.rs @@ -339,7 +339,7 @@ mod test { use stackable_secret_operator_utils::crd::{ConfigMapReference, SecretReference}; use crate::{ - backend::tls::{ + backend::auto_tls::{ DEFAULT_CA_CERT_LIFETIME, DEFAULT_CA_CERT_RETIREMENT_DURATION, DEFAULT_MAX_CERT_LIFETIME, }, diff --git a/rust/operator-binary/src/crd/secret_class/v1alpha2_impl.rs b/rust/operator-binary/src/crd/secret_class/v1alpha2_impl.rs index 8a7dfe79..dc644750 100644 --- a/rust/operator-binary/src/crd/secret_class/v1alpha2_impl.rs +++ b/rust/operator-binary/src/crd/secret_class/v1alpha2_impl.rs @@ -98,17 +98,17 @@ impl SearchNamespaceMatchCondition { impl AutoTlsBackend { pub(crate) fn default_max_certificate_lifetime() -> Duration { - backend::tls::DEFAULT_MAX_CERT_LIFETIME + backend::auto_tls::DEFAULT_MAX_CERT_LIFETIME } } impl AutoTlsCa { pub(crate) fn default_ca_certificate_lifetime() -> Duration { - backend::tls::DEFAULT_CA_CERT_LIFETIME + backend::auto_tls::DEFAULT_CA_CERT_LIFETIME } pub(crate) fn default_ca_certificate_retirement_duration() -> Duration { - backend::tls::DEFAULT_CA_CERT_RETIREMENT_DURATION + backend::auto_tls::DEFAULT_CA_CERT_RETIREMENT_DURATION } } From fe1d9caf7dc41d45a388b4138973fbcbe07caffe Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 12 Feb 2026 14:55:19 +0100 Subject: [PATCH 3/4] feat: Add relaxed file loading --- .../src/backend/kerberos_keytab.rs | 4 +- rust/operator-binary/src/csi_server/node.rs | 4 +- rust/operator-binary/src/format/mod.rs | 19 ++-- rust/operator-binary/src/format/well_known.rs | 102 ++++++++++-------- rust/operator-binary/src/utils.rs | 32 +++++- 5 files changed, 109 insertions(+), 52 deletions(-) diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index 4d1f7bc2..3a562d1d 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -196,6 +196,7 @@ cluster.local = {realm_name} .cluster.local = {realm_name} "# ); + let profile_file_path = tmp.path().join("krb5.conf"); { let mut profile_file = File::create(&profile_file_path) @@ -216,6 +217,7 @@ cluster.local = {realm_name} .await .context(WriteAdminKeytabSnafu)?; } + let keytab_file_path = tmp.path().join("pod-keytab"); let mut pod_principals: Vec = Vec::new(); for service_name in &selector.kerberos_service_names { @@ -300,7 +302,7 @@ cluster.local = {realm_name} .context(ReadProvisionedKeytabSnafu)?; Ok(SecretContents::new(SecretData::WellKnown( WellKnownSecretData::Kerberos(well_known::Kerberos { - keytab: keytab_data, + keytab: Some(keytab_data), krb5_conf: profile.into_bytes(), }), ))) diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index 3980c98d..721f2802 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -220,7 +220,7 @@ impl SecretProvisionerNode { selector: SecretVolumeSelector, ) -> Result<(), PublishError> { let SecretVolumeSelector { - only_provision_identity, + only_provision_identity: relaxed, format, compat, names, @@ -240,7 +240,7 @@ impl SecretProvisionerNode { for (k, v) in data .data - .into_files(format, names, compat, only_provision_identity) + .into_files(format, names, compat, relaxed) .context(publish_error::FormatDataSnafu)? { // The following few lines of code do some basic checks against diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index 1c192cb4..a070b154 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -20,10 +20,10 @@ pub enum SecretData { Unknown(SecretFiles), } impl SecretData { - pub fn parse(self) -> Result { + pub fn parse(self, relaxed: bool) -> Result { match self { Self::WellKnown(data) => Ok(data), - Self::Unknown(files) => WellKnownSecretData::from_files(files), + Self::Unknown(files) => WellKnownSecretData::from_files(files, relaxed), } } @@ -32,15 +32,22 @@ impl SecretData { format: Option, names: NamingOptions, compat: CompatibilityOptions, - only_identity: bool, + relaxed: bool, ) -> Result { let files = if let Some(format) = format { - self.parse()? + tracing::debug!( + ?format, + ?names, + relaxed, + "Explicit format requested: parsing and converting to transform into files" + ); + + self.parse(relaxed)? .convert_to(format, compat)? - .into_files(names, only_identity) + .into_files(names) } else { match self { - SecretData::WellKnown(data) => data.into_files(names, only_identity), + SecretData::WellKnown(data) => data.into_files(names), SecretData::Unknown(files) => files, } }; diff --git a/rust/operator-binary/src/format/well_known.rs b/rust/operator-binary/src/format/well_known.rs index 74cc8444..588d7eaf 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -4,6 +4,7 @@ use stackable_operator::schemars::{self, JsonSchema}; use strum::EnumDiscriminants; use super::{ConvertError, SecretFiles, convert}; +use crate::utils::ResultExt; const FILE_PEM_CERT_CERT: &str = "tls.crt"; const FILE_PEM_CERT_KEY: &str = "tls.key"; @@ -30,7 +31,7 @@ pub struct TlsPkcs12 { #[derive(Debug)] pub struct Kerberos { - pub keytab: Vec, + pub keytab: Option>, pub krb5_conf: Vec, } @@ -47,71 +48,88 @@ pub enum WellKnownSecretData { } impl WellKnownSecretData { - pub fn into_files(self, names: NamingOptions, only_identity: bool) -> SecretFiles { + pub fn into_files(self, names: NamingOptions) -> SecretFiles { match self { WellKnownSecretData::TlsPem(TlsPem { certificate_pem, key_pem, ca_pem, - }) => { - let mut files = vec![Some((names.tls_pem_ca_name, ca_pem))]; - - if !only_identity { - files.extend([ - Some(names.tls_pem_cert_name).zip(certificate_pem), - Some(names.tls_pem_key_name).zip(key_pem), - ]); - } - - files.into_iter().flatten().collect() - } + }) => [ + Some((names.tls_pem_ca_name, ca_pem)), + Some(names.tls_pem_cert_name).zip(certificate_pem), + Some(names.tls_pem_key_name).zip(key_pem), + ] + .into_iter() + .flatten() + .collect(), WellKnownSecretData::TlsPkcs12(TlsPkcs12 { keystore, truststore, - }) => { - let mut files = vec![Some((names.tls_pkcs12_truststore_name, truststore))]; - - if !only_identity { - files.push(Some(names.tls_pkcs12_keystore_name).zip(keystore)); - } - - files.into_iter().flatten().collect() - } - WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => { - let mut files = vec![(FILE_KERBEROS_KEYTAB_KRB5_CONF.to_string(), krb5_conf)]; - - if !only_identity { - files.push((FILE_KERBEROS_KEYTAB_KEYTAB.to_string(), keytab)); - } - - SecretFiles::from_iter(files) - } + }) => [ + Some((names.tls_pkcs12_truststore_name, truststore)), + Some(names.tls_pkcs12_keystore_name).zip(keystore), + ] + .into_iter() + .flatten() + .collect(), + WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => [ + Some((FILE_KERBEROS_KEYTAB_KRB5_CONF.to_owned(), krb5_conf)), + Some(FILE_KERBEROS_KEYTAB_KEYTAB.to_owned()).zip(keytab), + ] + .into_iter() + .flatten() + .collect(), } } - pub fn from_files(mut files: SecretFiles) -> Result { + pub fn from_files( + mut files: SecretFiles, + relaxed: bool, + ) -> Result { + tracing::debug!(relaxed, "Constructing well-known secret data from files"); + let mut take_file = |format, file| { files .remove(file) .context(from_files_error::MissingRequiredFileSnafu { format, file }) }; - if let Ok(certificate_pem) = take_file(SecretFormat::TlsPem, FILE_PEM_CERT_CERT) { + // Which file is tried to be parsed first matters. To support the use-case of people bringing + // their own non-sensitive data via a Secret and consumers only requiring access to + // non-sensitve data (for example for CA verification), the non-senstive files are parsed + // first. If the `relaxed` flag is provided, this function tries to parse sensitive files + // but won't hard-error when they are not found. + + if let Ok(ca_pem) = take_file(SecretFormat::TlsPem, FILE_PEM_CERT_CA) { let mut take_file = |file| take_file(SecretFormat::TlsPem, file); + + let certificate_pem = take_file(FILE_PEM_CERT_CERT).ok_if(relaxed)?; + let key_pem = take_file(FILE_PEM_CERT_KEY).ok_if(relaxed)?; + Ok(WellKnownSecretData::TlsPem(TlsPem { - certificate_pem: Some(certificate_pem), - key_pem: Some(take_file(FILE_PEM_CERT_KEY)?), - ca_pem: take_file(FILE_PEM_CERT_CA)?, + certificate_pem, + key_pem, + ca_pem, })) - } else if let Ok(keystore) = take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_KEYSTORE) { + } else if let Ok(truststore) = + take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_TRUSTSTORE) + { + let keystore = + take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_KEYSTORE).ok_if(relaxed)?; + Ok(WellKnownSecretData::TlsPkcs12(TlsPkcs12 { - keystore: Some(keystore), - truststore: take_file(SecretFormat::TlsPkcs12, FILE_PKCS12_CERT_TRUSTSTORE)?, + keystore, + truststore, })) - } else if let Ok(keytab) = take_file(SecretFormat::Kerberos, FILE_KERBEROS_KEYTAB_KEYTAB) { + } else if let Ok(krb5_conf) = + take_file(SecretFormat::Kerberos, FILE_KERBEROS_KEYTAB_KRB5_CONF) + { + let keytab = + take_file(SecretFormat::Kerberos, FILE_KERBEROS_KEYTAB_KEYTAB).ok_if(relaxed)?; + Ok(WellKnownSecretData::Kerberos(Kerberos { keytab, - krb5_conf: take_file(SecretFormat::Kerberos, FILE_KERBEROS_KEYTAB_KRB5_CONF)?, + krb5_conf, })) } else { from_files_error::UnknownFormatSnafu { diff --git a/rust/operator-binary/src/utils.rs b/rust/operator-binary/src/utils.rs index 1af8d521..9821d364 100644 --- a/rust/operator-binary/src/utils.rs +++ b/rust/operator-binary/src/utils.rs @@ -229,6 +229,24 @@ where } } +pub trait ResultExt { + /// Transforms this [`Result`] into [`Ok(Some(T))`] if [`Ok`], [`Ok(None)`] if [`Err`] and + /// `predicate` is `true` or [`Err(_)`] if [`Err`]. + /// + /// This basically applies [`Result::ok`] only if `predicate` is `true`. + fn ok_if(self, predicate: bool) -> Result, E>; +} + +impl ResultExt for Result { + fn ok_if(self, predicate: bool) -> Result, E> { + match self { + Ok(ok) => Ok(Some(ok)), + Err(_) if predicate => Ok(None), + Err(err) => Err(err), + } + } +} + #[cfg(test)] mod tests { use futures::StreamExt; @@ -236,7 +254,7 @@ mod tests { use time::OffsetDateTime; use super::{asn1time_to_offsetdatetime, iterator_try_concat_bytes}; - use crate::utils::{Flattened, FmtByteSlice, error_full_message, trystream_any}; + use crate::utils::{Flattened, FmtByteSlice, ResultExt, error_full_message, trystream_any}; #[test] fn fmt_hex_byte_slice() { @@ -346,4 +364,16 @@ mod tests { assert_eq!(small, vec![2, 10, 5]); assert_eq!(big, vec![1000, 2000]); } + + #[test] + fn ok_if() { + let ok: Result<_, ()> = Ok(42usize); + let err: Result<(), _> = Err(()); + + assert_eq!(Some(42usize), ok.ok_if(true).expect("must be Ok")); + assert_eq!(Some(42usize), ok.ok_if(false).expect("must be Ok")); + + assert_eq!(None, err.ok_if(true).expect("must be Ok")); + assert!(err.ok_if(false).is_err()); + } } From 263e59c4ff2e727a217f6752574400927331c88c Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 12 Feb 2026 15:48:00 +0100 Subject: [PATCH 4/4] feat: Conditionally generate Kerberos keytab --- rust/krb5-provision-keytab/src/lib.rs | 5 +- .../src/backend/kerberos_keytab.rs | 188 +++++++++--------- 2 files changed, 103 insertions(+), 90 deletions(-) diff --git a/rust/krb5-provision-keytab/src/lib.rs b/rust/krb5-provision-keytab/src/lib.rs index 7c94df92..c9eb9ae2 100644 --- a/rust/krb5-provision-keytab/src/lib.rs +++ b/rust/krb5-provision-keytab/src/lib.rs @@ -68,7 +68,10 @@ pub enum Error { /// Provisions a Kerberos Keytab based on the [`Request`]. /// /// This function assumes that the binary produced by this crate is on the `$PATH`, and will fail otherwise. -pub async fn provision_keytab(krb5_config_path: &Path, req: &Request) -> Result { +pub async fn provision_keytab_file( + krb5_config_path: &Path, + req: &Request, +) -> Result { let req_str = serde_json::to_vec(&req).context(SerializeRequestSnafu)?; let mut child = Command::new("stackable-krb5-provision-keytab") diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index 3a562d1d..b6a6bb80 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -3,7 +3,7 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_krb5_provision_keytab::{ // Some qualified paths get long enough to break rustfmt, alias the crate name to work around that self as provision, - provision_keytab, + provision_keytab_file, }; use stackable_operator::{ commons::networking::{HostName, KerberosRealmName}, @@ -167,6 +167,8 @@ impl SecretBackend for KerberosKeytab { admin_principal, } = self; + let provision_keytab = !selector.only_provision_identity; + let admin_server_clause = match admin { v1alpha2::KerberosKeytabBackendAdmin::Mit(v1alpha2::KerberosKeytabBackendMit { kadmin_server, @@ -207,102 +209,110 @@ cluster.local = {realm_name} .await .context(WriteConfigSnafu)?; } - let admin_keytab_file_path = tmp.path().join("admin-keytab"); - { - let mut admin_keytab_file = File::create(&admin_keytab_file_path) - .await - .context(WriteAdminKeytabSnafu)?; - admin_keytab_file - .write_all(admin_keytab) - .await - .context(WriteAdminKeytabSnafu)?; - } - let keytab_file_path = tmp.path().join("pod-keytab"); - let mut pod_principals: Vec = Vec::new(); - for service_name in &selector.kerberos_service_names { - for scope in &selector.scope { - for addr in - selector - .scope_addresses(&pod_info, scope) - .context(ScopeAddressesSnafu { - scope: scope.clone(), - })? + let keytab_data = + if provision_keytab { + let admin_keytab_file_path = tmp.path().join("admin-keytab"); { - pod_principals.push( - match addr { - Address::Dns(hostname) => { - format!("{service_name}/{hostname}") - } - Address::Ip(ip) => { - format!("{service_name}/{ip}") - } - } - .try_into() - .context(PodPrincipalSnafu)?, - ); + let mut admin_keytab_file = File::create(&admin_keytab_file_path) + .await + .context(WriteAdminKeytabSnafu)?; + admin_keytab_file + .write_all(admin_keytab) + .await + .context(WriteAdminKeytabSnafu)?; } - } - } - provision_keytab( - &profile_file_path, - &stackable_krb5_provision_keytab::Request { - admin_keytab_path: admin_keytab_file_path, - admin_principal_name: admin_principal.to_string(), - pod_keytab_path: keytab_file_path.clone(), - principals: pod_principals - .into_iter() - .map(|princ| stackable_krb5_provision_keytab::PrincipalRequest { - name: princ.to_string(), - }) - .collect(), - admin_backend: match admin { - v1alpha2::KerberosKeytabBackendAdmin::Mit { .. } => { - stackable_krb5_provision_keytab::AdminBackend::Mit - } - v1alpha2::KerberosKeytabBackendAdmin::ActiveDirectory( - v1alpha2::KerberosKeytabBackendActiveDirectory { - ldap_server, - ldap_tls_ca_secret, - password_cache_secret, - user_distinguished_name, - schema_distinguished_name, - generate_sam_account_name, - }, - ) => stackable_krb5_provision_keytab::AdminBackend::ActiveDirectory { - ldap_server: ldap_server.to_string(), - ldap_tls_ca_secret: ldap_tls_ca_secret.clone(), - password_cache_secret: password_cache_secret.clone(), - user_distinguished_name: user_distinguished_name.clone(), - schema_distinguished_name: schema_distinguished_name.clone(), - generate_sam_account_name: generate_sam_account_name.clone().map( - |v1alpha2::ActiveDirectorySamAccountNameRules { - prefix, - total_length, - }| { - provision::ActiveDirectorySamAccountNameRules { - prefix, - total_length, + + let keytab_file_path = tmp.path().join("pod-keytab"); + let mut pod_principals: Vec = Vec::new(); + for service_name in &selector.kerberos_service_names { + for scope in &selector.scope { + for addr in selector.scope_addresses(&pod_info, scope).context( + ScopeAddressesSnafu { + scope: scope.clone(), + }, + )? { + pod_principals.push( + match addr { + Address::Dns(hostname) => { + format!("{service_name}/{hostname}") + } + Address::Ip(ip) => { + format!("{service_name}/{ip}") + } } + .try_into() + .context(PodPrincipalSnafu)?, + ); + } + } + } + provision_keytab_file( + &profile_file_path, + &stackable_krb5_provision_keytab::Request { + admin_keytab_path: admin_keytab_file_path, + admin_principal_name: admin_principal.to_string(), + pod_keytab_path: keytab_file_path.clone(), + principals: pod_principals + .into_iter() + .map(|princ| stackable_krb5_provision_keytab::PrincipalRequest { + name: princ.to_string(), + }) + .collect(), + admin_backend: match admin { + v1alpha2::KerberosKeytabBackendAdmin::Mit { .. } => { + stackable_krb5_provision_keytab::AdminBackend::Mit + } + v1alpha2::KerberosKeytabBackendAdmin::ActiveDirectory( + v1alpha2::KerberosKeytabBackendActiveDirectory { + ldap_server, + ldap_tls_ca_secret, + password_cache_secret, + user_distinguished_name, + schema_distinguished_name, + generate_sam_account_name, + }, + ) => stackable_krb5_provision_keytab::AdminBackend::ActiveDirectory { + ldap_server: ldap_server.to_string(), + ldap_tls_ca_secret: ldap_tls_ca_secret.clone(), + password_cache_secret: password_cache_secret.clone(), + user_distinguished_name: user_distinguished_name.clone(), + schema_distinguished_name: schema_distinguished_name.clone(), + generate_sam_account_name: generate_sam_account_name.clone().map( + |v1alpha2::ActiveDirectorySamAccountNameRules { + prefix, + total_length, + }| { + provision::ActiveDirectorySamAccountNameRules { + prefix, + total_length, + } + }, + ), }, - ), + }, }, - }, - }, - ) - .await - .context(ProvisionKeytabSnafu)?; - let mut keytab_data = Vec::new(); - let mut keytab_file = File::open(keytab_file_path) - .await - .context(ReadProvisionedKeytabSnafu)?; - keytab_file - .read_to_end(&mut keytab_data) - .await - .context(ReadProvisionedKeytabSnafu)?; + ) + .await + .context(ProvisionKeytabSnafu)?; + let mut keytab_data = Vec::new(); + let mut keytab_file = File::open(keytab_file_path) + .await + .context(ReadProvisionedKeytabSnafu)?; + keytab_file + .read_to_end(&mut keytab_data) + .await + .context(ReadProvisionedKeytabSnafu)?; + + Some(keytab_data) + } else { + // NOTE (@Techassi): I kinda hate this, but I guess there is no way around it + None + }; + Ok(SecretContents::new(SecretData::WellKnown( WellKnownSecretData::Kerberos(well_known::Kerberos { - keytab: Some(keytab_data), + keytab: keytab_data, krb5_conf: profile.into_bytes(), }), )))