TLS: Update search index when file-backed certificates are refreshed

This commit is contained in:
Maurus Decimus
2026-06-23 17:58:04 +02:00
parent 46f03f7dad
commit 14593e6f72
3 changed files with 102 additions and 31 deletions

View File

@@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
## Fixed
- DANE: Verify DNSSEC is supported by the resolver before attempting to validate TLSA records.
- TLS: Update search index when file-backed certificates are refreshed.
## [0.16.10] - 2026-06-21

View File

@@ -4,11 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::network::acme::ParsedCert;
use ahash::{AHashMap, AHashSet};
use rcgen::generate_simple_self_signed;
use registry::schema::{
prelude::Object,
structs::{Certificate, SystemSettings},
use registry::{
schema::{
prelude::Object,
structs::{Certificate, PublicText, SecretText, SystemSettings},
},
types::{datetime::UTCDateTime, map::Map},
};
use rustls::{
SupportedProtocolVersion,
@@ -39,20 +43,61 @@ pub(crate) async fn parse_certificates(
let mut certs_expired = Vec::new();
let mut certs_expirations = AHashMap::new();
for cert_obj in bp.list_infallible::<Certificate>().await {
let not_valid_after = cert_obj.object.not_valid_after.timestamp();
let not_valid_before = cert_obj.object.not_valid_before.timestamp();
let obj_id = cert_obj.id;
let revision = cert_obj.revision;
let mut cert = cert_obj.object;
let is_file_backed = matches!(cert.certificate, PublicText::File(_))
|| matches!(cert.private_key, SecretText::File(_));
let mut public = None;
let mut refreshed_meta = None;
if is_file_backed {
let pem = match cert.certificate.value().await {
Ok(value) => value.into_owned().into_bytes(),
Err(err) => {
bp.build_error(obj_id, format!("Failed to obtain certificate value: {err}"));
continue;
}
};
match ParsedCert::parse(&pem) {
Ok(parsed) => {
let not_valid_after =
UTCDateTime::from_timestamp(parsed.valid_not_after.timestamp());
let not_valid_before =
UTCDateTime::from_timestamp(parsed.valid_not_before.timestamp());
let sans = Map::new(parsed.sans);
if cert.not_valid_after != not_valid_after
|| cert.not_valid_before != not_valid_before
|| cert.issuer != parsed.issuer
|| cert.subject_alternative_names != sans
{
refreshed_meta =
Some((not_valid_after, not_valid_before, parsed.issuer, sans));
}
public = Some(pem);
}
Err(err) => {
bp.build_error(obj_id, format!("Invalid certificate: {err}"));
continue;
}
}
}
let (not_valid_after, not_valid_before) = match refreshed_meta.as_ref() {
Some((after, before, _, _)) => (after.timestamp(), before.timestamp()),
None => (
cert.not_valid_after.timestamp(),
cert.not_valid_before.timestamp(),
),
};
if not_valid_after <= now {
certs_expired.push((
cert_obj.id,
cert_obj
.object
.subject_alternative_names
.clone()
.into_inner(),
obj_id,
cert.subject_alternative_names.clone().into_inner(),
Object {
inner: cert_obj.object.into(),
revision: cert_obj.revision,
inner: cert.into(),
revision,
},
));
continue;
@@ -60,33 +105,57 @@ pub(crate) async fn parse_certificates(
continue; // Skip certificates that are not yet valid
}
let mut cert = cert_obj.object;
let secret = match cert.private_key.secret().await {
Ok(secret) => secret.into_owned().into_bytes(),
Err(err) => {
bp.build_error(
cert_obj.id,
obj_id,
format!("Failed to obtain private key secret: {err}"),
);
continue;
}
};
let public = match cert.certificate.value().await {
Ok(value) => value.into_owned().into_bytes(),
Err(err) => {
bp.build_error(
cert_obj.id,
format!("Failed to obtain certificate value: {err}"),
);
continue;
}
let public = match public {
Some(public) => public,
None => match cert.certificate.value().await {
Ok(value) => value.into_owned().into_bytes(),
Err(err) => {
bp.build_error(obj_id, format!("Failed to obtain certificate value: {err}"));
continue;
}
},
};
if let Some((not_valid_after, not_valid_before, issuer, sans)) = refreshed_meta {
let old = Object {
inner: cert.clone().into(),
revision,
};
cert.not_valid_after = not_valid_after;
cert.not_valid_before = not_valid_before;
cert.issuer = issuer;
cert.subject_alternative_names = sans;
let new = Object {
inner: cert.clone().into(),
revision,
};
if let Err(err) = bp
.registry
.write(RegistryWrite::update(obj_id.id(), &new, &old))
.await
{
trc::error!(
err.details("Failed to refresh TLS certificate metadata in registry.")
.caused_by(trc::location!())
);
}
}
// Add default certificate
if system
.default_certificate_id
.as_ref()
.is_some_and(|id| *id == cert_obj.id.id())
.is_some_and(|id| *id == obj_id.id())
{
cert.subject_alternative_names
.push_unchecked("*".to_string());
@@ -120,7 +189,7 @@ pub(crate) async fn parse_certificates(
}
}
Err(err) => {
bp.build_error(cert_obj.id, format!("Invalid certificate: {err}"));
bp.build_error(obj_id, format!("Invalid certificate: {err}"));
}
}
}

View File

@@ -61,11 +61,12 @@ impl SmtpConfig {
};
if !config.resolvers.dnssec_available
&& config
.queue
.tls_strategy
.values()
.any(|t| !matches!(t.dane, RequireOptional::Disable))
&& (config.queue.tls_strategy.is_empty()
|| config
.queue
.tls_strategy
.values()
.any(|t| !matches!(t.dane, RequireOptional::Disable)))
{
bp.build_warning(
ObjectType::DnsResolver.singleton(),