From 980acfeaa70e8e4d4b67943018018fe5b8da0c3e Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 18 Mar 2025 14:13:50 +0100 Subject: [PATCH 01/12] feat: Add format-specific annotations to override secret file names --- rust/operator-binary/src/backend/mod.rs | 73 ++++++++++++++++++- rust/operator-binary/src/csi_server/node.rs | 20 ++++- rust/operator-binary/src/format/mod.rs | 6 +- rust/operator-binary/src/format/well_known.rs | 34 ++++++--- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 91de3932..dc1a566d 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -25,7 +25,13 @@ use stackable_operator::{ pub use tls::TlsGenerate; use self::pod_info::SchedulingPodInfo; -use crate::format::{SecretData, SecretFormat}; +use crate::format::{ + well_known::{ + FILE_PEM_CERT_CA, FILE_PEM_CERT_CERT, FILE_PEM_CERT_KEY, FILE_PKCS12_CERT_KEYSTORE, + FILE_PKCS12_CERT_TRUSTSTORE, + }, + SecretData, SecretFormat, +}; /// Configuration provided by the `Volume` selecting what secret data should be provided /// @@ -96,6 +102,51 @@ pub struct SecretVolumeSelector { )] pub compat_tls_pkcs12_password: Option, + /// An alternative name used for the TLS PKCS#12 keystore file. + /// + /// Has no effect if the `format` is not `tls-pkcs12`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pkcs12.keystore-name", + default = "default_pkcs12_keystore_name" + )] + pub tls_pkcs12_keystore_name: String, + + /// An alternative name used for the TLS PKCS#12 keystore file. + /// + /// Has no effect if the `format` is not `tls-pkcs12`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pkcs12.truststore-name", + default = "default_pkcs12_truststore_name" + )] + pub tls_pkcs12_truststore_name: String, + + /// An alternative name used for the TLS PEM certificate. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.cert-name", + default = "default_tls_pem_cert_name" + )] + pub tls_pem_cert_name: String, + + /// An alternative name used for the TLS PEM certificate key. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.key-name", + default = "default_tls_pem_key_name" + )] + pub tls_pem_key_name: String, + + /// An alternative name used for the TLS PEM certificate authority. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.ca-name", + default = "default_tls_pem_ca_name" + )] + pub tls_pem_ca_name: String, + /// The TLS cert lifetime (when using the [`tls`] backend). /// The format is documented in . #[serde( @@ -164,6 +215,26 @@ fn default_cert_jitter_factor() -> f64 { tls::DEFAULT_CERT_JITTER_FACTOR } +fn default_pkcs12_keystore_name() -> String { + FILE_PKCS12_CERT_KEYSTORE.to_owned() +} + +fn default_pkcs12_truststore_name() -> String { + FILE_PKCS12_CERT_TRUSTSTORE.to_owned() +} + +fn default_tls_pem_cert_name() -> String { + FILE_PEM_CERT_CERT.to_owned() +} + +fn default_tls_pem_key_name() -> String { + FILE_PEM_CERT_KEY.to_owned() +} + +fn default_tls_pem_ca_name() -> String { + FILE_PEM_CERT_CA.to_owned() +} + #[derive(Snafu, Debug)] #[snafu(module)] pub enum ScopeAddressesError { diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index f847e8c8..4b8644e3 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -23,9 +23,15 @@ use tonic::{Request, Response, Status}; use super::controller::TOPOLOGY_NODE; use crate::{ backend::{ - self, pod_info, pod_info::PodInfo, SecretBackendError, SecretContents, SecretVolumeSelector, + self, + pod_info::{self, PodInfo}, + SecretBackendError, SecretContents, SecretVolumeSelector, + }, + format::{ + self, + well_known::{CompatibilityOptions, NamingOptions}, + SecretFormat, }, - format::{self, well_known::CompatibilityOptions, SecretFormat}, grpc::csi::v1::{ node_server::Node, NodeExpandVolumeRequest, NodeExpandVolumeResponse, NodeGetCapabilitiesRequest, NodeGetCapabilitiesResponse, NodeGetInfoRequest, @@ -209,6 +215,7 @@ impl SecretProvisionerNode { target_path: &Path, data: SecretContents, format: Option, + names: &NamingOptions, compat: &CompatibilityOptions, ) -> Result<(), PublishError> { let create_secret = { @@ -223,7 +230,7 @@ impl SecretProvisionerNode { }; for (k, v) in data .data - .into_files(format, compat) + .into_files(format, names, compat) .context(publish_error::FormatDataSnafu)? { let item_path = target_path.join(k); @@ -384,6 +391,13 @@ impl Node for SecretProvisionerNode { &target_path, data, selector.format, + &NamingOptions { + tls_pkcs12_keystore_name: selector.tls_pkcs12_keystore_name, + tls_pkcs12_truststore_name: selector.tls_pkcs12_truststore_name, + tls_pem_cert_name: selector.tls_pem_cert_name, + tls_pem_key_name: selector.tls_pem_key_name, + tls_pem_ca_name: selector.tls_pem_ca_name, + }, &CompatibilityOptions { tls_pkcs12_password: selector.compat_tls_pkcs12_password, }, diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index d35eeeea..4fd7bbc6 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -7,6 +7,7 @@ pub use self::{ convert::ConvertError, well_known::{FromFilesError as ParseError, SecretFormat, WellKnownSecretData}, }; +use crate::format::well_known::NamingOptions; mod convert; mod utils; @@ -30,13 +31,14 @@ impl SecretData { pub fn into_files( self, format: Option, + names: &NamingOptions, compat: &CompatibilityOptions, ) -> Result { if let Some(format) = format { - Ok(self.parse()?.convert_to(format, compat)?.into_files()) + Ok(self.parse()?.convert_to(format, compat)?.into_files(names)) } else { Ok(match self { - SecretData::WellKnown(data) => data.into_files(), + 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 0e3c0e0d..9744cf51 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -4,12 +4,12 @@ use strum::EnumDiscriminants; use super::{convert, ConvertError, SecretFiles}; -const FILE_PEM_CERT_CERT: &str = "tls.crt"; -const FILE_PEM_CERT_KEY: &str = "tls.key"; -const FILE_PEM_CERT_CA: &str = "ca.crt"; +pub const FILE_PEM_CERT_CERT: &str = "tls.crt"; +pub const FILE_PEM_CERT_KEY: &str = "tls.key"; +pub const FILE_PEM_CERT_CA: &str = "ca.crt"; -const FILE_PKCS12_CERT_KEYSTORE: &str = "keystore.p12"; -const FILE_PKCS12_CERT_TRUSTSTORE: &str = "truststore.p12"; +pub const FILE_PKCS12_CERT_KEYSTORE: &str = "keystore.p12"; +pub const FILE_PKCS12_CERT_TRUSTSTORE: &str = "truststore.p12"; const FILE_KERBEROS_KEYTAB_KEYTAB: &str = "keytab"; const FILE_KERBEROS_KEYTAB_KRB5_CONF: &str = "krb5.conf"; @@ -46,24 +46,24 @@ pub enum WellKnownSecretData { } impl WellKnownSecretData { - pub fn into_files(self) -> SecretFiles { + pub fn into_files(self, names: &NamingOptions) -> SecretFiles { match self { WellKnownSecretData::TlsPem(TlsPem { certificate_pem, key_pem, ca_pem, }) => [ - (FILE_PEM_CERT_CERT.to_string(), certificate_pem), - (FILE_PEM_CERT_KEY.to_string(), key_pem), - (FILE_PEM_CERT_CA.to_string(), ca_pem), + (names.tls_pem_cert_name.to_string(), certificate_pem), + (names.tls_pem_key_name.to_string(), key_pem), + (names.tls_pem_ca_name.to_string(), ca_pem), ] .into(), WellKnownSecretData::TlsPkcs12(TlsPkcs12 { keystore, truststore, }) => [ - (FILE_PKCS12_CERT_KEYSTORE.to_string(), keystore), - (FILE_PKCS12_CERT_TRUSTSTORE.to_string(), truststore), + (names.tls_pkcs12_keystore_name.to_string(), keystore), + (names.tls_pkcs12_truststore_name.to_string(), truststore), ] .into(), WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => [ @@ -123,6 +123,18 @@ pub struct CompatibilityOptions { pub tls_pkcs12_password: Option, } +/// Options to customize the well-known format file names. +/// +/// The fields will either contain the default value or the custom user-provided one. This is also +/// the reason why the fields are not wrapped in [`Option`]. +pub struct NamingOptions { + pub tls_pkcs12_keystore_name: String, + pub tls_pkcs12_truststore_name: String, + pub tls_pem_cert_name: String, + pub tls_pem_key_name: String, + pub tls_pem_ca_name: String, +} + #[derive(Snafu, Debug)] #[snafu(module)] pub enum FromFilesError { From 6514f14fc3b3c7212a4e7667c7d88f48becd6229 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 18 Mar 2025 14:23:38 +0100 Subject: [PATCH 02/12] docs: Add annotations to volume page --- .../modules/secret-operator/pages/volume.adoc | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/modules/secret-operator/pages/volume.adoc b/docs/modules/secret-operator/pages/volume.adoc index 1a6becee..38d7608e 100644 --- a/docs/modules/secret-operator/pages/volume.adoc +++ b/docs/modules/secret-operator/pages/volume.adoc @@ -50,6 +50,61 @@ The xref:secretclass.adoc#format[format] that the secret should be written as. This can be either the default output format of the xref:secretclass.adoc#backend[backend], or a format that it defines a conversion into. +=== `secrets.stackable.tech/format.tls-pkcs12.keystore-name` + +*Required*: false + +*Default value*: `keystore.p12` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +An alternative name for the keystore file. +Has no effect if the `format` is not `tls-pkcs12`. + +=== `secrets.stackable.tech/format.tls-pkcs12.truststore-name` + +*Required*: false + +*Default value*: `truststore.p12` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +An alternative name for the truststore file. +Has no effect if the `format` is not `tls-pkcs12`. + +=== `secrets.stackable.tech/format.tls-pem.cert-name` + +*Required*: false + +*Default value*: `tls.crt` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +An alternative name for TLS PEM certificate. +Has no effect if the `format` is not `tls-pem`. + +=== `secrets.stackable.tech/format.tls-pem.key-name` + +*Required*: false + +*Default value*: `tls.key` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +An alternative name for TLS PEM certificate key. +Has no effect if the `format` is not `tls-pem`. + +=== `secrets.stackable.tech/format.tls-pem.ca-name` + +*Required*: false + +*Default value*: `ca.crt` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +An alternative name for TLS PEM certificate authority. +Has no effect if the `format` is not `tls-pem`. + === `secrets.stackable.tech/backend.autotls.cert.lifetime` *Required*: false From f8293792fa619c899448a9d1f3e12880b3c9e147 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 18 Mar 2025 15:18:55 +0100 Subject: [PATCH 03/12] test: Add custom secret name tests --- .../tls/{consumer.yaml => consumer.yaml.j2} | 24 ++++++++++++++----- tests/test-definition.yaml | 5 ++++ 2 files changed, 23 insertions(+), 6 deletions(-) rename tests/templates/kuttl/tls/{consumer.yaml => consumer.yaml.j2} (72%) diff --git a/tests/templates/kuttl/tls/consumer.yaml b/tests/templates/kuttl/tls/consumer.yaml.j2 similarity index 72% rename from tests/templates/kuttl/tls/consumer.yaml rename to tests/templates/kuttl/tls/consumer.yaml.j2 index dcd78cef..a734905c 100644 --- a/tests/templates/kuttl/tls/consumer.yaml +++ b/tests/templates/kuttl/tls/consumer.yaml.j2 @@ -15,14 +15,20 @@ spec: args: - -c - | +{% if test_scenario['values']['custom-secret-names'] %} + CERT_NAME=custom-tls.crt +{% else %} + CERT_NAME=tls.crt +{% endif %} + - | set -euo pipefail ls -la /stackable/tls-3d ls -la /stackable/tls-42h - cat /stackable/tls-3d/tls.crt | openssl x509 -noout -text - cat /stackable/tls-42h/tls.crt | openssl x509 -noout -text + cat "/stackable/tls-3d/$CERT_NAME" | openssl x509 -noout -text + cat "/stackable/tls-42h/$CERT_NAME" | openssl x509 -noout -text - notAfter=`cat /stackable/tls-3d/tls.crt | openssl x509 -noout -enddate| sed -e 's#notAfter=##'` + notAfter=$(cat /stackable/tls-3d/$CERT_NAME | openssl x509 -noout -enddate| sed -e 's#notAfter=##') notAfterDate=`date -d "${notAfter}" '+%s'` nowDate=`date '+%s'` diff="$((${notAfterDate}-${nowDate}))" @@ -30,7 +36,7 @@ spec: if test "${diff}" -lt "$((57*3600))"; then echo "Cert had a lifetime of less than 57 hours!" && exit 1; fi if test "${diff}" -gt "$((72*3600))"; then echo "Cert had a lifetime of greater than 72 hours!" && exit 1; fi - notAfter=`cat /stackable/tls-42h/tls.crt | openssl x509 -noout -enddate| sed -e 's#notAfter=##'` + notAfter=$(cat /stackable/tls-42h/$CERT_NAME | openssl x509 -noout -enddate| sed -e 's#notAfter=##') notAfterDate=`date -d "${notAfter}" '+%s'` nowDate=`date '+%s'` diff="$((${notAfterDate}-${nowDate}))" @@ -38,8 +44,8 @@ spec: if test "${diff}" -lt "$((33*3600))"; then echo "Cert had a lifetime of less than 33 hours!" && exit 1; fi if test "${diff}" -gt "$((42*3600))"; then echo "Cert had a lifetime of greater than 42 hours!" && exit 1; fi - cat /stackable/tls-3d/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" - cat /stackable/tls-42h/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" + cat "/stackable/tls-3d/$CERT_NAME" | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" + cat "/stackable/tls-42h/$CERT_NAME" | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" volumeMounts: - mountPath: /stackable/tls-3d name: tls-3d @@ -54,6 +60,9 @@ spec: secrets.stackable.tech/class: tls-$NAMESPACE secrets.stackable.tech/scope: node,pod,service=my-tls-service secrets.stackable.tech/backend.autotls.cert.lifetime: 3d +{% if test_scenario['values']['custom-secret-names'] %} + secrets.stackable.tech/format.tls-pem.cert-name: custom-tls.crt +{% endif %} spec: storageClassName: secrets.stackable.tech accessModes: @@ -69,6 +78,9 @@ spec: secrets.stackable.tech/class: tls-$NAMESPACE-42h secrets.stackable.tech/scope: node,pod,service=my-tls-service secrets.stackable.tech/backend.autotls.cert.lifetime: 31d +{% if test_scenario['values']['custom-secret-names'] %} + secrets.stackable.tech/format.tls-pem.cert-name: custom-tls.crt +{% endif %} spec: storageClassName: secrets.stackable.tech accessModes: diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 91cdc540..892153e2 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -15,6 +15,10 @@ dimensions: - 2048 - 3072 # - 4096 + - name: custom-secret-names + values: + - false + - true tests: - name: kerberos dimensions: @@ -30,6 +34,7 @@ tests: - openshift - name: tls dimensions: + - custom-secret-names - rsa-key-length - openshift - name: cert-manager-tls From a7718e00e8bf7729e299b79897c3bd73118ccffc Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 18 Mar 2025 15:41:19 +0100 Subject: [PATCH 04/12] feat: Add check against path traversal --- rust/operator-binary/src/csi_server/node.rs | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index 4b8644e3..8d1e8e76 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -6,7 +6,7 @@ use std::{ use openssl::sha::Sha256; use serde::{de::IntoDeserializer, Deserialize}; -use snafu::{ResultExt, Snafu}; +use snafu::{ensure, ResultExt, Snafu}; use stackable_operator::{ builder::meta::ObjectMetaBuilder, k8s_openapi::api::core::v1::Pod, @@ -98,6 +98,18 @@ enum PublishError { path: PathBuf, }, + #[snafu(display("failed to canonicalize path {}", path.display()))] + CanonicalizePath { + source: std::io::Error, + path: PathBuf, + }, + + #[snafu(display("encountered invalid path base in {}, expected {}", path.display(), expected_base.display()))] + InvalidPathBase { + path: PathBuf, + expected_base: PathBuf, + }, + #[snafu(display("failed to tag pod with expiry metadata"))] TagPod { source: stackable_operator::client::Error, @@ -126,6 +138,8 @@ impl From for Status { PublishError::SetDirPermissions { .. } => Status::unavailable(full_msg), PublishError::CreateFile { .. } => Status::unavailable(full_msg), PublishError::WriteFile { .. } => Status::unavailable(full_msg), + PublishError::CanonicalizePath { .. } => Status::unavailable(full_msg), + PublishError::InvalidPathBase { .. } => Status::unavailable(full_msg), PublishError::TagPod { .. } => Status::unavailable(full_msg), PublishError::BuildAnnotation { .. } => Status::unavailable(full_msg), } @@ -234,6 +248,22 @@ impl SecretProvisionerNode { .context(publish_error::FormatDataSnafu)? { let item_path = target_path.join(k); + + // Prevent unwanted path traversals by first canonicalizing the final + // path and then validating that the path starts with the base we + // expect. + let item_path = item_path + .canonicalize() + .context(publish_error::CanonicalizePathSnafu { path: &item_path })?; + + ensure!( + item_path.starts_with(target_path), + publish_error::InvalidPathBaseSnafu { + path: &item_path, + expected_base: &target_path + } + ); + if let Some(item_path_parent) = item_path.parent() { create_dir_all(item_path_parent) .await From 83fea671e00ffbe419c5bc4cd85c0190a5e5b5e1 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Mar 2025 10:47:12 +0100 Subject: [PATCH 05/12] refactor: Flatten struct, use moved value --- rust/operator-binary/src/backend/mod.rs | 75 +---------------- rust/operator-binary/src/csi_server/node.rs | 10 +-- rust/operator-binary/src/format/mod.rs | 2 +- rust/operator-binary/src/format/well_known.rs | 82 ++++++++++++++++--- 4 files changed, 78 insertions(+), 91 deletions(-) diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index dc1a566d..ddc4b62f 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -25,13 +25,7 @@ use stackable_operator::{ pub use tls::TlsGenerate; use self::pod_info::SchedulingPodInfo; -use crate::format::{ - well_known::{ - FILE_PEM_CERT_CA, FILE_PEM_CERT_CERT, FILE_PEM_CERT_KEY, FILE_PKCS12_CERT_KEYSTORE, - FILE_PKCS12_CERT_TRUSTSTORE, - }, - SecretData, SecretFormat, -}; +use crate::format::{well_known::NamingOptions, SecretData, SecretFormat}; /// Configuration provided by the `Volume` selecting what secret data should be provided /// @@ -102,50 +96,9 @@ pub struct SecretVolumeSelector { )] pub compat_tls_pkcs12_password: Option, - /// An alternative name used for the TLS PKCS#12 keystore file. - /// - /// Has no effect if the `format` is not `tls-pkcs12`. - #[serde( - rename = "secrets.stackable.tech/format.tls-pkcs12.keystore-name", - default = "default_pkcs12_keystore_name" - )] - pub tls_pkcs12_keystore_name: String, - - /// An alternative name used for the TLS PKCS#12 keystore file. - /// - /// Has no effect if the `format` is not `tls-pkcs12`. - #[serde( - rename = "secrets.stackable.tech/format.tls-pkcs12.truststore-name", - default = "default_pkcs12_truststore_name" - )] - pub tls_pkcs12_truststore_name: String, - - /// An alternative name used for the TLS PEM certificate. - /// - /// Has no effect if the `format` is not `tls-pem`. - #[serde( - rename = "secrets.stackable.tech/format.tls-pem.cert-name", - default = "default_tls_pem_cert_name" - )] - pub tls_pem_cert_name: String, - - /// An alternative name used for the TLS PEM certificate key. - /// - /// Has no effect if the `format` is not `tls-pem`. - #[serde( - rename = "secrets.stackable.tech/format.tls-pem.key-name", - default = "default_tls_pem_key_name" - )] - pub tls_pem_key_name: String, - - /// An alternative name used for the TLS PEM certificate authority. - /// - /// Has no effect if the `format` is not `tls-pem`. - #[serde( - rename = "secrets.stackable.tech/format.tls-pem.ca-name", - default = "default_tls_pem_ca_name" - )] - pub tls_pem_ca_name: String, + /// The (custom) filenames used by secrets. + #[serde(flatten)] + pub names: NamingOptions, /// The TLS cert lifetime (when using the [`tls`] backend). /// The format is documented in . @@ -215,26 +168,6 @@ fn default_cert_jitter_factor() -> f64 { tls::DEFAULT_CERT_JITTER_FACTOR } -fn default_pkcs12_keystore_name() -> String { - FILE_PKCS12_CERT_KEYSTORE.to_owned() -} - -fn default_pkcs12_truststore_name() -> String { - FILE_PKCS12_CERT_TRUSTSTORE.to_owned() -} - -fn default_tls_pem_cert_name() -> String { - FILE_PEM_CERT_CERT.to_owned() -} - -fn default_tls_pem_key_name() -> String { - FILE_PEM_CERT_KEY.to_owned() -} - -fn default_tls_pem_ca_name() -> String { - FILE_PEM_CERT_CA.to_owned() -} - #[derive(Snafu, Debug)] #[snafu(module)] pub enum ScopeAddressesError { diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index 8d1e8e76..2a51dd6f 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -229,7 +229,7 @@ impl SecretProvisionerNode { target_path: &Path, data: SecretContents, format: Option, - names: &NamingOptions, + names: NamingOptions, compat: &CompatibilityOptions, ) -> Result<(), PublishError> { let create_secret = { @@ -421,13 +421,7 @@ impl Node for SecretProvisionerNode { &target_path, data, selector.format, - &NamingOptions { - tls_pkcs12_keystore_name: selector.tls_pkcs12_keystore_name, - tls_pkcs12_truststore_name: selector.tls_pkcs12_truststore_name, - tls_pem_cert_name: selector.tls_pem_cert_name, - tls_pem_key_name: selector.tls_pem_key_name, - tls_pem_ca_name: selector.tls_pem_ca_name, - }, + selector.names, &CompatibilityOptions { tls_pkcs12_password: selector.compat_tls_pkcs12_password, }, diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index 4fd7bbc6..70e53d4d 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -31,7 +31,7 @@ impl SecretData { pub fn into_files( self, format: Option, - names: &NamingOptions, + names: NamingOptions, compat: &CompatibilityOptions, ) -> Result { if let Some(format) = format { diff --git a/rust/operator-binary/src/format/well_known.rs b/rust/operator-binary/src/format/well_known.rs index 9744cf51..982825cb 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -4,12 +4,12 @@ use strum::EnumDiscriminants; use super::{convert, ConvertError, SecretFiles}; -pub const FILE_PEM_CERT_CERT: &str = "tls.crt"; -pub const FILE_PEM_CERT_KEY: &str = "tls.key"; -pub const FILE_PEM_CERT_CA: &str = "ca.crt"; +const FILE_PEM_CERT_CERT: &str = "tls.crt"; +const FILE_PEM_CERT_KEY: &str = "tls.key"; +const FILE_PEM_CERT_CA: &str = "ca.crt"; -pub const FILE_PKCS12_CERT_KEYSTORE: &str = "keystore.p12"; -pub const FILE_PKCS12_CERT_TRUSTSTORE: &str = "truststore.p12"; +const FILE_PKCS12_CERT_KEYSTORE: &str = "keystore.p12"; +const FILE_PKCS12_CERT_TRUSTSTORE: &str = "truststore.p12"; const FILE_KERBEROS_KEYTAB_KEYTAB: &str = "keytab"; const FILE_KERBEROS_KEYTAB_KRB5_CONF: &str = "krb5.conf"; @@ -46,24 +46,24 @@ pub enum WellKnownSecretData { } impl WellKnownSecretData { - pub fn into_files(self, names: &NamingOptions) -> SecretFiles { + pub fn into_files(self, names: NamingOptions) -> SecretFiles { match self { WellKnownSecretData::TlsPem(TlsPem { certificate_pem, key_pem, ca_pem, }) => [ - (names.tls_pem_cert_name.to_string(), certificate_pem), - (names.tls_pem_key_name.to_string(), key_pem), - (names.tls_pem_ca_name.to_string(), ca_pem), + (names.tls_pem_cert_name, certificate_pem), + (names.tls_pem_key_name, key_pem), + (names.tls_pem_ca_name, ca_pem), ] .into(), WellKnownSecretData::TlsPkcs12(TlsPkcs12 { keystore, truststore, }) => [ - (names.tls_pkcs12_keystore_name.to_string(), keystore), - (names.tls_pkcs12_truststore_name.to_string(), truststore), + (names.tls_pkcs12_keystore_name, keystore), + (names.tls_pkcs12_truststore_name, truststore), ] .into(), WellKnownSecretData::Kerberos(Kerberos { keytab, krb5_conf }) => [ @@ -127,14 +127,74 @@ pub struct CompatibilityOptions { /// /// The fields will either contain the default value or the custom user-provided one. This is also /// the reason why the fields are not wrapped in [`Option`]. +#[derive(Debug, Deserialize)] pub struct NamingOptions { + /// An alternative name used for the TLS PKCS#12 keystore file. + /// + /// Has no effect if the `format` is not `tls-pkcs12`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pkcs12.keystore-name", + default = "default_pkcs12_keystore_name" + )] pub tls_pkcs12_keystore_name: String, + + /// An alternative name used for the TLS PKCS#12 keystore file. + /// + /// Has no effect if the `format` is not `tls-pkcs12`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pkcs12.truststore-name", + default = "default_pkcs12_truststore_name" + )] pub tls_pkcs12_truststore_name: String, + + /// An alternative name used for the TLS PEM certificate. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.cert-name", + default = "default_tls_pem_cert_name" + )] pub tls_pem_cert_name: String, + + /// An alternative name used for the TLS PEM certificate key. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.key-name", + default = "default_tls_pem_key_name" + )] pub tls_pem_key_name: String, + + /// An alternative name used for the TLS PEM certificate authority. + /// + /// Has no effect if the `format` is not `tls-pem`. + #[serde( + rename = "secrets.stackable.tech/format.tls-pem.ca-name", + default = "default_tls_pem_ca_name" + )] pub tls_pem_ca_name: String, } +fn default_pkcs12_keystore_name() -> String { + FILE_PKCS12_CERT_KEYSTORE.to_owned() +} + +fn default_pkcs12_truststore_name() -> String { + FILE_PKCS12_CERT_TRUSTSTORE.to_owned() +} + +fn default_tls_pem_cert_name() -> String { + FILE_PEM_CERT_CERT.to_owned() +} + +fn default_tls_pem_key_name() -> String { + FILE_PEM_CERT_KEY.to_owned() +} + +fn default_tls_pem_ca_name() -> String { + FILE_PEM_CERT_CA.to_owned() +} + #[derive(Snafu, Debug)] #[snafu(module)] pub enum FromFilesError { From 94e78c53885c5ed5f0c0253d0ad8f5ce3261fcb8 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Mar 2025 11:27:42 +0100 Subject: [PATCH 06/12] refactor: Flatten compat struct, use moved value --- rust/operator-binary/src/backend/mod.rs | 18 +++++++----------- rust/operator-binary/src/csi_server/node.rs | 7 +++---- rust/operator-binary/src/format/convert.rs | 2 +- rust/operator-binary/src/format/mod.rs | 2 +- rust/operator-binary/src/format/well_known.rs | 12 ++++++++++-- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index ddc4b62f..34a8bc9b 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -25,7 +25,10 @@ use stackable_operator::{ pub use tls::TlsGenerate; use self::pod_info::SchedulingPodInfo; -use crate::format::{well_known::NamingOptions, SecretData, SecretFormat}; +use crate::format::{ + well_known::{CompatibilityOptions, NamingOptions}, + SecretData, SecretFormat, +}; /// Configuration provided by the `Volume` selecting what secret data should be provided /// @@ -85,16 +88,9 @@ pub struct SecretVolumeSelector { )] pub kerberos_service_names: Vec, - /// The password used to encrypt the TLS PKCS#12 keystore - /// - /// Required for some applications that misbehave with blank keystore passwords (such as Hadoop). - /// Has no effect if `format` is not `tls-pkcs12`. - #[serde( - rename = "secrets.stackable.tech/format.compatibility.tls-pkcs12.password", - deserialize_with = "SecretVolumeSelector::deserialize_some", - default - )] - pub compat_tls_pkcs12_password: Option, + /// Compatibility options used by (legacy) applications. + #[serde(flatten)] + pub compat: CompatibilityOptions, /// The (custom) filenames used by secrets. #[serde(flatten)] diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index 2a51dd6f..4080a1b0 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -230,7 +230,7 @@ impl SecretProvisionerNode { data: SecretContents, format: Option, names: NamingOptions, - compat: &CompatibilityOptions, + compat: CompatibilityOptions, ) -> Result<(), PublishError> { let create_secret = { let mut opts = OpenOptions::new(); @@ -420,11 +420,10 @@ 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, - &CompatibilityOptions { - tls_pkcs12_password: selector.compat_tls_pkcs12_password, - }, + selector.compat, ) .await?; Ok(Response::new(NodePublishVolumeResponse {})) diff --git a/rust/operator-binary/src/format/convert.rs b/rust/operator-binary/src/format/convert.rs index 40a9ee6b..fb4e5cc2 100644 --- a/rust/operator-binary/src/format/convert.rs +++ b/rust/operator-binary/src/format/convert.rs @@ -16,7 +16,7 @@ use crate::format::utils::split_pem_certificates; pub fn convert( from: WellKnownSecretData, to: SecretFormat, - compat: &CompatibilityOptions, + compat: CompatibilityOptions, ) -> Result { match (from, to) { // Converting into the current format is always a no-op diff --git a/rust/operator-binary/src/format/mod.rs b/rust/operator-binary/src/format/mod.rs index 70e53d4d..9995a5f2 100644 --- a/rust/operator-binary/src/format/mod.rs +++ b/rust/operator-binary/src/format/mod.rs @@ -32,7 +32,7 @@ impl SecretData { self, format: Option, names: NamingOptions, - compat: &CompatibilityOptions, + compat: CompatibilityOptions, ) -> Result { if let Some(format) = format { Ok(self.parse()?.convert_to(format, compat)?.into_files(names)) diff --git a/rust/operator-binary/src/format/well_known.rs b/rust/operator-binary/src/format/well_known.rs index 982825cb..c56e3eb2 100644 --- a/rust/operator-binary/src/format/well_known.rs +++ b/rust/operator-binary/src/format/well_known.rs @@ -109,7 +109,7 @@ impl WellKnownSecretData { pub fn convert_to( self, to: SecretFormat, - compat: &CompatibilityOptions, + compat: CompatibilityOptions, ) -> Result { convert::convert(self, to, compat) } @@ -118,8 +118,16 @@ impl WellKnownSecretData { /// Options that some (legacy) applications require to ensure compatibility. /// /// The expectation is that this will be unset the vast majority of the time. -#[derive(Default)] +#[derive(Debug, Default, Deserialize)] pub struct CompatibilityOptions { + /// The password used to encrypt the TLS PKCS#12 keystore + /// + /// Required for some applications that misbehave with blank keystore passwords (such as Hadoop). + /// Has no effect if `format` is not `tls-pkcs12`. + #[serde( + rename = "secrets.stackable.tech/format.compatibility.tls-pkcs12.password", + default + )] pub tls_pkcs12_password: Option, } From 2d0a593fa3b31fcf7c17f3c3a7b68d17aa4d081d Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Mar 2025 13:05:28 +0100 Subject: [PATCH 07/12] test: Add unit test for selector deserialization --- rust/operator-binary/src/backend/mod.rs | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 34a8bc9b..61ddb815 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -297,3 +297,55 @@ impl SecretBackendError for Infallible { match *self {} } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use serde::de::{value::MapDeserializer, IntoDeserializer}; + + use super::*; + + fn required_fields_map() -> HashMap { + HashMap::from([ + ( + "secrets.stackable.tech/class".to_owned(), + "my-class".to_owned(), + ), + ( + "csi.storage.k8s.io/pod.name".to_owned(), + "my-pod".to_owned(), + ), + ( + "csi.storage.k8s.io/pod.namespace".to_owned(), + "my-namespace".to_owned(), + ), + ]) + } + + #[test] + fn deserialize_selector() { + let map = required_fields_map(); + + let _ = + SecretVolumeSelector::deserialize::>( + map.into_deserializer(), + ) + .unwrap(); + } + + #[test] + fn deserialize_selector_compat() { + let mut map = required_fields_map(); + map.insert( + "secrets.stackable.tech/format.compatibility.tls-pkcs12.password".to_owned(), + "supersecret".to_owned(), + ); + + let _ = + SecretVolumeSelector::deserialize::>( + map.into_deserializer(), + ) + .unwrap(); + } +} From ad76a260a037e14d721ccb5135b1baca24a248ff Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 11:37:15 +0200 Subject: [PATCH 08/12] refactor: Adjust path traversal checks Path::canonicalize will return an error if the path does not exist. The path we are checking obviously doesn't exist yet, because we want to prevent path traversals and the file at that path will only exist after we are done with the check. So using canonicalize does not work in this use-case. --- rust/operator-binary/src/csi_server/node.rs | 58 ++++++++++++--------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index 4080a1b0..d4f3241e 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -1,7 +1,7 @@ use std::{ fs::Permissions, os::unix::prelude::PermissionsExt, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use openssl::sha::Sha256; @@ -98,17 +98,11 @@ enum PublishError { path: PathBuf, }, - #[snafu(display("failed to canonicalize path {}", path.display()))] - CanonicalizePath { - source: std::io::Error, - path: PathBuf, - }, + #[snafu(display("file path {} must not contain more than one component", path.display()))] + InvalidComponentCount { path: PathBuf }, - #[snafu(display("encountered invalid path base in {}, expected {}", path.display(), expected_base.display()))] - InvalidPathBase { - path: PathBuf, - expected_base: PathBuf, - }, + #[snafu(display("file path {} must not be absolute", path.display()))] + InvalidAbsolutePath { path: PathBuf }, #[snafu(display("failed to tag pod with expiry metadata"))] TagPod { @@ -138,8 +132,8 @@ impl From for Status { PublishError::SetDirPermissions { .. } => Status::unavailable(full_msg), PublishError::CreateFile { .. } => Status::unavailable(full_msg), PublishError::WriteFile { .. } => Status::unavailable(full_msg), - PublishError::CanonicalizePath { .. } => Status::unavailable(full_msg), - PublishError::InvalidPathBase { .. } => Status::unavailable(full_msg), + PublishError::InvalidComponentCount { .. } => Status::unavailable(full_msg), + PublishError::InvalidAbsolutePath { .. } => Status::unavailable(full_msg), PublishError::TagPod { .. } => Status::unavailable(full_msg), PublishError::BuildAnnotation { .. } => Status::unavailable(full_msg), } @@ -247,23 +241,37 @@ impl SecretProvisionerNode { .into_files(format, names, compat) .context(publish_error::FormatDataSnafu)? { - let item_path = target_path.join(k); + // The following few lines of code do some basic checks against + // unwanted path traversals. In the future, we want to leverage + // capability based filesystem operations (openat) to prevent these + // traversals. - // Prevent unwanted path traversals by first canonicalizing the final - // path and then validating that the path starts with the base we - // expect. - let item_path = item_path - .canonicalize() - .context(publish_error::CanonicalizePathSnafu { path: &item_path })?; + // First, let's turn the (potentially custom) file path into a path. + let file_path = PathBuf::from(k); + // Next, ensure the path is not absolute (does not contain root), + // because joining an absolute path with a different path will + // replace the exiting path entirely. ensure!( - item_path.starts_with(target_path), - publish_error::InvalidPathBaseSnafu { - path: &item_path, - expected_base: &target_path - } + !file_path.has_root(), + publish_error::InvalidAbsolutePathSnafu { path: &file_path } ); + // Ensure that the file path only consists of a single normal + // component. This prevents any path traversals up the path using + // '..'. + ensure!( + file_path + .components() + .filter(|c| matches!(c, Component::Normal(_))) + .count() + == 1, + publish_error::InvalidComponentCountSnafu { path: &file_path } + ); + + // Now, we can join the base and file path + let item_path = target_path.join(file_path); + if let Some(item_path_parent) = item_path.parent() { create_dir_all(item_path_parent) .await From 1c2d64ce148f968479acc5071c23d7968250e943 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 11:39:43 +0200 Subject: [PATCH 09/12] test: Add custom CA name --- tests/templates/kuttl/tls/consumer.yaml.j2 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/templates/kuttl/tls/consumer.yaml.j2 b/tests/templates/kuttl/tls/consumer.yaml.j2 index 3508d5ca..4c51e340 100644 --- a/tests/templates/kuttl/tls/consumer.yaml.j2 +++ b/tests/templates/kuttl/tls/consumer.yaml.j2 @@ -17,8 +17,10 @@ spec: - | {% if test_scenario['values']['custom-secret-names'] %} CERT_NAME=custom-tls.crt + CA_NAME=custom-ca.crt {% else %} CERT_NAME=tls.crt + CA_NAME=ca.crt {% endif %} - | set -euo pipefail @@ -52,7 +54,7 @@ spec: subject=$1 while openssl x509 -subject -noout; do :; done \ - < /stackable/tls-3d/ca.crt \ + < "/stackable/tls-3d/$CA_NAME" \ | grep --line-regexp "subject=CN *= *$subject" } @@ -81,6 +83,7 @@ spec: secrets.stackable.tech/backend.autotls.cert.lifetime: 3d {% if test_scenario['values']['custom-secret-names'] %} secrets.stackable.tech/format.tls-pem.cert-name: custom-tls.crt + secrets.stackable.tech/format.tls-pem.ca-name: custom-ca.crt {% endif %} spec: storageClassName: secrets.stackable.tech @@ -99,6 +102,7 @@ spec: secrets.stackable.tech/backend.autotls.cert.lifetime: 31d {% if test_scenario['values']['custom-secret-names'] %} secrets.stackable.tech/format.tls-pem.cert-name: custom-tls.crt + secrets.stackable.tech/format.tls-pem.ca-name: custom-ca.crt {% endif %} spec: storageClassName: secrets.stackable.tech From a9be60f6237d1ea61eb3495bb68bc16020d1e136 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 13:18:53 +0200 Subject: [PATCH 10/12] chore: Use quoted paths in error messages --- rust/operator-binary/src/csi_server/node.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index d4f3241e..ed8c0f48 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -65,13 +65,13 @@ enum PublishError { #[snafu(display("backend failed to get secret data"))] BackendGetSecretData { source: backend::dynamic::DynError }, - #[snafu(display("failed to create secret parent dir {}", path.display()))] + #[snafu(display("failed to create secret parent dir {path:?}"))] CreateDir { source: std::io::Error, path: PathBuf, }, - #[snafu(display("failed to mount volume mount directory {}", path.display()))] + #[snafu(display("failed to mount volume mount directory {path:?}"))] Mount { source: std::io::Error, path: PathBuf, @@ -80,28 +80,28 @@ enum PublishError { #[snafu(display("failed to convert secret data into desired format"))] FormatData { source: format::IntoFilesError }, - #[snafu(display("failed to set volume permissions for {}", path.display()))] + #[snafu(display("failed to set volume permissions for {path:?}"))] SetDirPermissions { source: std::io::Error, path: PathBuf, }, - #[snafu(display("failed to create secret file {}", path.display()))] + #[snafu(display("failed to create secret file {path:?}"))] CreateFile { source: std::io::Error, path: PathBuf, }, - #[snafu(display("failed to write secret file {}", path.display()))] + #[snafu(display("failed to write secret file {path:?}"))] WriteFile { source: std::io::Error, path: PathBuf, }, - #[snafu(display("file path {} must not contain more than one component", path.display()))] + #[snafu(display("file path {path:?} must not contain more than one component"))] InvalidComponentCount { path: PathBuf }, - #[snafu(display("file path {} must not be absolute", path.display()))] + #[snafu(display("file path {path:?} must not be absolute"))] InvalidAbsolutePath { path: PathBuf }, #[snafu(display("failed to tag pod with expiry metadata"))] From 22864d2a45fa65baa4033fcd85b93b27e0e427d7 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 13:42:35 +0200 Subject: [PATCH 11/12] refactor: Ensure all components of a path a normal --- rust/operator-binary/src/csi_server/node.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/rust/operator-binary/src/csi_server/node.rs b/rust/operator-binary/src/csi_server/node.rs index ed8c0f48..43740255 100644 --- a/rust/operator-binary/src/csi_server/node.rs +++ b/rust/operator-binary/src/csi_server/node.rs @@ -98,8 +98,8 @@ enum PublishError { path: PathBuf, }, - #[snafu(display("file path {path:?} must not contain more than one component"))] - InvalidComponentCount { path: PathBuf }, + #[snafu(display("file path {path:?} must only contain normal components"))] + InvalidComponents { path: PathBuf }, #[snafu(display("file path {path:?} must not be absolute"))] InvalidAbsolutePath { path: PathBuf }, @@ -132,7 +132,7 @@ impl From for Status { PublishError::SetDirPermissions { .. } => Status::unavailable(full_msg), PublishError::CreateFile { .. } => Status::unavailable(full_msg), PublishError::WriteFile { .. } => Status::unavailable(full_msg), - PublishError::InvalidComponentCount { .. } => Status::unavailable(full_msg), + PublishError::InvalidComponents { .. } => Status::unavailable(full_msg), PublishError::InvalidAbsolutePath { .. } => Status::unavailable(full_msg), PublishError::TagPod { .. } => Status::unavailable(full_msg), PublishError::BuildAnnotation { .. } => Status::unavailable(full_msg), @@ -257,16 +257,13 @@ impl SecretProvisionerNode { publish_error::InvalidAbsolutePathSnafu { path: &file_path } ); - // Ensure that the file path only consists of a single normal - // component. This prevents any path traversals up the path using - // '..'. + // Ensure that the file path only contains normal components. This + // prevents any path traversals up the path using '..'. ensure!( file_path .components() - .filter(|c| matches!(c, Component::Normal(_))) - .count() - == 1, - publish_error::InvalidComponentCountSnafu { path: &file_path } + .all(|c| matches!(c, Component::Normal(_))), + publish_error::InvalidComponentsSnafu { path: &file_path } ); // Now, we can join the base and file path From dfd732b3e07c2fe4dcfb863310c3d6a3b4c15ff8 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 31 Mar 2025 16:52:46 +0200 Subject: [PATCH 12/12] chore: Add changelog entry --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3b36303..0182347a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +- Add format-specific annotations to override secret file names ([#572]). The following new + annotations are available: + - `secrets.stackable.tech/format.tls-pkcs12.keystore-name` + - `secrets.stackable.tech/format.tls-pkcs12.truststore-name` + - `secrets.stackable.tech/format.tls-pem.cert-name` + - `secrets.stackable.tech/format.tls-pem.key-name` + - `secrets.stackable.tech/format.tls-pem.ca-name` + +[#572]: https://github.com/stackabletech/secret-operator/pull/572 + ## [25.3.0] - 2025-03-21 ### Removed