From b9b035034ff7dee1089d8c629296391fe0539515 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 30 Nov 2023 17:34:07 +0100 Subject: [PATCH] centralize all the checks in the same place --- src/cert_store.rs | 102 +++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/src/cert_store.rs b/src/cert_store.rs index 7c6b4b3..edbd0a1 100644 --- a/src/cert_store.rs +++ b/src/cert_store.rs @@ -22,12 +22,19 @@ pub struct CertStore { consul: Consul, node_name: String, letsencrypt_email: String, + certs: RwLock>>, self_signed_certs: RwLock>>, + rx_proxy_config: watch::Receiver>, tx_need_cert: mpsc::UnboundedSender, } +struct ProcessedDomains { + static_domains: HashSet, + on_demand_domains: Vec<(glob::Pattern, Option)>, +} + impl CertStore { pub fn new( consul: Consul, @@ -41,10 +48,10 @@ impl CertStore { let cert_store = Arc::new(Self { consul, node_name, + letsencrypt_email, certs: RwLock::new(HashMap::new()), self_signed_certs: RwLock::new(HashMap::new()), rx_proxy_config, - letsencrypt_email, tx_need_cert: tx, }); @@ -66,57 +73,55 @@ impl CertStore { let mut rx_proxy_config = self.rx_proxy_config.clone(); let mut t_last_check: HashMap = HashMap::new(); - - // Collect data from proxy config - let mut static_domains: HashSet = HashSet::new(); - let mut on_demand_checks: Vec<(glob::Pattern, Option)> = vec![]; + let mut proc_domains: Option = None; loop { - // Collect domains that need a TLS certificate - // either from the proxy configuration (eagerly) - // or on reaction to a user request (lazily) let domains = select! { + // Refresh some internal states, schedule static_domains for renew res = rx_proxy_config.changed() => { if res.is_err() { bail!("rx_proxy_config closed"); } - on_demand_checks.clear(); + let mut static_domains: HashSet = HashSet::new(); + let mut on_demand_domains: Vec<(glob::Pattern, Option)> = vec![]; + let proxy_config: Arc = rx_proxy_config.borrow().clone(); + for ent in proxy_config.entries.iter() { - // Eagerly generate certificates for domains that - // are not patterns - match &ent.url_prefix.host { - HostDescription::Hostname(domain) => { - if let Some((host, _port)) = domain.split_once(':') { - static_domains.insert(host.to_string()); - //domains.insert(host.to_string()); - } else { - static_domains.insert(domain.clone()); - //domains.insert(domain.clone()); - } + // Eagerly generate certificates for domains that + // are not patterns + match &ent.url_prefix.host { + HostDescription::Hostname(domain) => { + if let Some((host, _port)) = domain.split_once(':') { + static_domains.insert(host.to_string()); + } else { + static_domains.insert(domain.clone()); + } }, - HostDescription::Pattern(pattern) => { - on_demand_checks.push((pattern.clone(), ent.on_demand_tls_ask.clone())); - } - } + HostDescription::Pattern(pattern) => { + on_demand_domains.push((pattern.clone(), ent.on_demand_tls_ask.clone())); + }, + } } - // only static_domains are refreshed - static_domains.clone() + // only static_domains are refreshed + proc_domains = Some(ProcessedDomains { static_domains: static_domains.clone(), on_demand_domains }); + self.domain_validation(static_domains, proc_domains.as_ref()).await } + // renew static and on-demand domains need_cert = rx_need_cert.recv() => { match need_cert { Some(dom) => { - let mut candidates: HashSet = HashSet::new(); + let mut candidates: HashSet = HashSet::new(); - // collect certificates as much as possible + // collect certificates as much as possible candidates.insert(dom); while let Ok(dom2) = rx_need_cert.try_recv() { candidates.insert(dom2); } - self.domain_validation(candidates, &static_domains, on_demand_checks.as_slice()).await + self.domain_validation(candidates, proc_domains.as_ref()).await } None => bail!("rx_need_cert closed"), } @@ -145,28 +150,36 @@ impl CertStore { async fn domain_validation( &self, candidates: HashSet, - static_domains: &HashSet, - checks: &[(glob::Pattern, Option)], + maybe_proc_domains: Option<&ProcessedDomains>, ) -> HashSet { let mut domains: HashSet = HashSet::new(); + // Handle initialization + let proc_domains = match maybe_proc_domains { + None => { + warn!("Proxy config is not yet loaded, refusing all certificate generation"); + return domains; + } + Some(proc) => proc, + }; + // Filter certificates... - for candidate in candidates.into_iter() { + 'outer: for candidate in candidates.into_iter() { // Disallow obvious wrong domains... if !candidate.contains('.') || candidate.ends_with(".local") { - warn!("Probably not a publicly accessible domain, skipping (a self-signed certificate will be used)"); + warn!("{} is probably not a publicly accessible domain, skipping (a self-signed certificate will be used)", candidate); continue; } // Try to register domain as a static domain - if static_domains.contains(&candidate) { + if proc_domains.static_domains.contains(&candidate) { trace!("domain {} validated as static domain", candidate); domains.insert(candidate); continue; } // It's not a static domain, maybe an on-demand domain? - for (pattern, maybe_check_url) in checks.iter() { + for (pattern, maybe_check_url) in proc_domains.on_demand_domains.iter() { // check glob pattern if pattern.matches(&candidate) { // if no check url is set, accept domain as long as it matches the pattern @@ -178,12 +191,14 @@ impl CertStore { pattern ); domains.insert(candidate); - break; + continue 'outer; } Some(url) => url, }; // if a check url is set, call it + // -- avoid DDoSing a backend + tokio::time::sleep(Duration::from_secs(2)).await; match self.on_demand_tls_ask(check_url, &candidate).await { Ok(()) => { trace!( @@ -193,7 +208,7 @@ impl CertStore { check_url ); domains.insert(candidate); - break; + continue 'outer; } Err(e) => { warn!("domain {} validation refused on glob pattern {} and on check url {} with error: {}", candidate, pattern, check_url, e); @@ -201,8 +216,6 @@ impl CertStore { } } } - // Avoid DDoSing a backend - tokio::time::sleep(Duration::from_secs(2)).await; } return domains; @@ -210,17 +223,6 @@ impl CertStore { /// This function is also in charge of the refresh of the domain names fn get_cert_for_https(self: &Arc, domain: &str) -> Result> { - // Check if domain is authorized - if !self - .rx_proxy_config - .borrow() - .entries - .iter() - .any(|ent| ent.url_prefix.host.matches(domain)) - { - bail!("Domain {} should not have a TLS certificate.", domain); - } - // Check in local memory if it exists if let Some(cert) = self.certs.read().unwrap().get(domain) { if cert.is_old() {