aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2023-11-30 16:53:04 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2023-11-30 16:53:04 +0100
commit753903ee021e7aac7c0914883ae8d547148b4d95 (patch)
tree0bd8ecb2297f07d5282a25f4a44f293b41f5766f
parentca449ebff452345229953b6ba406acefb157385b (diff)
downloadtricot-753903ee021e7aac7c0914883ae8d547148b4d95.tar.gz
tricot-753903ee021e7aac7c0914883ae8d547148b4d95.zip
implement feature
-rw-r--r--src/cert_store.rs169
-rw-r--r--src/proxy_config.rs23
2 files changed, 143 insertions, 49 deletions
diff --git a/src/cert_store.rs b/src/cert_store.rs
index e25395d..7c6b4b3 100644
--- a/src/cert_store.rs
+++ b/src/cert_store.rs
@@ -67,58 +67,73 @@ impl CertStore {
let mut t_last_check: HashMap<String, Instant> = HashMap::new();
- loop {
- let mut domains: HashSet<String> = HashSet::new();
+ // Collect data from proxy config
+ let mut static_domains: HashSet<String> = HashSet::new();
+ let mut on_demand_checks: Vec<(glob::Pattern, Option<String>)> = vec![];
- // Collect domains that need a TLS certificate
- // either from the proxy configuration (eagerly)
- // or on reaction to a user request (lazily)
- select! {
+ 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! {
res = rx_proxy_config.changed() => {
if res.is_err() {
bail!("rx_proxy_config closed");
}
+ on_demand_checks.clear();
let proxy_config: Arc<ProxyConfig> = rx_proxy_config.borrow().clone();
for ent in proxy_config.entries.iter() {
- // Eagerly generate certificates for domains that
- // are not patterns
- if let HostDescription::Hostname(domain) = &ent.url_prefix.host {
+ // 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(':') {
- domains.insert(host.to_string());
+ static_domains.insert(host.to_string());
+ //domains.insert(host.to_string());
} else {
- domains.insert(domain.clone());
+ static_domains.insert(domain.clone());
+ //domains.insert(domain.clone());
}
- }
-
- // @TODO Register a map of
- // UrlPrefix -> OnDemandTlsAskCheckUrl
+ },
+ HostDescription::Pattern(pattern) => {
+ on_demand_checks.push((pattern.clone(), ent.on_demand_tls_ask.clone()));
+ }
+ }
}
+
+ // only static_domains are refreshed
+ static_domains.clone()
}
need_cert = rx_need_cert.recv() => {
match need_cert {
Some(dom) => {
- domains.insert(dom);
+ let mut candidates: HashSet<String> = HashSet::new();
+
+ // collect certificates as much as possible
+ candidates.insert(dom);
while let Ok(dom2) = rx_need_cert.try_recv() {
- domains.insert(dom2);
+ candidates.insert(dom2);
}
+
+ self.domain_validation(candidates, &static_domains, on_demand_checks.as_slice()).await
}
None => bail!("rx_need_cert closed"),
- };
+ }
}
- }
+ };
- // Now that we have our list of domains to check,
- // actually do something
+ // Now that we have our list of domains to check,
+ // actually do something
for dom in domains.iter() {
- // Exclude from the list domains that were checked less than 60
- // seconds ago
+ // Exclude from the list domains that were checked less than 60
+ // seconds ago
match t_last_check.get(dom) {
Some(t) if Instant::now() - *t < Duration::from_secs(60) => continue,
_ => t_last_check.insert(dom.to_string(), Instant::now()),
};
- // Actual Let's Encrypt calls are done here (in sister function)
+ // Actual Let's Encrypt calls are done here (in sister function)
debug!("Checking cert for domain: {}", dom);
if let Err(e) = self.check_cert(dom).await {
warn!("({}) Could not get certificate: {}", dom, e);
@@ -127,6 +142,73 @@ impl CertStore {
}
}
+ async fn domain_validation(
+ &self,
+ candidates: HashSet<String>,
+ static_domains: &HashSet<String>,
+ checks: &[(glob::Pattern, Option<String>)],
+ ) -> HashSet<String> {
+ let mut domains: HashSet<String> = HashSet::new();
+
+ // Filter certificates...
+ 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)");
+ continue;
+ }
+
+ // Try to register domain as a static domain
+ if 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() {
+ // check glob pattern
+ if pattern.matches(&candidate) {
+ // if no check url is set, accept domain as long as it matches the pattern
+ let check_url = match maybe_check_url {
+ None => {
+ trace!(
+ "domain {} validated on glob pattern {} only",
+ candidate,
+ pattern
+ );
+ domains.insert(candidate);
+ break;
+ }
+ Some(url) => url,
+ };
+
+ // if a check url is set, call it
+ match self.on_demand_tls_ask(check_url, &candidate).await {
+ Ok(()) => {
+ trace!(
+ "domain {} validated on glob pattern {} and on check url {}",
+ candidate,
+ pattern,
+ check_url
+ );
+ domains.insert(candidate);
+ break;
+ }
+ Err(e) => {
+ warn!("domain {} validation refused on glob pattern {} and on check url {} with error: {}", candidate, pattern, check_url, e);
+ }
+ }
+ }
+ }
+ // Avoid DDoSing a backend
+ tokio::time::sleep(Duration::from_secs(2)).await;
+ }
+
+ return domains;
+ }
+
+ /// This function is also in charge of the refresh of the domain names
fn get_cert_for_https(self: &Arc<Self>, domain: &str) -> Result<Arc<Cert>> {
// Check if domain is authorized
if !self
@@ -199,15 +281,15 @@ impl CertStore {
Ok(())
}
- /// Check certificate ensure that the certificate is in the memory store
- /// and that it does not need to be renewed.
- ///
- /// If it's not in the memory store, it tries to load it from Consul,
- /// if it's not in Consul, it calls Let's Encrypt.
- ///
- /// If the certificate is outdated in the memory store, it tries to load
- /// a more recent version in Consul, if the Consul version is also outdated,
- /// it tries to renew it
+ /// Check certificate ensure that the certificate is in the memory store
+ /// and that it does not need to be renewed.
+ ///
+ /// If it's not in the memory store, it tries to load it from Consul,
+ /// if it's not in Consul, it calls Let's Encrypt.
+ ///
+ /// If the certificate is outdated in the memory store, it tries to load
+ /// a more recent version in Consul, if the Consul version is also outdated,
+ /// it tries to renew it
pub async fn check_cert(self: &Arc<Self>, domain: &str) -> Result<()> {
// First, try locally.
{
@@ -248,16 +330,10 @@ impl CertStore {
self.renew_cert(domain).await
}
- /// This is the place where certificates are generated or renewed
+ /// This is the place where certificates are generated or renewed
pub async fn renew_cert(self: &Arc<Self>, domain: &str) -> Result<()> {
info!("({}) Renewing certificate", domain);
- // Basic sanity check (we could add more kinds of checks here)
- // This is just to help avoid getting rate-limited against ACME server
- if !domain.contains('.') || domain.ends_with(".local") {
- bail!("Probably not a publicly accessible domain, skipping (a self-signed certificate will be used)");
- }
-
// ---- Acquire lock ----
// the lock is acquired for half an hour,
// so that in case of an error we won't retry before
@@ -373,6 +449,19 @@ impl CertStore {
Ok(())
}
+ async fn on_demand_tls_ask(&self, check_url: &str, domain: &str) -> Result<()> {
+ let httpcli = reqwest::Client::new();
+ let chall_url = format!("{}?domain={}", check_url, domain);
+ info!("({}) On-demand TLS check", domain);
+
+ let httpresp = httpcli.get(&chall_url).send().await?;
+ if httpresp.status() != reqwest::StatusCode::OK {
+ bail!("{} is not authorized for on-demand TLS", domain);
+ }
+
+ Ok(())
+ }
+
async fn check_domain_accessibility(&self, domain: &str, session: &str) -> Result<()> {
// Returns Ok(()) only if domain is a correct domain name that
// redirects to this server
diff --git a/src/proxy_config.rs b/src/proxy_config.rs
index 8381de2..7690f8a 100644
--- a/src/proxy_config.rs
+++ b/src/proxy_config.rs
@@ -108,6 +108,10 @@ pub struct ProxyEntry {
/// when matching this rule
pub redirects: Vec<(UrlPrefix, UrlPrefix, u16)>,
+ /// Wether or not the domain must be validated before asking a certificate
+ /// to let's encrypt (only for Glob patterns)
+ pub on_demand_tls_ask: Option<String>,
+
/// Number of calls in progress, used to deprioritize slow back-ends
pub calls_in_progress: atomic::AtomicI64,
/// Time of last call, used for round-robin selection
@@ -142,14 +146,14 @@ impl ProxyEntry {
let mut add_headers = vec![];
let mut redirects = vec![];
+ let mut on_demand_tls_ask: Option<String> = None;
for mid in middleware.into_iter() {
+ // LocalLb and GlobalLb are handled in the parent function
match mid {
ConfigTag::AddHeader(k, v) => add_headers.push((k.to_string(), v.clone())),
ConfigTag::AddRedirect(m, r, c) => redirects.push(((*m).clone(), (*r).clone(), *c)),
- ConfigTag::LocalLb | ConfigTag::GlobalLb => {
- /* handled in parent fx */
- ()
- }
+ ConfigTag::OnDemandTlsAsk(url) => on_demand_tls_ask = Some(url.to_string()),
+ ConfigTag::LocalLb | ConfigTag::GlobalLb => (),
};
}
@@ -166,6 +170,7 @@ impl ProxyEntry {
flags,
add_headers,
redirects,
+ on_demand_tls_ask,
// internal
last_call: atomic::AtomicI64::from(0),
calls_in_progress: atomic::AtomicI64::from(0),
@@ -247,6 +252,7 @@ enum MatchTag {
enum ConfigTag<'a> {
AddHeader(&'a str, String),
AddRedirect(UrlPrefix, UrlPrefix, u16),
+ OnDemandTlsAsk(&'a str),
GlobalLb,
LocalLb,
}
@@ -321,6 +327,9 @@ fn parse_tricot_tags(tag: &str) -> Option<ParsedTag> {
p_match, p_replace, http_code,
)))
}
+ ["tricot-on-demand-tls-ask", url, ..] => {
+ Some(ParsedTag::Middleware(ConfigTag::OnDemandTlsAsk(url)))
+ }
["tricot-global-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::GlobalLb)),
["tricot-local-lb", ..] => Some(ParsedTag::Middleware(ConfigTag::LocalLb)),
_ => None,
@@ -369,13 +378,9 @@ fn parse_consul_service(
// some legacy processing that would need a refactor later
for mid in collected_middleware.iter() {
match mid {
- ConfigTag::AddHeader(_, _) | ConfigTag::AddRedirect(_, _, _) =>
- /* not handled here */
- {
- ()
- }
ConfigTag::GlobalLb => flags.global_lb = true,
ConfigTag::LocalLb => flags.site_lb = true,
+ _ => (),
};
}