Skip to content

Commit

Permalink
Allow scoping rate-limits to specific resources and paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jcgruenhage committed Apr 10, 2023
1 parent eeca233 commit 7c57cff
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 113 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions acmed/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 30 additions & 3 deletions acmed/config/letsencrypt.toml
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions acmed/src/acme_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
4 changes: 2 additions & 2 deletions acmed/src/acme_proto/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)?;
Expand Down Expand Up @@ -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
)?;
Expand Down
34 changes: 25 additions & 9 deletions acmed/src/acme_proto/http.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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(())
}
Expand All @@ -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(())
}

Expand All @@ -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"))?;
Expand All @@ -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"))?;
Expand All @@ -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)
}
Expand All @@ -98,6 +112,7 @@ where
"authorization",
endpoint,
url,
None,
data_builder,
break_fn
)
Expand All @@ -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>(
Expand All @@ -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)
}
Expand All @@ -140,6 +155,7 @@ where
let response = http::post(
endpoint,
url,
None,
data_builder,
http::CONTENT_TYPE_JOSE,
http::CONTENT_TYPE_PEM,
Expand Down
25 changes: 20 additions & 5 deletions acmed/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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()));
Expand All @@ -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)]
Expand Down
Loading

0 comments on commit 7c57cff

Please sign in to comment.