Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add format-specific annotations to override secret file names #572

Merged
merged 13 commits into from
Mar 31, 2025
Merged
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions docs/modules/secret-operator/pages/volume.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 63 additions & 11 deletions rust/operator-binary/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ use stackable_operator::{
pub use tls::TlsGenerate;

use self::pod_info::SchedulingPodInfo;
use crate::format::{SecretData, SecretFormat};
use crate::format::{
well_known::{CompatibilityOptions, NamingOptions},
SecretData, SecretFormat,
};

/// Configuration provided by the `Volume` selecting what secret data should be provided
///
Expand Down Expand Up @@ -85,16 +88,13 @@ pub struct SecretVolumeSelector {
)]
pub kerberos_service_names: Vec<String>,

/// 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<String>,
/// Compatibility options used by (legacy) applications.
#[serde(flatten)]
pub compat: CompatibilityOptions,

/// 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 <https://docs.stackable.tech/home/nightly/concepts/duration>.
Expand Down Expand Up @@ -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<String, String> {
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::<MapDeserializer<'_, _, serde::de::value::Error>>(
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::<MapDeserializer<'_, _, serde::de::value::Error>>(
map.into_deserializer(),
)
.unwrap();
}
}
72 changes: 57 additions & 15 deletions rust/operator-binary/src/csi_server/node.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use std::{
fs::Permissions,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
path::{Component, Path, PathBuf},
};

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,
Expand All @@ -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,
Expand Down Expand Up @@ -59,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,
Expand All @@ -74,24 +80,30 @@ 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 {path:?} must only contain normal components"))]
InvalidComponents { path: PathBuf },

#[snafu(display("file path {path:?} must not be absolute"))]
InvalidAbsolutePath { path: PathBuf },

#[snafu(display("failed to tag pod with expiry metadata"))]
TagPod {
source: stackable_operator::client::Error,
Expand Down Expand Up @@ -120,6 +132,8 @@ impl From<PublishError> for Status {
PublishError::SetDirPermissions { .. } => Status::unavailable(full_msg),
PublishError::CreateFile { .. } => Status::unavailable(full_msg),
PublishError::WriteFile { .. } => 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),
}
Expand Down Expand Up @@ -209,7 +223,8 @@ impl SecretProvisionerNode {
target_path: &Path,
data: SecretContents,
format: Option<SecretFormat>,
compat: &CompatibilityOptions,
names: NamingOptions,
compat: CompatibilityOptions,
) -> Result<(), PublishError> {
let create_secret = {
let mut opts = OpenOptions::new();
Expand All @@ -223,10 +238,37 @@ 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);
// 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.

// 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!(
!file_path.has_root(),
publish_error::InvalidAbsolutePathSnafu { path: &file_path }
);

// Ensure that the file path only contains normal components. This
// prevents any path traversals up the path using '..'.
ensure!(
file_path
.components()
.all(|c| matches!(c, Component::Normal(_))),
publish_error::InvalidComponentsSnafu { 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
Expand Down Expand Up @@ -383,10 +425,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,
&CompatibilityOptions {
tls_pkcs12_password: selector.compat_tls_pkcs12_password,
},
selector.names,
selector.compat,
)
.await?;
Ok(Response::new(NodePublishVolumeResponse {}))
Expand Down
2 changes: 1 addition & 1 deletion rust/operator-binary/src/format/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::format::utils::split_pem_certificates;
pub fn convert(
from: WellKnownSecretData,
to: SecretFormat,
compat: &CompatibilityOptions,
compat: CompatibilityOptions,
) -> Result<WellKnownSecretData, ConvertError> {
match (from, to) {
// Converting into the current format is always a no-op
Expand Down
8 changes: 5 additions & 3 deletions rust/operator-binary/src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,13 +31,14 @@ impl SecretData {
pub fn into_files(
self,
format: Option<SecretFormat>,
compat: &CompatibilityOptions,
names: NamingOptions,
compat: CompatibilityOptions,
) -> Result<SecretFiles, IntoFilesError> {
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,
})
}
Expand Down
Loading