diff --git a/crates/aura-server/Cargo.toml b/crates/aura-server/Cargo.toml index b8babf7..b6d406e 100644 --- a/crates/aura-server/Cargo.toml +++ b/crates/aura-server/Cargo.toml @@ -16,7 +16,7 @@ tracing-subscriber = "0.3.19" futures = "0.3.31" tokio-stream = { version = "0.1.18", features = ["net"] } rustls = { version = "0.23.37", default-features = false, features = ["ring"] } -rustls-acme = { version = "0.15.1", features = ["tokio", "axum"] } +rustls-acme = { version = "0.15.4", features = ["tokio", "axum"] } axum = "0.8.9" rustls-pki-types = { version = "1.14.1", features = ["std"] } prost = "0.14.3" diff --git a/crates/aura-server/src/connection.rs b/crates/aura-server/src/connection.rs index af82a75..1d6a040 100644 --- a/crates/aura-server/src/connection.rs +++ b/crates/aura-server/src/connection.rs @@ -61,20 +61,25 @@ pub struct QuicServer { impl QuicServer { /// Create a new QUIC server based on the provided configuration. pub fn new(bind_addr: SocketAddr, state: Arc) -> Result { - // We're bypassing ACME for now due to JSON parsing issues with Let's Encrypt Staging ("missing field token"). - // Falling back to self-signed or manual TLS as requested. - - // Still start the health-check listener so Fly.io doesn't think we're dead - Self::start_health_check_listener(&state); - + // TLS source priority: explicit cert/key files > ACME (Let's Encrypt) > self-signed. let server_config = if let (Some(cert_path), Some(key_path)) = ( &state.config.server.cert_path, &state.config.server.key_path, ) { info!("Loading custom TLS certificates from {:?}...", cert_path); + // Nothing else binds the health-check port in this mode. + Self::start_health_check_listener(&state); Self::configure_manual_tls(cert_path, key_path)? + } else if let Some(domain) = state.config.server.acme_domain.clone() { + info!("Configuring ACME (Let's Encrypt) for domain {}...", domain); + // configure_acme spawns its own Axum server on acme_bind_port that + // serves the HTTP-01 challenge route AND a "/" health check, so we + // must NOT also call start_health_check_listener — both would bind + // acme_bind_port and the second bind would fail. + Self::configure_acme(&domain, &state)? } else { - info!("ACME disabled or bypassed. Generating self-signed fallback..."); + info!("No TLS cert or ACME domain configured. Generating self-signed fallback..."); + Self::start_health_check_listener(&state); Self::generate_self_signed_config()? }; @@ -101,7 +106,6 @@ impl QuicServer { }) } - #[allow(dead_code)] /// Configure ACME (Let's Encrypt) for automated certificate management. fn configure_acme(domain: &str, state: &Arc) -> Result { let contact = state @@ -132,6 +136,11 @@ impl QuicServer { if let Some(url) = &state.config.server.acme_directory_url { info!("[ACME] Using custom directory URL: {}", url); acme_builder = acme_builder.directory(url); + } else { + // rustls-acme's builder defaults to the Let's Encrypt STAGING + // directory, whose certificates clients don't trust. No configured + // URL means production. + acme_builder = acme_builder.directory_lets_encrypt(true); } let mut acme_state = acme_builder.state(); diff --git a/fly.toml b/fly.toml index 8d8f0c1..8546a0f 100644 --- a/fly.toml +++ b/fly.toml @@ -16,7 +16,8 @@ primary_region = 'ord' AURA_ACME_CACHE_PATH = '/data/acme' AURA_BIND_ADDRESS = '0.0.0.0:8443' AURA_DATABASE_PATH = '/data/aura.db' - AURA_ACME_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' + # Unset = Let's Encrypt production. Uncomment to test against staging: + # AURA_ACME_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' [[mounts]] source = 'aura_data'