Skip to content

Commit ff028ce

Browse files
authored
feat(server): support TLS certificate hot-reload (#1870)
* feat(server): support TLS certificate hot-reload Signed-off-by: Yuedong Wu <dwcn22@outlook.com> * refactor(server): extract shared TLS test utilities Signed-off-by: Yuedong Wu <dwcn22@outlook.com> --------- Signed-off-by: Yuedong Wu <dwcn22@outlook.com>
1 parent fd6cbf6 commit ff028ce

6 files changed

Lines changed: 984 additions & 79 deletions

File tree

Cargo.lock

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/openshell-server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ ipnet = "2"
9595
tempfile = "3"
9696
rustix = { workspace = true }
9797
x509-parser = "0.16"
98+
arc-swap = "1"
99+
notify = "8"
98100

99101
[features]
100102
default = ["telemetry"]

crates/openshell-server/src/lib.rs

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ mod ssh_sessions;
4040
pub mod supervisor_session;
4141
mod telemetry;
4242
mod tls;
43+
#[cfg(test)]
44+
pub(crate) mod tls_test_utils;
4345
pub mod tracing_bus;
4446
mod ws_tunnel;
4547

@@ -420,14 +422,23 @@ pub async fn run_server(
420422
info!("Metrics server disabled");
421423
}
422424

425+
let (shutdown_tx, shutdown_rx) = watch::channel(false);
426+
423427
// Build TLS acceptor when TLS is configured; otherwise serve plaintext.
424428
let tls_acceptor = if let Some(tls) = &config.tls {
425-
Some(TlsAcceptor::from_files(
429+
let acceptor = TlsAcceptor::from_files(
426430
&tls.cert_path,
427431
&tls.key_path,
428432
tls.client_ca_path.as_deref(),
429433
tls.require_client_auth,
430-
)?)
434+
)?;
435+
436+
// Spawn file-watcher-based TLS certificate reload worker.
437+
// Watches parent directories of cert/key/CA files and atomically
438+
// reloads when changes are detected.
439+
acceptor.spawn_reload_worker(shutdown_rx.clone());
440+
441+
Some(acceptor)
431442
} else {
432443
info!("TLS disabled — accepting plaintext connections");
433444
None
@@ -621,7 +632,10 @@ fn spawn_gateway_connection(
621632
warn!(client = %addr, listen = %listen_addr, "Rejected plaintext HTTP on non-loopback gateway listener");
622633
}
623634
Ok(ConnectionProtocol::Tls | ConnectionProtocol::Unknown) => {
624-
match acceptor.inner().accept(stream).await {
635+
// acceptor.acceptor() snapshots the current TLS config;
636+
// the returned acceptor owns an Arc that stays alive for
637+
// the full duration of the handshake.
638+
match acceptor.acceptor().accept(stream).await {
625639
Ok(tls_stream) => {
626640
let peer_identity = multiplex::extract_peer_identity(&tls_stream);
627641
if let Err(e) = service
@@ -914,8 +928,7 @@ mod tests {
914928
ComputeDriverKind, Config,
915929
proto::{HealthRequest, open_shell_client::OpenShellClient},
916930
};
917-
use rcgen::{CertificateParams, IsCa, KeyPair};
918-
use std::io::{Error, ErrorKind, Write};
931+
use std::io::{Error, ErrorKind};
919932
use std::net::SocketAddr;
920933
use std::sync::Arc;
921934
use std::time::Duration;
@@ -924,41 +937,13 @@ mod tests {
924937
use tokio::net::{TcpListener, TcpStream};
925938
use tokio::sync::watch;
926939

927-
fn install_rustls_provider() {
928-
let _ = rustls::crypto::ring::default_provider().install_default();
929-
}
940+
use crate::tls_test_utils::{generate_test_certs_with_ca, install_rustls_provider};
930941

931942
fn test_tls_acceptor() -> (TempDir, TlsAcceptor) {
932943
install_rustls_provider();
933944

934-
let mut ca_params =
935-
CertificateParams::new(Vec::<String>::new()).expect("failed to create CA params");
936-
ca_params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
937-
ca_params
938-
.distinguished_name
939-
.push(rcgen::DnType::CommonName, "test-ca");
940-
let ca_key = KeyPair::generate().expect("failed to generate CA key");
941-
let ca_cert = ca_params
942-
.self_signed(&ca_key)
943-
.expect("failed to sign CA cert");
944-
945-
let server_params = CertificateParams::new(vec!["localhost".to_string()])
946-
.expect("failed to create server params");
947-
let server_key = KeyPair::generate().expect("failed to generate server key");
948-
let server_cert = server_params
949-
.signed_by(&server_key, &ca_cert, &ca_key)
950-
.expect("failed to sign server cert");
951-
952945
let dir = tempdir().expect("failed to create tempdir");
953-
let write_file = |name: &str, data: &[u8]| {
954-
let path = dir.path().join(name);
955-
std::fs::File::create(&path)
956-
.and_then(|mut file| file.write_all(data))
957-
.expect("failed to write tls test file");
958-
};
959-
write_file("ca.pem", ca_cert.pem().as_bytes());
960-
write_file("server-cert.pem", server_cert.pem().as_bytes());
961-
write_file("server-key.pem", server_key.serialize_pem().as_bytes());
946+
generate_test_certs_with_ca(dir.path());
962947

963948
let acceptor = TlsAcceptor::from_files(
964949
&dir.path().join("server-cert.pem"),

0 commit comments

Comments
 (0)