use pingora_core::listeners::tls::TlsSettings;
use praxis_core::ProxyError;
use praxis_tls::ListenerTls;
use tokio::sync::watch;
pub(crate) fn build_tls_settings(
tls: &ListenerTls,
address: &str,
context_label: &str,
advertise_http_alpn: bool,
) -> Result<(TlsSettings, Option<watch::Sender<bool>>), ProxyError> {
#[cfg(feature = "config-reload")]
if tls.is_hot_reload() {
return build_reloadable_tls_settings(tls, address, context_label, advertise_http_alpn);
}
#[cfg(not(feature = "config-reload"))]
if tls.is_hot_reload() {
tracing::warn!(
address,
context_label,
"listener requests TLS hot_reload but this build lacks the `config-reload` feature; \
serving a static certificate"
);
}
tracing::debug!(address, context_label, "building TLS ServerConfig");
let server_config = praxis_tls::setup::build_server_config(tls, advertise_http_alpn)
.map_err(|e| ProxyError::Config(format!("TLS for {address}: {e}")))?;
let settings = TlsSettings::with_server_config(server_config)
.map_err(|e| ProxyError::Config(format!("TLS for {address}: {e}")))?;
Ok((settings, None))
}
#[cfg(feature = "config-reload")]
fn build_reloadable_tls_settings(
tls: &ListenerTls,
address: &str,
context_label: &str,
advertise_http_alpn: bool,
) -> Result<(TlsSettings, Option<watch::Sender<bool>>), ProxyError> {
tracing::debug!(address, context_label, "building TLS ServerConfig with hot-reload");
let result = praxis_tls::setup::build_reloadable_server_config(tls, advertise_http_alpn)
.map_err(|e| ProxyError::Config(format!("TLS hot-reload for {address}: {e}")))?;
let pair = tls
.certificates
.first()
.cloned()
.ok_or_else(|| ProxyError::Config(format!("TLS hot-reload for {address}: no certificate configured")))?;
let verifier_reload = result.verifier_handle.and_then(|handle| {
tls.client_ca
.as_ref()
.map(|ca_cfg| praxis_tls::watcher::ClientVerifierReload {
ca_path: ca_cfg.ca_path.clone(),
crl_paths: ca_cfg.crl_paths.clone(),
mode: tls.client_cert_mode,
trusted_spiffe_ids: tls.trusted_spiffe_ids.clone(),
swap_handle: handle,
})
});
let (shutdown_tx, shutdown_rx) = watch::channel(false);
praxis_tls::watcher::CertWatcher::spawn(result.cert_handle, pair, verifier_reload, shutdown_rx);
let settings = TlsSettings::with_server_config(result.config)
.map_err(|e| ProxyError::Config(format!("TLS for {address}: {e}")))?;
Ok((settings, Some(shutdown_tx)))
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::significant_drop_tightening,
clippy::semicolon_if_nothing_returned,
clippy::let_underscore_must_use,
clippy::too_many_lines,
reason = "tests"
)]
mod tests {
use praxis_tls::{CaConfig, CertKeyPair, ClientCertMode};
use super::*;
fn gen_test_certs() -> (tempfile::TempDir, String, String, String) {
use rcgen::{CertificateParams, DnType, IsCa, Issuer, KeyPair};
let temp_dir = tempfile::TempDir::new().expect("create temp dir");
let ca_key = KeyPair::generate().expect("generate CA key");
let mut ca_params = CertificateParams::new(Vec::<String>::new()).expect("CA params");
ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params.distinguished_name.push(DnType::CommonName, "Test CA");
let ca_cert = ca_params.self_signed(&ca_key).expect("self-sign CA");
let issuer = Issuer::from_params(&ca_params, &ca_key);
let server_key = KeyPair::generate().expect("generate server key");
let mut server_params = CertificateParams::new(vec!["localhost".to_owned()]).expect("server params");
server_params.distinguished_name.push(DnType::CommonName, "localhost");
let server_cert = server_params.signed_by(&server_key, &issuer).expect("sign server cert");
let ca_path = temp_dir.path().join("ca.pem");
let cert_path = temp_dir.path().join("server.pem");
let key_path = temp_dir.path().join("server-key.pem");
std::fs::write(&ca_path, ca_cert.pem()).expect("write CA");
std::fs::write(&cert_path, server_cert.pem()).expect("write cert");
std::fs::write(&key_path, server_key.serialize_pem()).expect("write key");
(
temp_dir,
ca_path.to_str().unwrap().to_owned(),
cert_path.to_str().unwrap().to_owned(),
key_path.to_str().unwrap().to_owned(),
)
}
fn ensure_crypto_provider() {
praxis_tls::provider::install();
}
#[test]
fn static_config_single_cert() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let (settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "TEST", false)?;
assert!(
shutdown_tx.is_none(),
"static config should not return a cert-watcher shutdown sender"
);
drop(settings);
Ok(())
}
#[test]
fn static_config_with_alpn() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
assert!(
shutdown_tx.is_none(),
"static config with HTTP ALPN should not return a cert-watcher shutdown sender"
);
Ok(())
}
#[test]
fn static_config_multi_cert_disables_hot_reload() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![
CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec!["example.com".to_owned()],
},
CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: true,
server_names: vec![],
},
],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
assert!(!tls.is_hot_reload(), "a multi-cert config must disable hot-reload");
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
assert!(
shutdown_tx.is_none(),
"a multi-cert config should use the static path and not return a shutdown sender"
);
Ok(())
}
#[test]
fn static_config_with_client_ca() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: Some(CaConfig {
ca_path: ca,
crl_paths: vec![],
}),
client_cert_mode: ClientCertMode::Request,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "mTLS", false)?;
assert!(
shutdown_tx.is_none(),
"static mTLS config should not return a cert-watcher shutdown sender"
);
Ok(())
}
#[test]
fn static_config_invalid_cert_path() {
ensure_crypto_provider();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: "/nonexistent/cert.pem".to_owned(),
key_path: "/nonexistent/key.pem".to_owned(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
assert!(
matches!(
build_tls_settings(&tls, "127.0.0.1:8443", "TEST", false),
Err(ProxyError::Config(msg)) if msg.contains("127.0.0.1:8443")
),
"an invalid cert path should fail with a config error naming the listener address 127.0.0.1:8443"
);
}
#[test]
#[cfg(not(feature = "config-reload"))]
fn hot_reload_without_feature_warns_and_uses_static() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
assert!(
tls.is_hot_reload(),
"a single cert with hot_reload=None should default to hot-reload"
);
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
assert!(
shutdown_tx.is_none(),
"without the config-reload feature the builder should fall back to a static config and not return a watcher"
);
Ok(())
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_with_feature_spawns_watcher() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
assert!(tls.is_hot_reload(), "a single cert should enable hot-reload by default");
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
assert!(
shutdown_tx.is_some(),
"with the config-reload feature the builder should return a cert-watcher shutdown sender"
);
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
Ok(())
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_with_client_ca() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: Some(CaConfig {
ca_path: ca,
crl_paths: vec![],
}),
client_cert_mode: ClientCertMode::Require,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "mTLS", false)?;
assert!(
shutdown_tx.is_some(),
"hot-reload with a client CA should spawn a watcher and return a shutdown sender"
);
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
Ok(())
}
#[test]
#[cfg(feature = "config-reload")]
fn hot_reload_without_client_ca() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: None,
min_version: None,
};
let (_settings, shutdown_tx) = build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
assert!(
shutdown_tx.is_some(),
"hot-reload without a client CA should still spawn a watcher and return a shutdown sender"
);
if let Some(tx) = shutdown_tx {
let _ = tx.send(true);
}
Ok(())
}
#[test]
fn context_label_used_in_logging() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert.clone(),
key_path: key.clone(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
build_tls_settings(&tls, "127.0.0.1:8443", "TCP", false)?;
build_tls_settings(&tls, "127.0.0.1:8443", "CUSTOM", false)?;
Ok(())
}
#[test]
fn address_included_in_error_message() {
ensure_crypto_provider();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: "/nonexistent/cert.pem".to_owned(),
key_path: "/nonexistent/key.pem".to_owned(),
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
assert!(
matches!(
build_tls_settings(&tls, "192.168.1.1:443", "HTTP", false),
Err(ProxyError::Config(msg)) if msg.contains("192.168.1.1:443")
),
"the config error should name the failing listener address 192.168.1.1:443"
);
assert!(
matches!(
build_tls_settings(&tls, "[::1]:8443", "TCP", false),
Err(ProxyError::Config(msg)) if msg.contains("[::1]:8443")
),
"the config error should name the failing IPv6 listener address [::1]:8443"
);
}
#[test]
fn alpn_variation_coverage() -> Result<(), ProxyError> {
ensure_crypto_provider();
let (_temp, _ca, cert, key) = gen_test_certs();
let tls = ListenerTls {
certificates: vec![CertKeyPair {
cert_path: cert,
key_path: key,
default: false,
server_names: vec![],
}],
cipher_suites: None,
client_ca: None,
client_cert_mode: ClientCertMode::None,
trusted_spiffe_ids: vec![],
hot_reload: Some(false),
min_version: None,
};
build_tls_settings(&tls, "127.0.0.1:8443", "HTTP", true)?;
build_tls_settings(&tls, "127.0.0.1:8443", "TCP", false)?;
Ok(())
}
}