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