Skip to content

Commit af490f1

Browse files
feat: Allow the specification of additional trust roots
1 parent 12d0b17 commit af490f1

File tree

5 files changed

+105
-7
lines changed

5 files changed

+105
-7
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
1414
- Made RSA key length configurable for certificates issued by cert-manager ([#528]).
1515
- Kerberos principal backends now also provision principals for IP address, not just DNS hostnames ([#552]).
1616
- OLM deployment helper ([#546]).
17+
- Allow the specification of additional trust roots in autoTls SecretClasses ([#573]).
1718

1819
### Changed
1920

@@ -38,6 +39,7 @@ All notable changes to this project will be documented in this file.
3839
[#566]: https://github.com/stackabletech/secret-operator/pull/566
3940
[#569]: https://github.com/stackabletech/secret-operator/pull/569
4041
[#571]: https://github.com/stackabletech/secret-operator/pull/571
42+
[#573]: https://github.com/stackabletech/secret-operator/pull/573
4143

4244
## [24.11.1] - 2025-01-10
4345

rust/operator-binary/src/backend/dynamic.rs

+2
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,13 @@ pub async fn from_class(
118118
}
119119
crd::SecretClassBackend::AutoTls(crd::AutoTlsBackend {
120120
ca,
121+
additional_trust_roots,
121122
max_certificate_lifetime,
122123
}) => from(
123124
super::TlsGenerate::get_or_create_k8s_certificate(
124125
client,
125126
&ca,
127+
&additional_trust_roots,
126128
max_certificate_lifetime,
127129
)
128130
.await?,

rust/operator-binary/src/backend/tls/ca.rs

+72-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Dynamically provisions and picks Certificate Authorities.
22
3-
use std::{collections::BTreeMap, fmt::Display};
3+
use std::{collections::BTreeMap, ffi::OsStr, fmt::Display, path::Path};
44

55
use openssl::{
66
asn1::{Asn1Integer, Asn1Time},
@@ -34,7 +34,7 @@ use tracing::{info, info_span, warn};
3434

3535
use crate::{
3636
backend::SecretBackendError,
37-
crd::CertificateKeyGeneration,
37+
crd::{AdditionalTrustRoot, CertificateKeyGeneration},
3838
utils::{asn1time_to_offsetdatetime, Asn1TimeParseError, Unloggable},
3939
};
4040

@@ -55,8 +55,8 @@ pub enum Error {
5555
#[snafu(display("failed to generate certificate key"))]
5656
GenerateKey { source: openssl::error::ErrorStack },
5757

58-
#[snafu(display("failed to load CA {secret}"))]
59-
FindCa {
58+
#[snafu(display("failed to load {secret}"))]
59+
FindSecret {
6060
source: kube::Error,
6161
secret: ObjectRef<Secret>,
6262
},
@@ -77,6 +77,12 @@ pub enum Error {
7777
secret: ObjectRef<Secret>,
7878
},
7979

80+
#[snafu(display("unsupported certificate format in key {key:?} of {secret}; supported extensions: .cer, .cert, .crt, .pem"))]
81+
UnsupportedCertificateFormat {
82+
key: String,
83+
secret: ObjectRef<Secret>,
84+
},
85+
8086
#[snafu(display("failed to parse CA lifetime from key {key:?} of {secret}"))]
8187
ParseLifetime {
8288
source: Asn1TimeParseError,
@@ -106,9 +112,10 @@ impl SecretBackendError for Error {
106112
match self {
107113
Error::GenerateKey { .. } => tonic::Code::Internal,
108114
Error::MissingCertificate { .. } => tonic::Code::FailedPrecondition,
109-
Error::FindCa { .. } => tonic::Code::Unavailable,
115+
Error::FindSecret { .. } => tonic::Code::Unavailable,
110116
Error::CaNotFoundAndGenDisabled { .. } => tonic::Code::FailedPrecondition,
111117
Error::LoadCertificate { .. } => tonic::Code::FailedPrecondition,
118+
Error::UnsupportedCertificateFormat { .. } => tonic::Code::InvalidArgument,
112119
Error::ParseLifetime { .. } => tonic::Code::FailedPrecondition,
113120
Error::BuildCertificate { .. } => tonic::Code::FailedPrecondition,
114121
Error::SerializeCertificate { .. } => tonic::Code::FailedPrecondition,
@@ -293,20 +300,22 @@ impl CertificateAuthority {
293300
#[derive(Debug)]
294301
pub struct Manager {
295302
certificate_authorities: Vec<CertificateAuthority>,
303+
additional_trusted_certificates: Vec<X509>,
296304
}
297305

298306
impl Manager {
299307
pub async fn load_or_create(
300308
client: &stackable_operator::client::Client,
301309
secret_ref: &SecretReference,
310+
additional_trust_roots: &[AdditionalTrustRoot],
302311
config: &Config,
303312
) -> Result<Self> {
304313
// Use entry API rather than apply so that we crash and retry on conflicts (to avoid creating spurious certs that we throw away immediately)
305314
let secrets_api = &client.get_api::<Secret>(&secret_ref.namespace);
306315
let ca_secret = secrets_api
307316
.entry(&secret_ref.name)
308317
.await
309-
.with_context(|_| FindCaSnafu { secret: secret_ref })?;
318+
.with_context(|_| FindSecretSnafu { secret: secret_ref })?;
310319
let mut update_ca_secret = false;
311320
let mut certificate_authorities = match &ca_secret {
312321
Entry::Occupied(ca_secret) => {
@@ -441,11 +450,67 @@ impl Manager {
441450
return SaveRequestedButForbiddenSnafu.fail();
442451
}
443452
}
453+
454+
let mut additional_trusted_certificates = vec![];
455+
for AdditionalTrustRoot { secret } in additional_trust_roots {
456+
additional_trusted_certificates
457+
.extend(Self::read_certificates_from_secret(client, secret).await?);
458+
}
459+
444460
Ok(Self {
445461
certificate_authorities,
462+
additional_trusted_certificates,
446463
})
447464
}
448465

466+
/// Read certificates from the given Secret
467+
///
468+
/// The keys are assumed to be filenames and their extensions denote the expected format of the
469+
/// certificate.
470+
async fn read_certificates_from_secret(
471+
client: &stackable_operator::client::Client,
472+
secret_ref: &SecretReference,
473+
) -> Result<Vec<X509>> {
474+
let mut certificates = vec![];
475+
476+
let secrets_api = &client.get_api::<Secret>(&secret_ref.namespace);
477+
let secret = secrets_api
478+
.get(&secret_ref.name)
479+
.await
480+
.with_context(|_| FindSecretSnafu { secret: secret_ref })?;
481+
482+
let secret_data = secret.data.unwrap_or_default();
483+
for (key, ByteString(value)) in &secret_data {
484+
let extension = Path::new(key).extension().and_then(OsStr::to_str);
485+
let certs = match extension {
486+
Some("pem") => X509::stack_from_pem(value),
487+
Some("cer") | Some("cert") | Some("crt") => X509::from_der(value)
488+
.map(|cert| vec![cert])
489+
.or(X509::stack_from_pem(value)),
490+
_ => {
491+
return UnsupportedCertificateFormatSnafu {
492+
key,
493+
secret: secret_ref,
494+
}
495+
.fail();
496+
}
497+
}
498+
.context(LoadCertificateSnafu {
499+
key,
500+
secret: secret_ref,
501+
})?;
502+
info!(
503+
"Add the certificate(s) {certs:?} from the key [{key}] of [{secret_ref}] to the additional trust roots.",
504+
certs = certs,
505+
secret_ref = secret_ref,
506+
key = key,
507+
);
508+
certificates.extend(certs);
509+
}
510+
511+
Ok(certificates)
512+
}
513+
449514
/// Get an appropriate [`CertificateAuthority`] for signing a given certificate.
450515
pub fn find_certificate_authority_for_signing(
451516
&self,
@@ -467,5 +532,6 @@ impl Manager {
467532
self.certificate_authorities
468533
.iter()
469534
.map(|ca| &ca.certificate)
535+
.chain(&self.additional_trusted_certificates)
470536
}
471537
}

rust/operator-binary/src/backend/tls/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use super::{
3333
ScopeAddressesError, SecretBackend, SecretBackendError, SecretContents,
3434
};
3535
use crate::{
36-
crd::{self, CertificateKeyGeneration},
36+
crd::{self, AdditionalTrustRoot, CertificateKeyGeneration},
3737
format::{well_known, SecretData, WellKnownSecretData},
3838
utils::iterator_try_concat_bytes,
3939
};
@@ -149,12 +149,14 @@ impl TlsGenerate {
149149
ca_certificate_lifetime,
150150
key_generation,
151151
}: &crd::AutoTlsCa,
152+
additional_trust_roots: &[AdditionalTrustRoot],
152153
max_cert_lifetime: Duration,
153154
) -> Result<Self> {
154155
Ok(Self {
155156
ca_manager: ca::Manager::load_or_create(
156157
client,
157158
ca_secret,
159+
additional_trust_roots,
158160
&ca::Config {
159161
manage_ca: *auto_generate_ca,
160162
ca_certificate_lifetime: *ca_certificate_lifetime,

rust/operator-binary/src/crd.rs

+26
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ pub struct AutoTlsBackend {
8989
/// Configures the certificate authority used to issue Pod certificates.
9090
pub ca: AutoTlsCa,
9191

92+
/// Additional trust roots which are added to the provided `ca.crt` file.
93+
#[serde(default)]
94+
pub additional_trust_roots: Vec<AdditionalTrustRoot>,
95+
9296
/// Maximum lifetime the created certificates are allowed to have.
9397
/// In case consumers request a longer lifetime than allowed by this setting,
9498
/// the lifetime will be the minimum of both, so this setting takes precedence.
@@ -137,6 +141,17 @@ impl AutoTlsCa {
137141
}
138142
}
139143

144+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
145+
#[serde(rename_all = "camelCase")]
146+
pub struct AdditionalTrustRoot {
147+
/// Reference (name and namespace) to a Kubernetes Secret object where additional certificates
148+
/// are stored.
149+
/// The extensions of the keys denote its contents: A key suffixed with `.pem` contains a stack
150+
/// of base64 encoded DER certificates, a key suffixed with `.cer`, `.cert`, or `.crt` contains
151+
/// either a binary DER certificate or a stack of base64 encoded DER certificates.
152+
pub secret: SecretReference,
153+
}
154+
140155
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
141156
#[serde(rename_all = "camelCase")]
142157
pub enum CertificateKeyGeneration {
@@ -417,6 +432,7 @@ mod test {
417432
length: CertificateKeyGeneration::RSA_KEY_LENGTH_3072
418433
}
419434
},
435+
additional_trust_roots: vec![],
420436
max_certificate_lifetime: DEFAULT_MAX_CERT_LIFETIME,
421437
})
422438
}
@@ -436,6 +452,10 @@ mod test {
436452
namespace: default
437453
autoGenerate: true
438454
caCertificateLifetime: 100d
455+
additionalTrustRoots:
456+
- secret:
457+
name: tls-root-ca
458+
namespace: default
439459
maxCertificateLifetime: 31d
440460
"#;
441461
let deserializer = serde_yaml::Deserializer::from_str(input);
@@ -454,6 +474,12 @@ mod test {
454474
ca_certificate_lifetime: Duration::from_days_unchecked(100),
455475
key_generation: CertificateKeyGeneration::default()
456476
},
477+
additional_trust_roots: vec![AdditionalTrustRoot {
478+
secret: SecretReference {
479+
name: "tls-root-ca".to_string(),
480+
namespace: "default".to_string(),
481+
}
482+
}],
457483
max_certificate_lifetime: Duration::from_days_unchecked(31),
458484
})
459485
}

0 commit comments

Comments
 (0)