diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d913617 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target + diff --git a/Cargo.lock b/Cargo.lock index 66da4cc..38e8c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,6 +993,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pem" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06673860db84d02a63942fa69cd9543f2624a5df3aea7f33173048fa7ad5cf1a" +dependencies = [ + "base64", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1146,6 +1157,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rcgen" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5911d1403f4143c9d56a702069d593e8d0f3fab880a85e103604d0893ea31ba7" +dependencies = [ + "chrono", + "pem", + "ring", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -1893,6 +1916,7 @@ dependencies = [ "lazy_static", "log", "pretty_env_logger", + "rcgen", "regex", "reqwest", "rustls 0.20.2", @@ -2201,3 +2225,12 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "yasna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +dependencies = [ + "chrono", +] diff --git a/Cargo.toml b/Cargo.toml index bd40fe6..5dca10f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,4 @@ unicase = "2" lazy_static = "1.4" structopt = "0.3" glob = "0.3" +rcgen = "0.8" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ebda1fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM rust:1.57-bullseye as builder + +RUN apt-get update && \ + apt-get install -y libssl-dev pkg-config + +WORKDIR /srv + +# Build dependencies and cache them +COPY Cargo.* ./ +RUN mkdir -p src && \ + echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \ + cargo build --release && \ + rm -r src && \ + rm target/release/deps/tricot* + +# Build final app +COPY ./src ./src +RUN cargo build --release + +FROM debian:bullseye-slim +RUN apt-get update && apt-get install -y libssl1.1 iptables +COPY --from=builder /srv/target/release/tricot /usr/local/sbin/tricot +CMD ["/usr/local/sbin/tricot"] diff --git a/src/cert_store.rs b/src/cert_store.rs index 8d45df4..4cc2fc0 100644 --- a/src/cert_store.rs +++ b/src/cert_store.rs @@ -19,6 +19,7 @@ use crate::proxy_config::*; pub struct CertStore { consul: Consul, certs: RwLock>>, + self_signed_certs: RwLock>>, rx_proxy_config: watch::Receiver>, } @@ -27,6 +28,7 @@ impl CertStore { Arc::new(Self { consul, certs: RwLock::new(HashMap::new()), + self_signed_certs: RwLock::new(HashMap::new()), rx_proxy_config, }) } @@ -66,16 +68,23 @@ impl CertStore { } // Check in local memory if it exists - let certs = self.certs.read().unwrap(); - if let Some(cert) = certs.get(domain) { + if let Some(cert) = self.certs.read().unwrap().get(domain) { if !cert.is_old() { return Ok(cert.clone()); } } - // Not found in local memory + // Not found in local memory, try to get it in background tokio::spawn(self.clone().get_cert_task(domain.to_string())); - bail!("Certificate not found (will try to get it in background)"); + + // In the meantime, use a self-signed certificate + if let Some(cert) = self.self_signed_certs.read().unwrap().get(domain) { + if !cert.is_old() { + return Ok(cert.clone()); + } + } + + self.gen_self_signed_certificate(domain) } pub async fn get_cert_task(self: Arc, domain: String) -> Result> { @@ -221,6 +230,27 @@ impl CertStore { info!("Cert successfully renewed: {}", domain); Ok(cert) } + + fn gen_self_signed_certificate(&self, domain: &str) -> Result> { + let subject_alt_names = vec![domain.to_string(), "localhost".to_string()]; + let cert = rcgen::generate_simple_self_signed(subject_alt_names)?; + + let certser = CertSer { + hostname: domain.to_string(), + date: Utc::today().naive_utc(), + valid_days: 1024, + key_pem: cert.serialize_private_key_pem(), + cert_pem: cert.serialize_pem()?, + }; + let cert = Arc::new(Cert::new(certser)?); + self.self_signed_certs + .write() + .unwrap() + .insert(domain.to_string(), cert.clone()); + info!("Added self-signed certificate for {}", domain); + + Ok(cert) + } } pub struct StoreResolver(pub Arc); @@ -228,7 +258,12 @@ pub struct StoreResolver(pub Arc); impl rustls::server::ResolvesServerCert for StoreResolver { fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option> { let domain = client_hello.server_name()?; - let cert = self.0.get_cert_for_https(domain).ok()?; - Some(cert.certkey.clone()) + match self.0.get_cert_for_https(domain) { + Ok(cert) => Some(cert.certkey.clone()), + Err(e) => { + warn!("Could not get certificate for {}: {}", domain, e); + None + } + } } } diff --git a/src/proxy_config.rs b/src/proxy_config.rs index 2807a3b..1b46305 100644 --- a/src/proxy_config.rs +++ b/src/proxy_config.rs @@ -60,13 +60,17 @@ impl std::fmt::Display for ProxyEntry { HostDescription::Hostname(h) => write!(f, "{}", h)?, HostDescription::Pattern(p) => write!(f, "Pattern('{}')", p.as_str())?, } - write!(f, "{} {}", self.path_prefix.as_ref().unwrap_or(&String::new()), self.priority)?; + write!( + f, + "{} {}", + self.path_prefix.as_ref().unwrap_or(&String::new()), + self.priority + )?; if !self.add_headers.is_empty() { write!(f, "+Headers: {:?}", self.add_headers)?; } Ok(()) } - } #[derive(Debug)]