diff --git a/CHANGELOG.md b/CHANGELOG.md index 90eaad5..cf748e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The minimum supported Rust version (MSRV) is now 1.64. - Manual (and badly designed) threads have been replaced by async. - Randomized early delay, for spacing out renewals when dealing with a lot of certificates. +- Reworked rate-limits, now with scopes for API paths and ACME resources. ## [0.21.0] - 2022-12-19 diff --git a/Cargo.lock b/Cargo.lock index 80c49be..9f5892b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,10 +34,13 @@ dependencies = [ "clap", "futures", "glob", + "governor", + "itertools", "log", "nix", "nom", "rand", + "regex", "reqwest", "serde", "serde_json", @@ -317,6 +320,12 @@ dependencies = [ "libc", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -500,6 +509,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -535,6 +550,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "governor" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +dependencies = [ + "cfg-if", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "smallvec", +] + [[package]] name = "h2" version = "0.3.19" @@ -721,6 +751,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -846,6 +885,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -856,6 +901,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num_cpus" version = "1.15.0" diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index 22de4af..f19f5c1 100644 --- a/acmed/Cargo.toml +++ b/acmed/Cargo.toml @@ -36,6 +36,9 @@ toml = "0.7" tokio = { version = "1", features = ["full"] } rand = "0.8.5" reqwest = "0.11.16" +governor = { version = "0.5.1", default-features = false, features = ["std"] } +regex = "1.7.3" +itertools = "0.10.5" [target.'cfg(unix)'.dependencies] nix = "0.26" diff --git a/acmed/config/letsencrypt.toml b/acmed/config/letsencrypt.toml index 60700f4..659abf3 100644 --- a/acmed/config/letsencrypt.toml +++ b/acmed/config/letsencrypt.toml @@ -1,16 +1,43 @@ [[rate-limit]] -name = "Let's Encrypt rate-limit" +name = "Let's Encrypt newOrder" +acme_resources = ["newOrder"] +number = 300 +period = "3h" + +[[rate-limit]] +name = "Let's Encrypt overall named resources" +acme_resources = ["newNonce", "newAccount", "newOrder", "revokeCert"] number = 20 period = "1s" +[[rate-limit]] +name = "Let's Encrypt overall path prefix" +path = "^https://acmed-v02.api\.letsencrypt\.org/(directory)|(acme/.*)$" +number = 40 +period = "1s" + [[endpoint]] name = "Let's Encrypt v2 production" url = "https://acme-v02.api.letsencrypt.org/directory" -rate_limits = ["Let's Encrypt rate-limit"] +rate_limits = [ + "Let's Encrypt newOrder", + "Let's Encrypt overall named resources", + "Let's Encrypt overall path prefix" +] tos_agreed = false +[[rate-limit]] +name = "Let's Encrypt newOrder staging" +acme_resources = ["newOrder"] +number = 300 +period = "3h" + [[endpoint]] name = "Let's Encrypt v2 staging" url = "https://acme-staging-v02.api.letsencrypt.org/directory" -rate_limits = ["Let's Encrypt rate-limit"] +rate_limits = [ + "Let's Encrypt newOrder staging", + "Let's Encrypt overall named resources", + "Let's Encrypt overall path prefix" +] tos_agreed = false diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index e16b28b..b3dd8f3 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -180,6 +180,7 @@ pub async fn request_certificate( &mut *(endpoint_s.write().await), &data_builder, &chall_url, + None, ) .await .map_err(HttpError::in_err)?; diff --git a/acmed/src/acme_proto/account.rs b/acmed/src/acme_proto/account.rs index 8b8fec0..558f479 100644 --- a/acmed/src/acme_proto/account.rs +++ b/acmed/src/acme_proto/account.rs @@ -96,7 +96,7 @@ pub async fn update_account_contacts( set_data_builder_sync!(account_owned, endpoint_name, acc_up_struct.as_bytes()); let url = account.get_endpoint(&endpoint_name)?.account_url.clone(); create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, &data_builder, &url).await, + http::post_jose_no_response(endpoint, &data_builder, &url, None).await, endpoint, account )?; @@ -141,7 +141,7 @@ pub async fn update_account_key( ) }; create_account_if_does_not_exist!( - http::post_jose_no_response(endpoint, &data_builder, &url).await, + http::post_jose_no_response(endpoint, &data_builder, &url, None).await, endpoint, account )?; diff --git a/acmed/src/acme_proto/http.rs b/acmed/src/acme_proto/http.rs index 212ef1c..1d31ecd 100644 --- a/acmed/src/acme_proto/http.rs +++ b/acmed/src/acme_proto/http.rs @@ -1,14 +1,15 @@ use crate::acme_proto::structs::{AccountResponse, Authorization, Directory, Order}; +use crate::config::NamedAcmeResource; use crate::endpoint::Endpoint; use crate::http; use acme_common::error::Error; use std::{thread, time}; macro_rules! pool_object { - ($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $data_builder: expr, $break: expr) => {{ + ($obj_type: ty, $obj_name: expr, $endpoint: expr, $url: expr, $resource: expr, $data_builder: expr, $break: expr) => {{ for _ in 0..crate::DEFAULT_POOL_NB_TRIES { thread::sleep(time::Duration::from_secs(crate::DEFAULT_POOL_WAIT_SEC)); - let response = http::post_jose($endpoint, $url, $data_builder).await?; + let response = http::post_jose($endpoint, $url, $resource, $data_builder).await?; let obj = response.json::<$obj_type>()?; if $break(&obj) { return Ok(obj); @@ -21,7 +22,7 @@ macro_rules! pool_object { pub async fn refresh_directory(endpoint: &mut Endpoint) -> Result<(), http::HttpError> { let url = endpoint.url.clone(); - let response = http::get(endpoint, &url).await?; + let response = http::get(endpoint, &url, Some(NamedAcmeResource::Directory)).await?; endpoint.dir = response.json::()?; Ok(()) } @@ -30,11 +31,12 @@ pub async fn post_jose_no_response( endpoint: &mut Endpoint, data_builder: &F, url: &str, + resource: Option, ) -> Result<(), http::HttpError> where F: Fn(&str, &str) -> Result, { - let _ = http::post_jose(endpoint, url, data_builder).await?; + let _ = http::post_jose(endpoint, url, resource, data_builder).await?; Ok(()) } @@ -46,7 +48,13 @@ where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_account.clone(); - let response = http::post_jose(endpoint, &url, data_builder).await?; + let response = http::post_jose( + endpoint, + &url, + Some(NamedAcmeResource::NewAccount), + data_builder, + ) + .await?; let acc_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -62,7 +70,13 @@ where F: Fn(&str, &str) -> Result, { let url = endpoint.dir.new_order.clone(); - let response = http::post_jose(endpoint, &url, data_builder).await?; + let response = http::post_jose( + endpoint, + &url, + Some(NamedAcmeResource::NewOrder), + data_builder, + ) + .await?; let order_uri = response .get_header(http::HEADER_LOCATION) .ok_or_else(|| Error::from("no account location found"))?; @@ -78,7 +92,7 @@ pub async fn get_authorization( where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, url, data_builder).await?; + let response = http::post_jose(endpoint, url, None, data_builder).await?; let auth = response.json::()?; Ok(auth) } @@ -98,6 +112,7 @@ where "authorization", endpoint, url, + None, data_builder, break_fn ) @@ -113,7 +128,7 @@ where F: Fn(&str, &str) -> Result, S: Fn(&Order) -> bool, { - pool_object!(Order, "order", endpoint, url, data_builder, break_fn) + pool_object!(Order, "order", endpoint, url, None, data_builder, break_fn) } pub async fn finalize_order( @@ -124,7 +139,7 @@ pub async fn finalize_order( where F: Fn(&str, &str) -> Result, { - let response = http::post_jose(endpoint, url, data_builder).await?; + let response = http::post_jose(endpoint, url, None, data_builder).await?; let order = response.json::()?; Ok(order) } @@ -140,6 +155,7 @@ where let response = http::post( endpoint, url, + None, data_builder, http::CONTENT_TYPE_JOSE, http::CONTENT_TYPE_PEM, diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 032654d..59f812b 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -12,6 +12,7 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt; use std::fs::{self, File}; use std::io::prelude::*; +use std::num::NonZeroU32; use std::path::{Path, PathBuf}; use std::result::Result; use std::time::Duration; @@ -72,10 +73,10 @@ pub struct Config { } impl Config { - fn get_rate_limit(&self, name: &str) -> Result<(usize, String), Error> { + fn get_rate_limit(&self, name: &str) -> Result { for rl in self.rate_limit.iter() { if rl.name == name { - return Ok((rl.number, rl.period.to_owned())); + return Ok(rl.clone()); } } Err(format!("{name}: rate limit not found").into()) @@ -266,8 +267,7 @@ impl Endpoint { ) -> Result { let mut limits = vec![]; for rl_name in self.rate_limits.iter() { - let (nb, timeframe) = cnf.get_rate_limit(rl_name)?; - limits.push((nb, timeframe)); + limits.push(cnf.get_rate_limit(rl_name)?); } let mut root_lst: Vec = vec![]; root_lst.extend(root_certs.iter().map(|v| v.to_string())); @@ -293,8 +293,23 @@ impl Endpoint { #[serde(deny_unknown_fields)] pub struct RateLimit { pub name: String, - pub number: usize, + pub number: NonZeroU32, pub period: String, + #[serde(default)] + pub acme_resources: Vec, + pub path: Option, +} + +#[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub enum NamedAcmeResource { + Directory, + NewNonce, + NewAccount, + NewOrder, + NewAuthz, + RevokeCert, + KeyChange, } #[derive(Deserialize)] diff --git a/acmed/src/endpoint.rs b/acmed/src/endpoint.rs index 5279c7b..8e7fa7f 100644 --- a/acmed/src/endpoint.rs +++ b/acmed/src/endpoint.rs @@ -1,17 +1,24 @@ -use crate::acme_proto::structs::Directory; +use crate::config::NamedAcmeResource; use crate::duration::parse_duration; +use crate::{acme_proto::structs::Directory, config}; use acme_common::error::Error; -use std::cmp; -use std::time::{Duration, Instant}; -use tokio::time::sleep; +use governor::{ + clock::DefaultClock, + state::{direct::NotKeyed, InMemoryState}, + Quota, RateLimiter, +}; +use itertools::Itertools; +use regex::Regex; +use std::convert::{TryFrom, TryInto}; +use std::time::Duration; -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Endpoint { pub name: String, pub url: String, pub tos_agreed: bool, pub nonce: Option, - pub rl: RateLimit, + pub rl: RateLimits, pub dir: Directory, pub root_certificates: Vec, } @@ -21,7 +28,7 @@ impl Endpoint { name: &str, url: &str, tos_agreed: bool, - limits: &[(usize, String)], + limits: &[config::RateLimit], root_certs: &[String], ) -> Result { Ok(Self { @@ -29,7 +36,7 @@ impl Endpoint { url: url.to_string(), tos_agreed, nonce: None, - rl: RateLimit::new(limits)?, + rl: RateLimits::new(limits)?, dir: Directory { meta: None, new_nonce: String::new(), @@ -44,87 +51,121 @@ impl Endpoint { } } -#[derive(Clone, Debug)] -pub struct RateLimit { - limits: Vec<(usize, Duration)>, - query_log: Vec, +#[derive(Debug)] +pub struct RateLimits { + limits: Vec, } -impl RateLimit { - pub fn new(raw_limits: &[(usize, String)]) -> Result { - let mut limits = vec![]; - for (nb, raw_duration) in raw_limits.iter() { - let parsed_duration = parse_duration(raw_duration)?; - limits.push((*nb, parsed_duration)); - } - limits.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - limits.reverse(); - Ok(Self { - limits, - query_log: vec![], - }) +impl RateLimits { + pub fn new(raw_limits: &[config::RateLimit]) -> Result { + let limits: Result, Error> = raw_limits + .iter() + .sorted_by(rate_limit_cmp) + // We're reverting the comparison here, as we want to get the strongest limits (those + // with the highest waiting period per request) first. + .rev() + .map(|raw| raw.try_into()) + .collect(); + Ok(Self { limits: limits? }) } - pub async fn block_until_allowed(&mut self) { - if self.limits.is_empty() { - return; - } - let mut sleep_duration = self.get_sleep_duration(); - loop { - sleep(sleep_duration).await; - self.prune_log(); - if self.request_allowed() { - self.query_log.push(Instant::now()); - return; + pub async fn block_until_allowed(&mut self, resource: Option, path: &str) { + for limit in &self.limits { + if limit.matches(resource, path) { + limit.until_ready().await } - sleep_duration = self.get_sleep_duration(); } } +} - fn get_sleep_duration(&self) -> Duration { - let (nb_req, min_duration) = match self.limits.last() { - Some((n, d)) => (*n as u64, *d), - None => { - return Duration::from_millis(0); - } - }; - let nb_mili = match min_duration.as_secs() { - 0 | 1 => crate::MIN_RATE_LIMIT_SLEEP_MILISEC, - n => { - let a = n * 200 / nb_req; - let a = cmp::min(a, crate::MAX_RATE_LIMIT_SLEEP_MILISEC); - cmp::max(a, crate::MIN_RATE_LIMIT_SLEEP_MILISEC) - } +fn rate_limit_cmp(a: &&config::RateLimit, b: &&config::RateLimit) -> std::cmp::Ordering { + let a_dur = parse_duration(&a.period).unwrap_or(Duration::ZERO) / u32::from(a.number); + let b_dur = parse_duration(&b.period).unwrap_or(Duration::ZERO) / u32::from(b.number); + + // A limit is "stronger" if it's period is long. The duration calculated here is the time + // per request. A shorter duration to wait, hence more requests, is *less* of a limit, so + // directly using the result of the comparison of the two calculated durations is correct. + Ord::cmp(&a_dur, &b_dur) +} + +#[derive(Debug)] +pub struct RateLimit { + limiter: RateLimiter, + resources: Vec, + path: Option, +} + +impl RateLimit { + fn matches(&self, resource: Option, path: &str) -> bool { + let resource_matches = resource + .map(|resource| self.resources.contains(&resource)) + .unwrap_or(false); + let path_matches = self + .path + .as_ref() + .map(|matcher| matcher.is_match(path)) + .unwrap_or(false); + resource_matches || path_matches + } + async fn until_ready(&self) { + self.limiter.until_ready().await + } +} + +impl TryFrom<&config::RateLimit> for RateLimit { + type Error = Error; + + fn try_from(value: &config::RateLimit) -> Result { + let period = parse_duration(&value.period)?; + let amount = value.number; + let quota = Quota::with_period(period / u32::from(amount)) + .ok_or("rate-limit period was passed as zero, which is illegal")? + .allow_burst(amount); + let limiter = RateLimiter::direct(quota); + let path = match &value.path { + Some(path) => Some(Regex::new(path).map_err(|e| e.to_string())?), + None => None, }; - Duration::from_millis(nb_mili) + Ok(Self { + limiter, + resources: value.acme_resources.clone(), + path, + }) } +} - fn request_allowed(&self) -> bool { - for (max_allowed, duration) in self.limits.iter() { - match Instant::now().checked_sub(*duration) { - Some(max_date) => { - let nb_req = self - .query_log - .iter() - .filter(move |x| **x > max_date) - .count(); - if nb_req >= *max_allowed { - return false; - } - } - None => { - return false; - } - }; - } - true +#[cfg(test)] +mod tests { + use std::{cmp::Ordering, num::NonZeroU32}; + + use crate::config; + + #[test] + fn check_ratelimit_ordering() { + let sixty_per_hour = cfg_ratelimit_helper(NonZeroU32::new(60).unwrap(), "1h".into()); + let one_per_minute = cfg_ratelimit_helper(NonZeroU32::new(1).unwrap(), "1m".into()); + let one_per_second = cfg_ratelimit_helper(NonZeroU32::new(1).unwrap(), "1s".into()); + assert_eq!( + super::rate_limit_cmp(&&sixty_per_hour, &&one_per_minute), + Ordering::Equal + ); + assert_eq!( + super::rate_limit_cmp(&&one_per_second, &&one_per_minute), + Ordering::Less + ); + assert_eq!( + super::rate_limit_cmp(&&sixty_per_hour, &&one_per_second), + Ordering::Greater + ); } - fn prune_log(&mut self) { - if let Some((_, max_limit)) = self.limits.first() { - if let Some(prune_date) = Instant::now().checked_sub(*max_limit) { - self.query_log.retain(move |&d| d > prune_date); - } + fn cfg_ratelimit_helper(number: NonZeroU32, period: String) -> config::RateLimit { + config::RateLimit { + name: String::new(), + number, + period, + acme_resources: vec![], + path: None, } } } diff --git a/acmed/src/http.rs b/acmed/src/http.rs index 8ac6d8a..b6cf8d6 100644 --- a/acmed/src/http.rs +++ b/acmed/src/http.rs @@ -1,4 +1,5 @@ use crate::acme_proto::structs::{AcmeError, HttpApiError}; +use crate::config::NamedAcmeResource; use crate::endpoint::Endpoint; #[cfg(feature = "crypto_openssl")] use acme_common::error::Error; @@ -107,9 +108,8 @@ fn is_nonce(data: &str) -> bool { } async fn new_nonce(endpoint: &mut Endpoint) -> Result<(), HttpError> { - rate_limit(endpoint).await; let url = endpoint.dir.new_nonce.clone(); - let _ = get(endpoint, &url).await?; + let _ = get(endpoint, &url, Some(NamedAcmeResource::NewNonce)).await?; Ok(()) } @@ -134,8 +134,8 @@ fn check_status(response: &Response) -> Result<(), Error> { Ok(()) } -async fn rate_limit(endpoint: &mut Endpoint) { - endpoint.rl.block_until_allowed().await; +async fn rate_limit(endpoint: &mut Endpoint, resource: Option, path: &str) { + endpoint.rl.block_until_allowed(resource, path).await; } fn header_to_string(header_value: &HeaderValue) -> Result { @@ -173,9 +173,13 @@ fn get_client(root_certs: &[String]) -> Result { Ok(client_builder.build()?) } -pub async fn get(endpoint: &mut Endpoint, url: &str) -> Result { +pub async fn get( + endpoint: &mut Endpoint, + url: &str, + resource: Option, +) -> Result { let client = get_client(&endpoint.root_certificates)?; - rate_limit(endpoint).await; + rate_limit(endpoint, resource, url).await; let response = client .get(url) .header(header::ACCEPT, CONTENT_TYPE_JSON) @@ -191,6 +195,7 @@ pub async fn get(endpoint: &mut Endpoint, url: &str) -> Result( endpoint: &mut Endpoint, url: &str, + resource: Option, data_builder: &F, content_type: &str, accept: &str, @@ -208,7 +213,7 @@ where request = request.header(header::CONTENT_TYPE, content_type); let nonce = &endpoint.nonce.clone().unwrap_or_default(); let body = data_builder(nonce, url)?; - rate_limit(endpoint).await; + rate_limit(endpoint, resource, url).await; log::trace!("POST request body: {body}"); let response = request.body(body).send().await?; update_nonce(endpoint, &response)?; @@ -235,6 +240,7 @@ where pub async fn post_jose( endpoint: &mut Endpoint, url: &str, + resource: Option, data_builder: &F, ) -> Result where @@ -243,6 +249,7 @@ where post( endpoint, url, + resource, data_builder, CONTENT_TYPE_JOSE, CONTENT_TYPE_JSON, diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index 9d95c69..fa61e33 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -141,12 +141,12 @@ impl MainEventLoop { Ok(MainEventLoop { certificates, accounts: accounts - .iter() - .map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned())))) + .into_iter() + .map(|(k, v)| (k, Arc::new(RwLock::new(v)))) .collect(), endpoints: endpoints - .iter() - .map(|(k, v)| (k.to_owned(), Arc::new(RwLock::new(v.to_owned())))) + .into_iter() + .map(|(k, v)| (k, Arc::new(RwLock::new(v)))) .collect(), }) } diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index bb7286f..4039c60 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -309,7 +309,7 @@ Specify the user who will own newly-created private-key files. See for more details. .It Cm random_early_renew Ar string Period of time before the usual certificate renewal, in which the certificate will renew at a random time. This is useful for when -you want to even out your certificate orders when you're dealing with very large numbers of certificates. The format is described in the +you want to even outoyour certificate orders when you're dealing with very large numbers of certificates. The format is described in the .Sx TIME PERIODS section. By default, this is disabled, or rather, the time frame is set to 0. .It Cm renew_delay Ar string @@ -391,17 +391,39 @@ and all three defines the same global option, the final value will be the one de .Pp Unix style globing is supported. .It Ic rate-limit -Array of table where each element defines a HTTPS rate limit. +Array of table where each element defines a HTTPS rate limit. For a rate-limit to apply, the request must match the limit by requesting one of the named ACME resources listed in +.Em acme_resources +or by having a path matching the regular expression in +.Em path . .Bl -tag .It Cm name Ar string The name the rate limit is registered under. Must be unique. .It Cm number Ar integer -Number of requests authorized withing the time period. +Number of requests authorized withing the time period. The amount of requests must be non-zero. .It Cm period Ar string Period of time during which a maximal number of requests is authorized. The format is described in the .Sx TIME PERIODS -section. +section. The period must be non-zero. +.It Cm acme_resources Ar array +Array of strings, containing named ACME resources that this limit applies to. Possible values are: +.Bl -dash -compact +.It +directory +.It +newNonce +.It +newAccount +.It +newOrder +.It +newAuthz +.It +revokeCert +.It +keyChange .El +.It Cm path Ar string +A regular expression matching the paths that this rate-limit should apply to. .El .Sh WRITING A HOOK When requesting a certificate from a CA using ACME, there are three steps that are hard to automatize. The first one is solving challenges in order to prove the ownership of every identifier to be included: it requires to interact with the configuration of other services, hence depends on how the infrastructure works. The second one is restarting all the services that use a given certificate, for the same reason. The last one is archiving: although several default methods can be implemented, sometimes admins wants or are required to do it in a different way.