Browse Source

Allow scoping rate-limits to specific resources and paths

pull/85/head
Jan Christian Grünhage 2 years ago
parent
commit
f0df198f25
  1. 1
      CHANGELOG.md
  2. 51
      Cargo.lock
  3. 3
      acmed/Cargo.toml
  4. 33
      acmed/config/letsencrypt.toml
  5. 1
      acmed/src/acme_proto.rs
  6. 4
      acmed/src/acme_proto/account.rs
  7. 34
      acmed/src/acme_proto/http.rs
  8. 25
      acmed/src/config.rs
  9. 193
      acmed/src/endpoint.rs
  10. 21
      acmed/src/http.rs
  11. 8
      acmed/src/main_event_loop.rs
  12. 30
      man/en/acmed.toml.5

1
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

51
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.16"
@ -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"
@ -850,6 +889,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"
@ -860,6 +905,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"

3
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"

33
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

1
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)?;

4
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
)?;

34
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::<Directory>()?;
Ok(())
}
@ -30,11 +31,12 @@ pub async fn post_jose_no_response<F>(
endpoint: &mut Endpoint,
data_builder: &F,
url: &str,
resource: Option<NamedAcmeResource>,
) -> Result<(), http::HttpError>
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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<String, Error>,
{
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<String, Error>,
{
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<F>(
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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::<Authorization>()?;
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<String, Error>,
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<F>(
@ -124,7 +139,7 @@ pub async fn finalize_order<F>(
where
F: Fn(&str, &str) -> Result<String, Error>,
{
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::<Order>()?;
Ok(order)
}
@ -140,6 +155,7 @@ where
let response = http::post(
endpoint,
url,
None,
data_builder,
http::CONTENT_TYPE_JOSE,
http::CONTENT_TYPE_PEM,

25
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<RateLimit, Error> {
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<crate::endpoint::Endpoint, Error> {
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<String> = 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<NamedAcmeResource>,
pub path: Option<String>,
}
#[derive(Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum NamedAcmeResource {
Directory,
NewNonce,
NewAccount,
NewOrder,
NewAuthz,
RevokeCert,
KeyChange,
}
#[derive(Deserialize)]

193
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<String>,
pub rl: RateLimit,
pub rl: RateLimits,
pub dir: Directory,
pub root_certificates: Vec<String>,
}
@ -21,7 +28,7 @@ impl Endpoint {
name: &str,
url: &str,
tos_agreed: bool,
limits: &[(usize, String)],
limits: &[config::RateLimit],
root_certs: &[String],
) -> Result<Self, Error> {
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<Instant>,
#[derive(Debug)]
pub struct RateLimits {
limits: Vec<RateLimit>,
}
impl RateLimit {
pub fn new(raw_limits: &[(usize, String)]) -> Result<Self, Error> {
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<Self, Error> {
let limits: Result<Vec<RateLimit>, 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<NamedAcmeResource>, 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<NotKeyed, InMemoryState, DefaultClock>,
resources: Vec<NamedAcmeResource>,
path: Option<Regex>,
}
impl RateLimit {
fn matches(&self, resource: Option<NamedAcmeResource>, 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<Self, Self::Error> {
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,
}
}
}

21
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<NamedAcmeResource>, path: &str) {
endpoint.rl.block_until_allowed(resource, path).await;
}
fn header_to_string(header_value: &HeaderValue) -> Result<String, Error> {
@ -173,9 +173,13 @@ fn get_client(root_certs: &[String]) -> Result<Client, Error> {
Ok(client_builder.build()?)
}
pub async fn get(endpoint: &mut Endpoint, url: &str) -> Result<ValidHttpResponse, HttpError> {
pub async fn get(
endpoint: &mut Endpoint,
url: &str,
resource: Option<NamedAcmeResource>,
) -> Result<ValidHttpResponse, HttpError> {
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<ValidHttpResponse
pub async fn post<F>(
endpoint: &mut Endpoint,
url: &str,
resource: Option<NamedAcmeResource>,
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<F>(
endpoint: &mut Endpoint,
url: &str,
resource: Option<NamedAcmeResource>,
data_builder: &F,
) -> Result<ValidHttpResponse, HttpError>
where
@ -243,6 +249,7 @@ where
post(
endpoint,
url,
resource,
data_builder,
CONTENT_TYPE_JOSE,
CONTENT_TYPE_JSON,

8
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(),
})
}

30
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.

Loading…
Cancel
Save