diff --git a/Cargo.lock b/Cargo.lock index f9032146e..d9a41cd7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2529,9 +2529,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", diff --git a/crates/stackable-operator/CHANGELOG.md b/crates/stackable-operator/CHANGELOG.md index b9fb62d3b..133e1c90f 100644 --- a/crates/stackable-operator/CHANGELOG.md +++ b/crates/stackable-operator/CHANGELOG.md @@ -9,6 +9,17 @@ All notable changes to this project will be documented in this file. - Add support for specifying a `clientAuthenticationMethod` for OIDC ([#1178]). This was originally done in [#1158] and had been reverted in [#1170]. +### Changed + +- BREAKING: Add mandatory `provision_parts` argument to `SecretOperatorVolumeSourceBuilder::new` ([#1165]). + It now forces the caller to make an explicit choice if the public parts are sufficient or if private + (e.g. a certificate for the Pod) parts are needed as well. This is done to avoid accidentally requesting + too much parts. For details see [this issue](https://github.com/stackabletech/issues/issues/547). + + Additionally, `SecretClassVolume::to_volume` and `SecretClassVolume::to_ephemeral_volume_source` + also take the same new argument. + +[#1165]: https://github.com/stackabletech/operator-rs/pull/1165 [#1178]: https://github.com/stackabletech/operator-rs/pull/1178 ## [0.108.0] - 2026-03-10 diff --git a/crates/stackable-operator/src/builder/pod/volume.rs b/crates/stackable-operator/src/builder/pod/volume.rs index 9b7a1bd98..75903ae1e 100644 --- a/crates/stackable-operator/src/builder/pod/volume.rs +++ b/crates/stackable-operator/src/builder/pod/volume.rs @@ -14,6 +14,7 @@ use tracing::warn; use crate::{ builder::meta::ObjectMetaBuilder, + commons::secret_class::SecretClassVolumeProvisionParts, kvp::{Annotation, AnnotationError, Annotations, LabelError, Labels}, }; @@ -281,10 +282,21 @@ pub struct SecretOperatorVolumeSourceBuilder { kerberos_service_names: Vec, tls_pkcs12_password: Option, auto_tls_cert_lifetime: Option, + provision_parts: SecretClassVolumeProvisionParts, } impl SecretOperatorVolumeSourceBuilder { - pub fn new(secret_class: impl Into) -> Self { + /// Creates a builder for a secret-operator volume that uses the specified SecretClass to + /// request the specified [`SecretClassVolumeProvisionParts`]. + /// + /// This function forces the caller to make an explicit choice if the public parts are + /// sufficient or if private (e.g. a certificate for the Pod) parts are needed as well. + /// This is done to avoid accidentally requesting too much parts. For details see + /// [this issue](https://github.com/stackabletech/issues/issues/547). + pub fn new( + secret_class: impl Into, + provision_parts: SecretClassVolumeProvisionParts, + ) -> Self { Self { secret_class: secret_class.into(), scopes: Vec::new(), @@ -292,6 +304,7 @@ impl SecretOperatorVolumeSourceBuilder { kerberos_service_names: Vec::new(), tls_pkcs12_password: None, auto_tls_cert_lifetime: None, + provision_parts, } } @@ -340,8 +353,10 @@ impl SecretOperatorVolumeSourceBuilder { pub fn build(&self) -> Result { let mut annotations = Annotations::new(); + #[rustfmt::skip] annotations - .insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?); + .insert(Annotation::secret_class(&self.secret_class).context(ParseAnnotationSnafu)?) + .insert(Annotation::secret_provision_parts(&self.provision_parts).context(ParseAnnotationSnafu)?); if !self.scopes.is_empty() { annotations diff --git a/crates/stackable-operator/src/commons/secret_class.rs b/crates/stackable-operator/src/commons/secret_class.rs index 2ed8ca7de..c4c85c6e5 100644 --- a/crates/stackable-operator/src/commons/secret_class.rs +++ b/crates/stackable-operator/src/commons/secret_class.rs @@ -38,9 +38,10 @@ impl SecretClassVolume { pub fn to_ephemeral_volume_source( &self, + provision_parts: SecretClassVolumeProvisionParts, ) -> Result { let mut secret_operator_volume_builder = - SecretOperatorVolumeSourceBuilder::new(&self.secret_class); + SecretOperatorVolumeSourceBuilder::new(&self.secret_class, provision_parts); if let Some(scope) = &self.scope { if scope.pod { @@ -62,8 +63,12 @@ impl SecretClassVolume { .context(SecretOperatorVolumeSnafu) } - pub fn to_volume(&self, volume_name: &str) -> Result { - let ephemeral = self.to_ephemeral_volume_source()?; + pub fn to_volume( + &self, + volume_name: &str, + provision_parts: SecretClassVolumeProvisionParts, + ) -> Result { + let ephemeral = self.to_ephemeral_volume_source(provision_parts)?; Ok(VolumeBuilder::new(volume_name).ephemeral(ephemeral).build()) } } @@ -94,6 +99,25 @@ pub struct SecretClassVolumeScope { pub listener_volumes: Vec, } +/// What parts of secret material should be provisioned into the requested volume. +// +// There intentionally isn't a global [`Default`] impl, as it is secret-operator's concern what it +// chooses as a default. +// TODO (@Techassi): This to me is a HUGE indicator this lives in the wrong place. All these secret +// volume builders/helpers should be defined as part of a secret-operator library to be as close as +// possible to secret-operator, which is the authoritative source of truth for all of this. +#[derive(Copy, Clone, Debug, PartialEq, Eq, strum::AsRefStr)] +#[strum(serialize_all = "kebab-case")] +pub enum SecretClassVolumeProvisionParts { + /// Only provision public parts, such as the CA certificate (either as PEM or truststore) or + /// `krb5.conf`. + Public, + + /// Provision all parts, which includes all [`Public`](Self::Public) ones as well as additional + /// private parts, such as a TLS cert + private key, a keystore or a keytab. + PublicPrivate, +} + #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -101,7 +125,7 @@ mod tests { use super::*; #[test] - fn volume_to_csi_volume_source() { + fn volume_to_ephemeral_volume_source() { let secret_class_volume_source = SecretClassVolume { secret_class: "myclass".to_string(), // pragma: allowlist secret scope: Some(SecretClassVolumeScope { @@ -111,7 +135,8 @@ mod tests { listener_volumes: vec!["mylistener".to_string()], }), } - .to_ephemeral_volume_source() + // Let's assume we need some form of private data (e.g. a certificate or S3 credentials) + .to_ephemeral_volume_source(SecretClassVolumeProvisionParts::PublicPrivate) .unwrap(); let expected_volume_attributes = BTreeMap::from([ @@ -123,6 +148,10 @@ mod tests { "secrets.stackable.tech/scope".to_string(), "pod,service=myservice,listener-volume=mylistener".to_string(), ), + ( + "secrets.stackable.tech/provision-parts".to_string(), + "public-private".to_string(), + ), ]); assert_eq!( diff --git a/crates/stackable-operator/src/commons/tls_verification.rs b/crates/stackable-operator/src/commons/tls_verification.rs index 1e399b0cf..e0b97b440 100644 --- a/crates/stackable-operator/src/commons/tls_verification.rs +++ b/crates/stackable-operator/src/commons/tls_verification.rs @@ -8,7 +8,9 @@ use crate::{ self, pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder}, }, - commons::secret_class::{SecretClassVolume, SecretClassVolumeError}, + commons::secret_class::{ + SecretClassVolume, SecretClassVolumeError, SecretClassVolumeProvisionParts, + }, constants::secret::SECRET_BASE_PATH, }; @@ -72,7 +74,8 @@ impl TlsClientDetails { let volume_name = format!("{secret_class}-ca-cert"); let secret_class_volume = SecretClassVolume::new(secret_class.clone(), None); let volume = secret_class_volume - .to_volume(&volume_name) + // We only need the public CA cert + .to_volume(&volume_name, SecretClassVolumeProvisionParts::Public) .context(SecretClassVolumeSnafu)?; volumes.push(volume); diff --git a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs index 674ddbd91..d0dbcc990 100644 --- a/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/authentication/ldap/v1alpha1_impl.rs @@ -7,7 +7,10 @@ use crate::{ self, pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder}, }, - commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError}, + commons::{ + secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts}, + tls_verification::TlsClientDetailsError, + }, constants::secret::SECRET_BASE_PATH, crd::authentication::ldap::v1alpha1::{AuthenticationProvider, FieldNames}, }; @@ -94,7 +97,8 @@ impl AuthenticationProvider { let secret_class = &bind_credentials.secret_class; let volume_name = format!("{secret_class}-bind-credentials"); let volume = bind_credentials - .to_volume(&volume_name) + // We need the private LDAP bind credentials + .to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate) .context(BindCredentialsSnafu)?; volumes.push(volume); @@ -234,7 +238,10 @@ mod tests { secret_class: "ldap-ca-cert".to_string(), scope: None, } - .to_volume("ldap-ca-cert-ca-cert") + .to_volume( + "ldap-ca-cert-ca-cert", + SecretClassVolumeProvisionParts::Public + ) .unwrap() ] ); @@ -263,13 +270,19 @@ mod tests { secret_class: "openldap-bind-credentials".to_string(), scope: None, } - .to_volume("openldap-bind-credentials-bind-credentials") + .to_volume( + "openldap-bind-credentials-bind-credentials", + SecretClassVolumeProvisionParts::PublicPrivate + ) .unwrap(), SecretClassVolume { secret_class: "ldap-ca-cert".to_string(), scope: None, } - .to_volume("ldap-ca-cert-ca-cert") + .to_volume( + "ldap-ca-cert-ca-cert", + SecretClassVolumeProvisionParts::Public + ) .unwrap() ] ); diff --git a/crates/stackable-operator/src/crd/s3/connection/mod.rs b/crates/stackable-operator/src/crd/s3/connection/mod.rs index b41c280d8..bf0ccad7e 100644 --- a/crates/stackable-operator/src/crd/s3/connection/mod.rs +++ b/crates/stackable-operator/src/crd/s3/connection/mod.rs @@ -174,10 +174,16 @@ mod tests { .unwrap() .annotations .unwrap(), - &BTreeMap::from([( - "secrets.stackable.tech/class".to_string(), - "ionos-s3-credentials".to_string() - )]), + &BTreeMap::from([ + ( + "secrets.stackable.tech/class".to_string(), + "ionos-s3-credentials".to_string() + ), + ( + "secrets.stackable.tech/provision-parts".to_string(), + "public-private".to_string() + ) + ]), ); assert_eq!(mount.name, volume.name); diff --git a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs index 0c6c1efd7..78f5c701f 100644 --- a/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs +++ b/crates/stackable-operator/src/crd/s3/connection/v1alpha1_impl.rs @@ -5,7 +5,10 @@ use url::Url; use crate::{ builder::pod::{PodBuilder, container::ContainerBuilder, volume::VolumeMountBuilder}, client::Client, - commons::{secret_class::SecretClassVolumeError, tls_verification::TlsClientDetailsError}, + commons::{ + secret_class::{SecretClassVolumeError, SecretClassVolumeProvisionParts}, + tls_verification::TlsClientDetailsError, + }, constants::secret::SECRET_BASE_PATH, crd::s3::{ connection::ResolvedConnection, @@ -110,7 +113,8 @@ impl ConnectionSpec { volumes.push( credentials - .to_volume(&volume_name) + // We need the private S3 credentials + .to_volume(&volume_name, SecretClassVolumeProvisionParts::PublicPrivate) .context(AddS3CredentialVolumesSnafu)?, ); mounts.push( diff --git a/crates/stackable-operator/src/kvp/annotation/mod.rs b/crates/stackable-operator/src/kvp/annotation/mod.rs index efe7ec388..402fa362b 100644 --- a/crates/stackable-operator/src/kvp/annotation/mod.rs +++ b/crates/stackable-operator/src/kvp/annotation/mod.rs @@ -19,6 +19,7 @@ use delegate::delegate; use crate::{ builder::pod::volume::SecretOperatorVolumeScope, + commons::secret_class::SecretClassVolumeProvisionParts, iter::TryFromIterator, kvp::{Key, KeyValuePair, KeyValuePairError, KeyValuePairs, KeyValuePairsError}, }; @@ -80,6 +81,15 @@ impl Annotation { self.0 } + /// Constructs a `secrets.stackable.tech/provision-parts` annotation. + pub fn secret_provision_parts( + provision_parts: &SecretClassVolumeProvisionParts, + ) -> Result { + let kvp = + KeyValuePair::try_from(("secrets.stackable.tech/provision-parts", provision_parts))?; + Ok(Self(kvp)) + } + /// Constructs a `secrets.stackable.tech/class` annotation. pub fn secret_class(secret_class: &str) -> Result { let kvp = KeyValuePair::try_from(("secrets.stackable.tech/class", secret_class))?;