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