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
This commit is contained in:
Jeena 2026-01-16 13:05:20 +00:00
parent ea8083fe1f
commit caf9d0984f
3 changed files with 102 additions and 14 deletions

35
dist/INSTALL.md vendored
View file

@ -200,6 +200,41 @@ See `config.toml` for all available options. Key settings:
- `max_concurrent_requests`: Connection limit - `max_concurrent_requests`: Connection limit
- `log_level`: Logging verbosity - `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 ## Upgrading
```bash ```bash

2
dist/pollux.service vendored
View file

@ -15,6 +15,8 @@ NoNewPrivileges=yes
ProtectHome=yes ProtectHome=yes
ProtectSystem=strict ProtectSystem=strict
ReadOnlyPaths=/etc/pollux /etc/letsencrypt/live/example.com /var/www/example.com 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: # NOTE: Adjust paths to match your config:
# - /etc/letsencrypt/live/example.com for Let's Encrypt certs # - /etc/letsencrypt/live/example.com for Let's Encrypt certs
# - /var/www/example.com for your content root # - /var/www/example.com for your content root

View file

@ -8,10 +8,32 @@ use clap::Parser;
use rustls::ServerConfig; use rustls::ServerConfig;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::signal::unix::{signal, SignalKind};
use tokio_rustls::TlsAcceptor; use tokio_rustls::TlsAcceptor;
use logging::init_logging; use logging::init_logging;
async fn reload_tls_acceptor(
cert_path: &str,
key_path: &str,
) -> Result<TlsAcceptor, Box<dyn std::error::Error>> {
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) { 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!("Pollux Gemini Server");
println!("Listening on: {}:{}", host, port); println!("Listening on: {}:{}", host, port);
@ -104,27 +126,56 @@ async fn main() {
.with_no_client_auth() .with_no_client_auth()
.with_single_cert(certs, key).unwrap(); .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(); 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 information
print_startup_info(&bind_host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests); print_startup_info(&bind_host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests);
loop { loop {
let (stream, _) = listener.accept().await.unwrap(); 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())); tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap()));
let acceptor = acceptor.clone(); let acceptor = Arc::clone(&acceptor);
let dir = root.clone(); let dir = root.clone();
let expected_hostname = hostname.clone(); // Use configured hostname let expected_hostname = hostname.clone();
let max_concurrent = max_concurrent_requests; let max_concurrent = max_concurrent_requests;
let test_delay = test_processing_delay; let test_delay = test_processing_delay;
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(stream) = acceptor.accept(stream).await { 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 { if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await {
tracing::error!("Error handling connection: {}", e); 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
}
}
}
}
}
} }