From 59326edc7afd20e70b2106aa9e69cf80539e1b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Christian=20Gr=C3=BCnhage?= Date: Fri, 7 Apr 2023 00:22:36 +0200 Subject: [PATCH] Randomized early renew Let's Encrypt suggests in the integration guide, that for spacing out renewals after issueing a lot of new certificates in a batch, you renew a few of them a bit early until it's evened out. This adds a config option that allows to set a timeframe in which early random delays are attempted. --- CHANGELOG.md | 1 + Cargo.lock | 48 ++++++++++++++++++++++++++++++++++++ acmed/Cargo.toml | 1 + acmed/src/certificate.rs | 8 ++++-- acmed/src/config.rs | 30 ++++++++++++++++++++++ acmed/src/main.rs | 1 + acmed/src/main_event_loop.rs | 1 + man/en/acmed.toml.5 | 15 +++++++++++ 8 files changed, 103 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d569bb6..90eaad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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. ## [0.21.0] - 2022-12-19 diff --git a/Cargo.lock b/Cargo.lock index 049da4b..0c7e16e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,7 @@ dependencies = [ "log", "nix", "nom", + "rand", "serde", "serde_json", "tinytemplate", @@ -536,6 +537,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "glob" version = "0.3.1" @@ -915,6 +927,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.56" @@ -939,6 +957,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/acmed/Cargo.toml b/acmed/Cargo.toml index df2fe0a..a6d097f 100644 --- a/acmed/Cargo.toml +++ b/acmed/Cargo.toml @@ -35,6 +35,7 @@ serde_json = "1.0" tinytemplate = "1.2" toml = "0.7" tokio = { version = "1", features = ["full"] } +rand = "0.8.5" [target.'cfg(unix)'.dependencies] nix = "0.26" diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index 28e245d..89f4410 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -6,6 +6,7 @@ use crate::storage::{certificate_files_exists, get_certificate, FileManager}; use acme_common::crypto::{HashFunction, KeyType, SubjectAttribute, X509Certificate}; use acme_common::error::Error; use log::{debug, info, trace, warn}; +use rand::{thread_rng, Rng}; use std::collections::{HashMap, HashSet}; use std::fmt; use std::time::Duration; @@ -22,6 +23,7 @@ pub struct Certificate { pub hooks: Vec, pub crt_name: String, pub env: HashMap, + pub random_early_renew: Duration, pub renew_delay: Duration, pub file_manager: FileManager, } @@ -77,7 +79,9 @@ impl Certificate { expires_in.as_secs() / 86400, self.renew_delay.as_secs() / 86400, )); - Ok(expires_in.saturating_sub(self.renew_delay)) + Ok(expires_in + .saturating_sub(self.renew_delay) + .saturating_sub(thread_rng().gen_range(Duration::ZERO..self.random_early_renew))) } fn has_missing_identifiers(&self, cert: &X509Certificate) -> bool { @@ -125,7 +129,7 @@ impl Certificate { self.debug("the current certificate doesn't include all the required identifiers"); return Ok(Duration::ZERO); } - Ok(self.renew_in(&cert)?) + self.renew_in(&cert) } pub async fn call_challenge_hooks( diff --git a/acmed/src/config.rs b/acmed/src/config.rs index a1aa52f..032654d 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -186,11 +186,19 @@ pub struct GlobalOptions { pub pk_file_group: Option, pub pk_file_mode: Option, pub pk_file_user: Option, + pub random_early_renew: Option, pub renew_delay: Option, pub root_certificates: Option>, } impl GlobalOptions { + pub fn get_random_early_renew(&self) -> Result { + match &self.random_early_renew { + Some(d) => parse_duration(d), + None => Ok(Duration::new(crate::DEFAULT_CERT_RANDOM_EARLY_RENEW, 0)), + } + } + pub fn get_renew_delay(&self) -> Result { match &self.renew_delay { Some(d) => parse_duration(d), @@ -211,6 +219,7 @@ impl GlobalOptions { pub struct Endpoint { pub file_name_format: Option, pub name: String, + pub random_early_renew: Option, #[serde(default)] pub rate_limits: Vec, pub renew_delay: Option, @@ -220,6 +229,16 @@ pub struct Endpoint { } impl Endpoint { + pub fn get_random_early_renew(&self, cnf: &Config) -> Result { + match &self.random_early_renew { + Some(d) => parse_duration(d), + None => match &cnf.global { + Some(g) => g.get_random_early_renew(), + None => Ok(Duration::new(crate::DEFAULT_CERT_RANDOM_EARLY_RENEW, 0)), + }, + } + } + pub fn get_renew_delay(&self, cnf: &Config) -> Result { match &self.renew_delay { Some(d) => parse_duration(d), @@ -437,6 +456,7 @@ pub struct Certificate { pub key_type: Option, pub kp_reuse: Option, pub name: Option, + pub random_early_renew: Option, pub renew_delay: Option, #[serde(default)] pub subject_attributes: SubjectAttributes, @@ -538,6 +558,16 @@ impl Certificate { Ok(res) } + pub fn get_random_early_renew(&self, cnf: &Config) -> Result { + match &self.random_early_renew { + Some(d) => parse_duration(d), + None => { + let endpoint = self.do_get_endpoint(cnf)?; + endpoint.get_random_early_renew(cnf) + } + } + } + pub fn get_renew_delay(&self, cnf: &Config) -> Result { match &self.renew_delay { Some(d) => parse_duration(d), diff --git a/acmed/src/main.rs b/acmed/src/main.rs index 3cbe93e..8cc94c7 100644 --- a/acmed/src/main.rs +++ b/acmed/src/main.rs @@ -37,6 +37,7 @@ pub const DEFAULT_POOL_TIME: u64 = 5000; pub const DEFAULT_CSR_DIGEST: HashFunction = HashFunction::Sha256; pub const DEFAULT_CERT_KEY_TYPE: KeyType = KeyType::Rsa2048; pub const DEFAULT_CERT_FILE_MODE: u32 = 0o644; +pub const DEFAULT_CERT_RANDOM_EARLY_RENEW: u64 = 0; // default to not renewing early pub const DEFAULT_CERT_RENEW_DELAY: u64 = 1_814_400; // 1_814_400 is 3 weeks (3 * 7 * 24 * 60 * 60) pub const DEFAULT_PK_FILE_MODE: u32 = 0o600; pub const DEFAULT_ACCOUNT_FILE_MODE: u32 = 0o600; diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index c3c2c4d..9d95c69 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -116,6 +116,7 @@ impl MainEventLoop { .collect(), crt_name, env: crt.env.to_owned(), + random_early_renew: crt.get_random_early_renew(&cnf)?, renew_delay: crt.get_renew_delay(&cnf)?, file_manager: fm, }; diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 81d6c41..20edcc7 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -206,6 +206,11 @@ Name of the certificate. Must be unique unless the key type is different. Will b and .Sq / characters will be replaced by an underscore. Default is the first identifier. +.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 +.Sx TIME PERIODS +section. Default is the value defined in the associated endpoint. .It Cm renew_delay Ar string Period of time between the certificate renewal and its expiration date. The format is described in the .Sx TIME PERIODS @@ -246,6 +251,11 @@ element. The name the endpoint is registered under. Must be unique. .It Cm rate_limits Ar array Array containing the names of the HTTPS rate limits to apply. +.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 +.Sx TIME PERIODS +section. Default is the value defined in the global section. .It Cm renew_delay Ar string Period of time between the certificate renewal and its expiration date. The format is described in the .Sx TIME PERIODS @@ -297,6 +307,11 @@ for more details. Specify the user who will own newly-created private-key files. See .Xr chown 2 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 +.Sx TIME PERIODS +section. By default, this is disabled, or rather, the time frame is set to 0. .It Cm renew_delay Ar string Period of time between the certificate renewal and its expiration date. The format is described in the .Sx TIME PERIODS