20241024 1271 cert reload on SIGHUP (#3140)

reload certificates and keys on SIGHUP
This commit is contained in:
Firstyear 2024-10-25 10:01:30 +10:00 committed by GitHub
parent 4c2eeeb135
commit d2ae2ca206
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 339 additions and 264 deletions

View file

@ -44,7 +44,9 @@ db_path = "/var/lib/private/kanidm/kanidm.db"
# memory pressure on your system. # memory pressure on your system.
# db_arc_size = 2048 # db_arc_size = 2048
# #
# TLS chain and key in pem format. Both must be present # TLS chain and key in pem format. Both must be present.
# If the server recieves a SIGHUP, these files will be
# re-read and reloaded if their content is valid.
tls_chain = "/var/lib/private/kanidm/chain.pem" tls_chain = "/var/lib/private/kanidm/chain.pem"
tls_key = "/var/lib/private/kanidm/key.pem" tls_key = "/var/lib/private/kanidm/key.pem"
# #

View file

@ -6,7 +6,7 @@ use openssl::error::ErrorStack;
use openssl::nid::Nid; use openssl::nid::Nid;
use openssl::pkey::{PKeyRef, Private}; use openssl::pkey::{PKeyRef, Private};
use openssl::rsa::Rsa; use openssl::rsa::Rsa;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode};
use openssl::x509::{ use openssl::x509::{
extension::{ extension::{
AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage, AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage,
@ -17,10 +17,10 @@ use openssl::x509::{
use openssl::{asn1, bn, hash, pkey}; use openssl::{asn1, bn, hash, pkey};
use sketching::*; use sketching::*;
use crate::config::Configuration; use crate::config::TlsConfiguration;
use std::fs::File; use std::fs;
use std::io::{Read, Write}; use std::io::{ErrorKind, Read, Write};
use std::path::Path; use std::path::Path;
const CA_VALID_DAYS: u32 = 30; const CA_VALID_DAYS: u32 = 30;
@ -93,55 +93,163 @@ pub fn check_privkey_minimums(privkey: &PKeyRef<Private>) -> Result<(), String>
/// From the server configuration, generate an OpenSSL acceptor that we can use /// From the server configuration, generate an OpenSSL acceptor that we can use
/// to build our sockets for HTTPS/LDAPS. /// to build our sockets for HTTPS/LDAPS.
pub fn setup_tls(config: &Configuration) -> Result<Option<SslAcceptor>, ()> { pub fn setup_tls(
match &config.tls_config { tls_config: &Option<TlsConfiguration>,
Some(tls_config) => { ) -> Result<Option<SslAcceptor>, std::io::Error> {
// Signing algorithm minimums are enforced by the SSLAcceptor - it won't start up with a sha1-signed cert. let Some(tls_param) = tls_config.as_ref() else {
// https://wiki.mozilla.org/Security/Server_Side_TLS return Ok(None);
let mut ssl_builder = };
SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).map_err(|openssl_err| {
error!("Failed to start TLS builder"); let mut tls_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls())?;
error!(?openssl_err);
tls_builder
.set_certificate_chain_file(tls_param.chain.clone())
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
tls_builder
.set_private_key_file(tls_param.key.clone(), SslFiletype::PEM)
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
tls_builder.check_private_key().map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
// If configured, setup TLS client authentication.
if let Some(client_ca) = tls_param.client_ca.as_ref() {
info!("Loading client certificates from {}", client_ca.display());
let verify = SslVerifyMode::PEER;
// In future we may add a "require mTLS option" which would necessitate this.
// verify.insert(SslVerifyMode::FAIL_IF_NO_PEER_CERT);
tls_builder.set_verify(verify);
// When client certs are available, we disable the TLS session cache.
// This is so that when the smartcard is *removed* on the client, it forces
// the client session to immediately expire.
//
// https://stackoverflow.com/questions/12393711/session-disconnect-the-client-after-smart-card-is-removed
//
// Alternately, on logout we need to trigger https://docs.rs/openssl/latest/openssl/ssl/struct.Ssl.html#method.set_ssl_context
// with https://docs.rs/openssl/latest/openssl/ssl/struct.Ssl.html#method.ssl_context +
// https://docs.rs/openssl/latest/openssl/ssl/struct.SslContextRef.html#method.remove_session
//
// Or we lower session time outs etc.
tls_builder.set_session_cache_mode(SslSessionCacheMode::OFF);
let read_dir = fs::read_dir(client_ca).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!(
"Failed to create TLS listener while loading client ca from {}: {:?}",
client_ca.display(),
err
),
)
})?;
for cert_dir_ent in read_dir.filter_map(|item| item.ok()).filter(|item| {
item.file_name()
.to_str()
// Hashed certs end in .0
// Hashed crls are .r0
.map(|fname| fname.ends_with(".0"))
.unwrap_or_default()
}) {
let mut cert_pem = String::new();
fs::File::open(cert_dir_ent.path())
.and_then(|mut file| file.read_to_string(&mut cert_pem))
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?; })?;
ssl_builder let cert = X509::from_pem(cert_pem.as_bytes()).map_err(|err| {
.set_certificate_chain_file(&tls_config.chain) std::io::Error::new(
.map_err(|openssl_err| { ErrorKind::Other,
error!("Failed to access certificate chain file"); format!("Failed to create TLS listener: {:?}", err),
error!(?openssl_err); )
let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.chain);
info!(%diag);
})?;
ssl_builder
.set_private_key_file(&tls_config.key, SslFiletype::PEM)
.map_err(|openssl_err| {
error!("Failed to access private key file");
error!(?openssl_err);
let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.chain);
info!(%diag);
})?;
ssl_builder.check_private_key().map_err(|openssl_err| {
error!("Failed to validate private key");
error!(?openssl_err);
})?; })?;
let acceptor = ssl_builder.build(); let cert_store = tls_builder.cert_store_mut();
cert_store.add_cert(cert.clone()).map_err(|err| {
// let's enforce some TLS minimums! std::io::Error::new(
let privkey = acceptor.context().private_key().ok_or_else(|| { ErrorKind::Other,
error!("Failed to access acceptor private key"); format!(
"Failed to load cert store while creating TLS listener: {:?}",
err
),
)
})?; })?;
// This tells the client what CA's they should use. It DOES NOT
check_privkey_minimums(privkey).map_err(|err| { // verify them. That's the job of the cert store above!
error!("{}", err); tls_builder.add_client_ca(&cert).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?; })?;
Ok(Some(acceptor))
} }
None => Ok(None),
// TODO: Build our own CRL map HERE!
// Allow dumping client cert chains for dev debugging
// In the case this is status=false, should we be dumping these anyway?
if enabled!(tracing::Level::TRACE) {
tls_builder.set_verify_callback(verify, |status, x509store| {
if let Some(current_cert) = x509store.current_cert() {
let cert_text_bytes = current_cert.to_text().unwrap_or_default();
let cert_text = String::from_utf8_lossy(cert_text_bytes.as_slice());
tracing::warn!(client_cert = %cert_text);
};
if let Some(chain) = x509store.chain() {
for cert in chain.iter() {
let cert_text_bytes = cert.to_text().unwrap_or_default();
let cert_text = String::from_utf8_lossy(cert_text_bytes.as_slice());
tracing::warn!(chain_cert = %cert_text);
}
}
status
});
}
// End tls_client setup
} }
let tls_acceptor = tls_builder.build();
// let's enforce some TLS minimums!
let privkey = tls_acceptor.context().private_key().ok_or_else(|| {
std::io::Error::new(
ErrorKind::Other,
"Failed to access tls_acceptor private key".to_string(),
)
})?;
check_privkey_minimums(privkey).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Private key minimums were not met: {:?}", err),
)
})?;
Ok(Some(tls_acceptor))
} }
fn get_ec_group() -> Result<EcGroup, ErrorStack> { fn get_ec_group() -> Result<EcGroup, ErrorStack> {
@ -170,13 +278,13 @@ pub(crate) fn write_ca(
error!(err = ?e, "Failed to convert cert to PEM"); error!(err = ?e, "Failed to convert cert to PEM");
})?; })?;
File::create(key_path) fs::File::create(key_path)
.and_then(|mut file| file.write_all(&key_pem)) .and_then(|mut file| file.write_all(&key_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to create {:?}", key_path); error!(err = ?e, "Failed to create {:?}", key_path);
})?; })?;
File::create(cert_path) fs::File::create(cert_path)
.and_then(|mut file| file.write_all(&cert_pem)) .and_then(|mut file| file.write_all(&cert_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to create {:?}", cert_path); error!(err = ?e, "Failed to create {:?}", cert_path);
@ -341,14 +449,14 @@ pub(crate) fn load_ca(
let ca_cert_path: &Path = ca_cert_ar.as_ref(); let ca_cert_path: &Path = ca_cert_ar.as_ref();
let mut ca_key_pem = vec![]; let mut ca_key_pem = vec![];
File::open(ca_key_path) fs::File::open(ca_key_path)
.and_then(|mut file| file.read_to_end(&mut ca_key_pem)) .and_then(|mut file| file.read_to_end(&mut ca_key_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to read {:?}", ca_key_path); error!(err = ?e, "Failed to read {:?}", ca_key_path);
})?; })?;
let mut ca_cert_pem = vec![]; let mut ca_cert_pem = vec![];
File::open(ca_cert_path) fs::File::open(ca_cert_path)
.and_then(|mut file| file.read_to_end(&mut ca_cert_pem)) .and_then(|mut file| file.read_to_end(&mut ca_cert_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to read {:?}", ca_cert_path); error!(err = ?e, "Failed to read {:?}", ca_cert_path);
@ -413,19 +521,19 @@ pub(crate) fn write_cert(
} }
} }
File::create(key_path) fs::File::create(key_path)
.and_then(|mut file| file.write_all(&key_pem)) .and_then(|mut file| file.write_all(&key_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to create {:?}", key_path); error!(err = ?e, "Failed to create {:?}", key_path);
})?; })?;
File::create(chain_path) fs::File::create(chain_path)
.and_then(|mut file| file.write_all(&chain_pem)) .and_then(|mut file| file.write_all(&chain_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to create {:?}", chain_path); error!(err = ?e, "Failed to create {:?}", chain_path);
})?; })?;
File::create(cert_path) fs::File::create(cert_path)
.and_then(|mut file| file.write_all(&cert_pem)) .and_then(|mut file| file.write_all(&cert_pem))
.map_err(|e| { .map_err(|e| {
error!(err = ?e, "Failed to create {:?}", cert_path); error!(err = ?e, "Failed to create {:?}", cert_path);

View file

@ -19,7 +19,7 @@ mod views;
use self::extractors::ClientConnInfo; use self::extractors::ClientConnInfo;
use self::javascript::*; use self::javascript::*;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1}; use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::config::{Configuration, ServerRole, TlsConfiguration}; use crate::config::{Configuration, ServerRole};
use crate::CoreAction; use crate::CoreAction;
use axum::{ use axum::{
@ -40,8 +40,7 @@ use hyper::body::Incoming;
use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::rt::{TokioExecutor, TokioIo};
use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID}; use kanidm_proto::{constants::KSESSIONID, internal::COOKIE_AUTH_SESSION_ID};
use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor}; use kanidmd_lib::{idm::ClientCertInfo, status::StatusActor};
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslSessionCacheMode, SslVerifyMode}; use openssl::ssl::{Ssl, SslAcceptor};
use openssl::x509::X509;
use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate}; use kanidm_lib_crypto::x509_cert::{der::Decode, x509_public_key_s256, Certificate};
@ -51,6 +50,7 @@ use std::fmt::Write;
use tokio::{ use tokio::{
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
sync::broadcast, sync::broadcast,
sync::mpsc,
task, task,
}; };
use tokio_openssl::SslStream; use tokio_openssl::SslStream;
@ -58,8 +58,7 @@ use tower::Service;
use tower_http::{services::ServeDir, trace::TraceLayer}; use tower_http::{services::ServeDir, trace::TraceLayer};
use uuid::Uuid; use uuid::Uuid;
use std::fs; use std::io::ErrorKind;
use std::io::{ErrorKind, Read};
use std::path::PathBuf; use std::path::PathBuf;
use std::pin::Pin; use std::pin::Pin;
use std::{net::SocketAddr, str::FromStr}; use std::{net::SocketAddr, str::FromStr};
@ -209,9 +208,12 @@ pub async fn create_https_server(
status_ref: &'static StatusActor, status_ref: &'static StatusActor,
qe_w_ref: &'static QueryServerWriteV1, qe_w_ref: &'static QueryServerWriteV1,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
mut rx: broadcast::Receiver<CoreAction>,
server_message_tx: broadcast::Sender<CoreAction>, server_message_tx: broadcast::Sender<CoreAction>,
maybe_tls_acceptor: Option<SslAcceptor>,
tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
) -> Result<task::JoinHandle<()>, ()> { ) -> Result<task::JoinHandle<()>, ()> {
let rx = server_message_tx.subscribe();
let js_files = get_js_files(config.role)?; let js_files = get_js_files(config.role)?;
// set up the CSP headers // set up the CSP headers
// script-src 'self' // script-src 'self'
@ -384,204 +386,93 @@ pub async fn create_https_server(
info!("Starting the web server..."); info!("Starting the web server...");
Ok(task::spawn(async move { match maybe_tls_acceptor {
tokio::select! { Some(tls_acceptor) => {
Ok(action) = rx.recv() => { let listener = match TcpListener::bind(addr).await {
match action { Ok(l) => l,
CoreAction::Shutdown => {}, Err(err) => {
error!(?err, "Failed to bind tcp listener");
return Err(());
} }
} };
res = match config.tls_config { Ok(task::spawn(server_loop(
Some(tls_param) => { tls_acceptor,
// This isn't optimal, but we can't share this with the listener,
// other path for integration tests because that doesn't app,
// do tls (yet?) rx,
let listener = match TcpListener::bind(addr).await { server_message_tx,
Ok(l) => l, tls_acceptor_reload_rx,
Err(err) => { )))
error!(?err, "Failed to bind tcp listener"); }
return None => Ok(task::spawn(server_loop_plaintext(addr, app, rx))),
} }
};
task::spawn(server_loop(tls_param, listener, app))
},
None => {
task::spawn(axum_server::bind(addr).serve(app))
}
} => {
match res {
Ok(res_inner) => {
match res_inner {
Ok(_) => debug!("Web server exited OK"),
Err(err) => {
error!("Web server exited with {:?}", err);
}
}
},
Err(err) => {
error!("Web server exited with {:?}", err);
}
};
if let Err(err) = server_message_tx.send(CoreAction::Shutdown) {
error!("Web server failed to send shutdown message! {:?}", err)
};
}
};
info!("Stopped {}", super::TaskName::HttpsServer);
}))
} }
async fn server_loop( async fn server_loop(
tls_param: TlsConfiguration, mut tls_acceptor: SslAcceptor,
listener: TcpListener, listener: TcpListener,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>, app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
) -> Result<(), std::io::Error> { mut rx: broadcast::Receiver<CoreAction>,
let mut tls_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls())?; server_message_tx: broadcast::Sender<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
tls_builder ) {
.set_certificate_chain_file(tls_param.chain.clone())
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
tls_builder
.set_private_key_file(tls_param.key.clone(), SslFiletype::PEM)
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
tls_builder.check_private_key().map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
// If configured, setup TLS client authentication.
if let Some(client_ca) = tls_param.client_ca.as_ref() {
info!("Loading client certificates from {}", client_ca.display());
let verify = SslVerifyMode::PEER;
// In future we may add a "require mTLS option" which would necessitate this.
// verify.insert(SslVerifyMode::FAIL_IF_NO_PEER_CERT);
tls_builder.set_verify(verify);
// When client certs are available, we disable the TLS session cache.
// This is so that when the smartcard is *removed* on the client, it forces
// the client session to immediately expire.
//
// https://stackoverflow.com/questions/12393711/session-disconnect-the-client-after-smart-card-is-removed
//
// Alternately, on logout we need to trigger https://docs.rs/openssl/latest/openssl/ssl/struct.Ssl.html#method.set_ssl_context
// with https://docs.rs/openssl/latest/openssl/ssl/struct.Ssl.html#method.ssl_context +
// https://docs.rs/openssl/latest/openssl/ssl/struct.SslContextRef.html#method.remove_session
//
// Or we lower session time outs etc.
tls_builder.set_session_cache_mode(SslSessionCacheMode::OFF);
let read_dir = fs::read_dir(client_ca).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!(
"Failed to create TLS listener while loading client ca from {}: {:?}",
client_ca.display(),
err
),
)
})?;
for cert_dir_ent in read_dir.filter_map(|item| item.ok()).filter(|item| {
item.file_name()
.to_str()
// Hashed certs end in .0
// Hashed crls are .r0
.map(|fname| fname.ends_with(".0"))
.unwrap_or_default()
}) {
let mut cert_pem = String::new();
fs::File::open(cert_dir_ent.path())
.and_then(|mut file| file.read_to_string(&mut cert_pem))
.map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
let cert = X509::from_pem(cert_pem.as_bytes()).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
let cert_store = tls_builder.cert_store_mut();
cert_store.add_cert(cert.clone()).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!(
"Failed to load cert store while creating TLS listener: {:?}",
err
),
)
})?;
// This tells the client what CA's they should use. It DOES NOT
// verify them. That's the job of the cert store above!
tls_builder.add_client_ca(&cert).map_err(|err| {
std::io::Error::new(
ErrorKind::Other,
format!("Failed to create TLS listener: {:?}", err),
)
})?;
}
// TODO: Build our own CRL map HERE!
// Allow dumping client cert chains for dev debugging
// In the case this is status=false, should we be dumping these anyway?
if enabled!(tracing::Level::TRACE) {
tls_builder.set_verify_callback(verify, |status, x509store| {
if let Some(current_cert) = x509store.current_cert() {
let cert_text_bytes = current_cert.to_text().unwrap_or_default();
let cert_text = String::from_utf8_lossy(cert_text_bytes.as_slice());
tracing::warn!(client_cert = %cert_text);
};
if let Some(chain) = x509store.chain() {
for cert in chain.iter() {
let cert_text_bytes = cert.to_text().unwrap_or_default();
let cert_text = String::from_utf8_lossy(cert_text_bytes.as_slice());
tracing::warn!(chain_cert = %cert_text);
}
}
status
});
}
// End tls_client setup
}
let tls_acceptor = tls_builder.build();
pin_mut!(listener); pin_mut!(listener);
loop { loop {
if let Ok((stream, addr)) = listener.accept().await { tokio::select! {
let tls_acceptor = tls_acceptor.clone(); Ok(action) = rx.recv() => {
let app = app.clone(); match action {
task::spawn(handle_conn(tls_acceptor, stream, app, addr)); CoreAction::Shutdown => break,
}
}
accept = listener.accept() => {
match accept {
Ok((stream, addr)) => {
let tls_acceptor = tls_acceptor.clone();
let app = app.clone();
task::spawn(handle_conn(tls_acceptor, stream, app, addr));
}
Err(err) => {
error!("Web server exited with {:?}", err);
if let Err(err) = server_message_tx.send(CoreAction::Shutdown) {
error!("Web server failed to send shutdown message! {:?}", err)
};
break;
}
}
}
Some(mut new_tls_acceptor) = tls_acceptor_reload_rx.recv() => {
std::mem::swap(&mut tls_acceptor, &mut new_tls_acceptor);
info!("Reloaded http tls acceptor");
}
} }
} }
info!("Stopped {}", super::TaskName::HttpsServer);
}
async fn server_loop_plaintext(
addr: SocketAddr,
app: IntoMakeServiceWithConnectInfo<Router, ClientConnInfo>,
mut rx: broadcast::Receiver<CoreAction>,
) {
let listener = axum_server::bind(addr).serve(app);
pin_mut!(listener);
loop {
tokio::select! {
Ok(action) = rx.recv() => {
match action {
CoreAction::Shutdown =>
break,
}
}
_ = &mut listener => {}
}
}
info!("Stopped {}", super::TaskName::HttpsServer);
} }
/// This handles an individual connection. /// This handles an individual connection.

View file

@ -16,6 +16,7 @@ use tokio_util::codec::{FramedRead, FramedWrite};
use crate::CoreAction; use crate::CoreAction;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::mpsc;
struct LdapSession { struct LdapSession {
uat: Option<LdapBoundToken>, uat: Option<LdapBoundToken>,
@ -126,11 +127,12 @@ async fn client_process(
} }
/// TLS LDAP Listener, hands off to [client_process] /// TLS LDAP Listener, hands off to [client_process]
async fn tls_acceptor( async fn ldap_tls_acceptor(
listener: TcpListener, listener: TcpListener,
tls_acceptor: SslAcceptor, mut tls_acceptor: SslAcceptor,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
mut rx: broadcast::Receiver<CoreAction>, mut rx: broadcast::Receiver<CoreAction>,
mut tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
) { ) {
loop { loop {
tokio::select! { tokio::select! {
@ -150,6 +152,10 @@ async fn tls_acceptor(
} }
} }
} }
Some(mut new_tls_acceptor) = tls_acceptor_reload_rx.recv() => {
std::mem::swap(&mut tls_acceptor, &mut new_tls_acceptor);
info!("Reloaded ldap tls acceptor");
}
} }
} }
info!("Stopped {}", super::TaskName::LdapActor); info!("Stopped {}", super::TaskName::LdapActor);
@ -160,6 +166,7 @@ pub(crate) async fn create_ldap_server(
opt_ssl_acceptor: Option<SslAcceptor>, opt_ssl_acceptor: Option<SslAcceptor>,
qe_r_ref: &'static QueryServerReadV1, qe_r_ref: &'static QueryServerReadV1,
rx: broadcast::Receiver<CoreAction>, rx: broadcast::Receiver<CoreAction>,
tls_acceptor_reload_rx: mpsc::Receiver<SslAcceptor>,
) -> Result<tokio::task::JoinHandle<()>, ()> { ) -> Result<tokio::task::JoinHandle<()>, ()> {
if address.starts_with(":::") { if address.starts_with(":::") {
// takes :::xxxx to xxxx // takes :::xxxx to xxxx
@ -182,7 +189,13 @@ pub(crate) async fn create_ldap_server(
Some(ssl_acceptor) => { Some(ssl_acceptor) => {
info!("Starting LDAPS interface ldaps://{} ...", address); info!("Starting LDAPS interface ldaps://{} ...", address);
tokio::spawn(tls_acceptor(listener, ssl_acceptor, qe_r_ref, rx)) tokio::spawn(ldap_tls_acceptor(
listener,
ssl_acceptor,
qe_r_ref,
rx,
tls_acceptor_reload_rx,
))
} }
None => { None => {
error!("The server won't run without TLS!"); error!("The server won't run without TLS!");

View file

@ -52,12 +52,14 @@ use kanidmd_lib::value::CredentialType;
use libc::umask; use libc::umask;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::Notify;
use tokio::task; use tokio::task;
use crate::actors::{QueryServerReadV1, QueryServerWriteV1}; use crate::actors::{QueryServerReadV1, QueryServerWriteV1};
use crate::admin::AdminActor; use crate::admin::AdminActor;
use crate::config::{Configuration, ServerRole}; use crate::config::{Configuration, ServerRole};
use crate::interval::IntervalActor; use crate::interval::IntervalActor;
use tokio::sync::mpsc;
// === internal setup helpers // === internal setup helpers
@ -738,6 +740,7 @@ pub(crate) enum TaskName {
IntervalActor, IntervalActor,
LdapActor, LdapActor,
Replication, Replication,
TlsAcceptorReload,
} }
impl Display for TaskName { impl Display for TaskName {
@ -754,6 +757,7 @@ impl Display for TaskName {
TaskName::IntervalActor => "Interval Actor", TaskName::IntervalActor => "Interval Actor",
TaskName::LdapActor => "LDAP Acceptor Actor", TaskName::LdapActor => "LDAP Acceptor Actor",
TaskName::Replication => "Replication", TaskName::Replication => "Replication",
TaskName::TlsAcceptorReload => "TlsAcceptor Reload Monitor",
} }
) )
} }
@ -761,12 +765,17 @@ impl Display for TaskName {
pub struct CoreHandle { pub struct CoreHandle {
clean_shutdown: bool, clean_shutdown: bool,
pub tx: broadcast::Sender<CoreAction>, tx: broadcast::Sender<CoreAction>,
tls_acceptor_reload_notify: Arc<Notify>,
/// This stores a name for the handle, and the handle itself so we can tell which failed/succeeded at the end. /// This stores a name for the handle, and the handle itself so we can tell which failed/succeeded at the end.
handles: Vec<(TaskName, task::JoinHandle<()>)>, handles: Vec<(TaskName, task::JoinHandle<()>)>,
} }
impl CoreHandle { impl CoreHandle {
pub fn subscribe(&mut self) -> broadcast::Receiver<CoreAction> {
self.tx.subscribe()
}
pub async fn shutdown(&mut self) { pub async fn shutdown(&mut self) {
if self.tx.send(CoreAction::Shutdown).is_err() { if self.tx.send(CoreAction::Shutdown).is_err() {
eprintln!("No receivers acked shutdown request. Treating as unclean."); eprintln!("No receivers acked shutdown request. Treating as unclean.");
@ -782,6 +791,10 @@ impl CoreHandle {
self.clean_shutdown = true; self.clean_shutdown = true;
} }
pub async fn tls_acceptor_reload(&mut self) {
self.tls_acceptor_reload_notify.notify_one()
}
} }
impl Drop for CoreHandle { impl Drop for CoreHandle {
@ -826,10 +839,10 @@ pub async fn create_server_core(
let status_ref = StatusActor::start(); let status_ref = StatusActor::start();
// Setup TLS (if any) // Setup TLS (if any)
let _opt_tls_params = match crypto::setup_tls(&config) { let maybe_tls_acceptor = match crypto::setup_tls(&config.tls_config) {
Ok(opt_tls_params) => opt_tls_params, Ok(tls_acc) => tls_acc,
Err(()) => { Err(err) => {
error!("Failed to configure TLS parameters"); error!(?err, "Failed to configure TLS acceptor");
return Err(()); return Err(());
} }
}; };
@ -1009,6 +1022,54 @@ pub async fn create_server_core(
info!("Stopped {}", TaskName::AuditdActor); info!("Stopped {}", TaskName::AuditdActor);
}); });
// Setup a TLS Acceptor Reload trigger.
let mut broadcast_rx = broadcast_tx.subscribe();
let tls_acceptor_reload_notify = Arc::new(Notify::new());
let tls_accepter_reload_task_notify = tls_acceptor_reload_notify.clone();
let tls_config = config.tls_config.clone();
let ldap_configured = config.ldapaddress.is_some();
let (ldap_tls_acceptor_reload_tx, ldap_tls_acceptor_reload_rx) = mpsc::channel(1);
let (http_tls_acceptor_reload_tx, http_tls_acceptor_reload_rx) = mpsc::channel(1);
let tls_acceptor_reload_handle = task::spawn(async move {
loop {
tokio::select! {
Ok(action) = broadcast_rx.recv() => {
match action {
CoreAction::Shutdown => break,
}
}
_ = tls_accepter_reload_task_notify.notified() => {
let tls_acceptor = match crypto::setup_tls(&tls_config) {
Ok(Some(tls_acc)) => tls_acc,
Ok(None) => {
warn!("TLS not configured, ignoring reload request.");
continue;
}
Err(err) => {
error!(?err, "Failed to configure and reload TLS acceptor");
continue;
}
};
// We don't log here as the receivers will notify when they have completed
// the reload.
if ldap_configured &&
ldap_tls_acceptor_reload_tx.send(tls_acceptor.clone()).await.is_err() {
error!("ldap tls acceptor did not accept the reload, the server may have failed!");
};
if http_tls_acceptor_reload_tx.send(tls_acceptor.clone()).await.is_err() {
error!("http tls acceptor did not accept the reload, the server may have failed!");
break;
};
}
}
}
info!("Stopped {}", TaskName::TlsAcceptorReload);
});
// Setup timed events associated to the write thread // Setup timed events associated to the write thread
let interval_handle = IntervalActor::start(server_write_ref, broadcast_tx.subscribe()); let interval_handle = IntervalActor::start(server_write_ref, broadcast_tx.subscribe());
// Setup timed events associated to the read thread // Setup timed events associated to the read thread
@ -1035,13 +1096,8 @@ pub async fn create_server_core(
// If we have been requested to init LDAP, configure it now. // If we have been requested to init LDAP, configure it now.
let maybe_ldap_acceptor_handle = match &config.ldapaddress { let maybe_ldap_acceptor_handle = match &config.ldapaddress {
Some(la) => { Some(la) => {
let opt_ldap_ssl_acceptor = match crypto::setup_tls(&config) { let opt_ldap_ssl_acceptor = maybe_tls_acceptor.clone();
Ok(t) => t,
Err(()) => {
error!("Failed to configure LDAP TLS parameters");
return Err(());
}
};
if !config_test { if !config_test {
// ⚠️ only start the sockets and listeners in non-config-test modes. // ⚠️ only start the sockets and listeners in non-config-test modes.
let h = ldaps::create_ldap_server( let h = ldaps::create_ldap_server(
@ -1049,6 +1105,7 @@ pub async fn create_server_core(
opt_ldap_ssl_acceptor, opt_ldap_ssl_acceptor,
server_read_ref, server_read_ref,
broadcast_tx.subscribe(), broadcast_tx.subscribe(),
ldap_tls_acceptor_reload_rx,
) )
.await?; .await?;
Some(h) Some(h)
@ -1092,8 +1149,9 @@ pub async fn create_server_core(
status_ref, status_ref,
server_write_ref, server_write_ref,
server_read_ref, server_read_ref,
broadcast_tx.subscribe(),
broadcast_tx.clone(), broadcast_tx.clone(),
maybe_tls_acceptor,
http_tls_acceptor_reload_rx,
) )
.await .await
{ {
@ -1133,6 +1191,7 @@ pub async fn create_server_core(
(TaskName::IntervalActor, interval_handle), (TaskName::IntervalActor, interval_handle),
(TaskName::DelayedActionActor, delayed_handle), (TaskName::DelayedActionActor, delayed_handle),
(TaskName::AuditdActor, auditd_handle), (TaskName::AuditdActor, auditd_handle),
(TaskName::TlsAcceptorReload, tls_acceptor_reload_handle),
]; ];
if let Some(backup_handle) = maybe_backup_handle { if let Some(backup_handle) = maybe_backup_handle {
@ -1157,6 +1216,7 @@ pub async fn create_server_core(
Ok(CoreHandle { Ok(CoreHandle {
clean_shutdown: false, clean_shutdown: false,
tls_acceptor_reload_notify,
tx: broadcast_tx, tx: broadcast_tx,
handles, handles,
}) })

View file

@ -756,7 +756,7 @@ async fn kanidm_main(
loop { loop {
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
{ {
let mut listener = sctx.tx.subscribe(); let mut listener = sctx.subscribe();
tokio::select! { tokio::select! {
Ok(()) = tokio::signal::ctrl_c() => { Ok(()) = tokio::signal::ctrl_c() => {
break break
@ -780,7 +780,8 @@ async fn kanidm_main(
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => { } => {
// Ignore // Reload TLS certificates
sctx.tls_acceptor_reload().await
} }
Some(()) = async move { Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1(); let sigterm = tokio::signal::unix::SignalKind::user_defined1();