From caf9d0984f678a93cf503eea93ba5b5feeee49e1 Mon Sep 17 00:00:00 2001 From: Jeena Date: Fri, 16 Jan 2026 13:05:20 +0000 Subject: [PATCH] Implement SIGHUP certificate reloading for Let's Encrypt - Add tokio signal handling for SIGHUP - Implement thread-safe TLS acceptor reloading with Mutex - Modify main loop to handle signals alongside connections - Update systemd service (already had ExecReload) - Add certbot hook script documentation to INSTALL.md - Enable zero-downtime certificate renewal support --- dist/INSTALL.md | 35 ++++++++++++++++++++ dist/pollux.service | 2 ++ src/main.rs | 79 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/dist/INSTALL.md b/dist/INSTALL.md index 8d5caa3..380f6d7 100644 --- a/dist/INSTALL.md +++ b/dist/INSTALL.md @@ -200,6 +200,41 @@ See `config.toml` for all available options. Key settings: - `max_concurrent_requests`: Connection limit - `log_level`: Logging verbosity +## Certificate Management + +The server supports automatic certificate reloading via SIGHUP signals. + +### Let's Encrypt Integration + +For automatic certificate renewal with certbot: + +```bash +# Create post-renewal hook +sudo tee /etc/letsencrypt/renewal-hooks/post/reload-pollux.sh > /dev/null << 'EOF' +#!/bin/bash +# Reload Pollux after Let's Encrypt certificate renewal + +systemctl reload pollux +logger -t certbot-pollux-reload "Reloaded pollux after certificate renewal" +EOF + +# Make it executable +sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-pollux.sh + +# Test the hook +sudo /etc/letsencrypt/renewal-hooks/post/reload-pollux.sh +``` + +### Manual Certificate Reload + +```bash +# Reload certificates without restarting +sudo systemctl reload pollux + +# Check reload in logs +sudo journalctl -u pollux -f +``` + ## Upgrading ```bash diff --git a/dist/pollux.service b/dist/pollux.service index a05eb16..51a461d 100644 --- a/dist/pollux.service +++ b/dist/pollux.service @@ -15,6 +15,8 @@ NoNewPrivileges=yes ProtectHome=yes ProtectSystem=strict ReadOnlyPaths=/etc/pollux /etc/letsencrypt/live/example.com /var/www/example.com +# NOTE: Adjust /etc/letsencrypt/live/example.com and /var/www/example.com to match your config +# The server needs read access to config, certificates, and content files # NOTE: Adjust paths to match your config: # - /etc/letsencrypt/live/example.com for Let's Encrypt certs # - /var/www/example.com for your content root diff --git a/src/main.rs b/src/main.rs index 5ea2c67..fa826b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,10 +8,32 @@ use clap::Parser; use rustls::ServerConfig; use std::path::Path; use std::sync::Arc; +use tokio::sync::Mutex; use tokio::net::TcpListener; +use tokio::signal::unix::{signal, SignalKind}; use tokio_rustls::TlsAcceptor; use logging::init_logging; +async fn reload_tls_acceptor( + cert_path: &str, + key_path: &str, +) -> Result> { + tracing::info!("Reloading TLS certificates"); + + let certs = tls::load_certs(cert_path)?; + let key = tls::load_private_key(key_path)?; + + let config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key)?; + + let acceptor = TlsAcceptor::from(Arc::new(config)); + + tracing::info!("TLS certificates reloaded successfully"); + Ok(acceptor) +} + fn print_startup_info(host: &str, port: u16, root: &str, cert: &str, key: &str, log_level: Option<&str>, max_concurrent: usize) { println!("Pollux Gemini Server"); println!("Listening on: {}:{}", host, port); @@ -104,27 +126,56 @@ async fn main() { .with_no_client_auth() .with_single_cert(certs, key).unwrap(); - let acceptor = TlsAcceptor::from(Arc::new(config)); + let initial_acceptor = TlsAcceptor::from(Arc::new(config)); + let acceptor = Arc::new(Mutex::new(initial_acceptor)); let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await.unwrap(); - + + // Create SIGHUP signal handler for certificate reload + let mut sighup = signal(SignalKind::hangup()) + .map_err(|e| format!("Failed to create SIGHUP handler: {}", e)) + .unwrap(); + // Print startup information print_startup_info(&bind_host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests); loop { - let (stream, _) = listener.accept().await.unwrap(); - tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap())); - let acceptor = acceptor.clone(); - let dir = root.clone(); - let expected_hostname = hostname.clone(); // Use configured hostname - let max_concurrent = max_concurrent_requests; - let test_delay = test_processing_delay; - tokio::spawn(async move { - if let Ok(stream) = acceptor.accept(stream).await { - if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await { - tracing::error!("Error handling connection: {}", e); + tokio::select! { + // Handle new connections + result = listener.accept() => { + let (stream, _) = result.unwrap(); + tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap())); + let acceptor = Arc::clone(&acceptor); + let dir = root.clone(); + let expected_hostname = hostname.clone(); + let max_concurrent = max_concurrent_requests; + let test_delay = test_processing_delay; + tokio::spawn(async move { + let acceptor_guard = acceptor.lock().await; + if let Ok(stream) = acceptor_guard.accept(stream).await { + drop(acceptor_guard); // Release lock before long-running handler + if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await { + tracing::error!("Error handling connection: {}", e); + } + } + }); + } + + // Handle SIGHUP for certificate reload + _ = sighup.recv() => { + tracing::info!("Received SIGHUP, reloading certificates"); + match reload_tls_acceptor(&cert_path, &key_path).await { + Ok(new_acceptor) => { + let mut acceptor_guard = acceptor.lock().await; + *acceptor_guard = new_acceptor; + tracing::info!("TLS certificates reloaded successfully"); + } + Err(e) => { + tracing::error!("Failed to reload TLS certificates: {}", e); + // Continue with old certificates + } } } - }); + } } } \ No newline at end of file