Skip to main content

orca_proxy/
tls.rs

1//! TLS configuration for the reverse proxy.
2//!
3//! Supports: self-signed certs (auto-generated), user-provided certs,
4//! and ACME/Let's Encrypt via `instant-acme` (zero-config auto-TLS).
5
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use rustls::ServerConfig;
10use tokio_rustls::TlsAcceptor;
11use tracing::info;
12
13use crate::acme::AcmeManager;
14
15/// Offer HTTP/2 via ALPN, HTTP/1.1 as fallback. The serve loop handles both
16/// through `hyper_util::server::conn::auto`.
17pub(crate) fn with_h2_alpn(mut config: ServerConfig) -> ServerConfig {
18    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
19    config
20}
21
22/// TLS mode for the proxy.
23#[derive(Debug, Clone)]
24pub enum TlsMode {
25    /// No TLS (HTTP only).
26    None,
27    /// Auto-generated self-signed certificate.
28    SelfSigned,
29    /// User-provided certificate and key files.
30    Custom { cert_path: String, key_path: String },
31    /// ACME/Let's Encrypt via `instant-acme` — fully automatic.
32    ///
33    /// The proxy serves HTTP-01 challenges on port 80 and provisions certs
34    /// automatically when a domain is configured.
35    Acme {
36        /// Email for the Let's Encrypt account registration.
37        email: String,
38        /// Directory to cache provisioned certificates.
39        /// Defaults to `~/.orca/certs/` if not specified.
40        cache_dir: Option<PathBuf>,
41    },
42}
43
44/// Create a TLS acceptor based on the configured mode.
45///
46/// For `TlsMode::Acme`, pass the primary `domain` to load certs for.
47/// Returns `None` if no certs are cached yet (they will be provisioned
48/// automatically when the proxy starts).
49pub fn create_tls_acceptor(mode: &TlsMode) -> anyhow::Result<Option<TlsAcceptor>> {
50    create_tls_acceptor_for_domain(mode, None)
51}
52
53/// Create a TLS acceptor, optionally for a specific ACME domain.
54pub fn create_tls_acceptor_for_domain(
55    mode: &TlsMode,
56    domain: Option<&str>,
57) -> anyhow::Result<Option<TlsAcceptor>> {
58    match mode {
59        TlsMode::None => Ok(None),
60        TlsMode::SelfSigned => {
61            info!("Generating self-signed TLS certificate");
62            let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])?;
63            let cert_der = cert.cert.der().clone();
64            let key_der = cert.key_pair.serialize_der();
65
66            let certs = vec![cert_der];
67            let key = rustls::pki_types::PrivatePkcs8KeyDer::from(key_der).into();
68
69            let config = with_h2_alpn(
70                ServerConfig::builder()
71                    .with_no_client_auth()
72                    .with_single_cert(certs, key)?,
73            );
74
75            Ok(Some(TlsAcceptor::from(Arc::new(config))))
76        }
77        TlsMode::Custom {
78            cert_path,
79            key_path,
80        } => {
81            info!("Loading TLS certificate from {cert_path}");
82            let cert_file = std::fs::read(cert_path)?;
83            let key_file = std::fs::read(key_path)?;
84
85            let certs =
86                rustls_pemfile::certs(&mut cert_file.as_slice()).collect::<Result<Vec<_>, _>>()?;
87            let key = rustls_pemfile::private_key(&mut key_file.as_slice())?
88                .ok_or_else(|| anyhow::anyhow!("no private key found in {key_path}"))?;
89
90            let config = with_h2_alpn(
91                ServerConfig::builder()
92                    .with_no_client_auth()
93                    .with_single_cert(certs, key)?,
94            );
95
96            Ok(Some(TlsAcceptor::from(Arc::new(config))))
97        }
98        TlsMode::Acme { email, cache_dir } => {
99            let cache = cache_dir.clone().unwrap_or_else(|| {
100                dirs::home_dir()
101                    .unwrap_or_else(|| PathBuf::from("."))
102                    .join(".orca/certs")
103            });
104            let manager = AcmeManager::new(email.clone(), cache);
105            manager.secure_key_material();
106
107            let Some(domain) = domain else {
108                // No domain specified — ACME will auto-provision when domains
109                // are registered and the proxy starts.
110                info!("ACME mode: certs will be auto-provisioned on startup");
111                return Ok(None);
112            };
113
114            match manager.tls_acceptor_for(domain)? {
115                Some(acceptor) => {
116                    info!(domain, "Loaded cached ACME certificate");
117                    Ok(Some(acceptor))
118                }
119                None => {
120                    info!(
121                        domain,
122                        "No cached ACME cert — will auto-provision on startup"
123                    );
124                    Ok(None)
125                }
126            }
127        }
128    }
129}