From 2e81a959487fc21859546f0f82ecb55f01fb77b4 Mon Sep 17 00:00:00 2001 From: William Lallemand Date: Fri, 2 May 2025 14:55:08 +0200 Subject: [PATCH] MEDIUM: acme: add a basic scheduler This patch implements a very basic scheduler for the ACME tasks. The scheduler is a task which is started from the postparser function when at least one acme section was configured. The scheduler will loop over the certificates in the ckchs_tree, and for each certificate will start an ACME task if the notAfter date is past curtime + (notAfter - notBefore) / 12, or 7 days if notBefore is not available. Once the lookup over all certificates is terminated, the task will sleep and will wakeup after 12 hours. --- doc/configuration.txt | 11 +++++-- src/acme.c | 73 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 0d87151b7..520caf990 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -5933,9 +5933,14 @@ the disk is not supposed to be done after the configuration is loaded, because it could block the event loop, blocking the traffic on the same thread. Meaning that the certificates and keys generated from HAProxy will need to be dumped from outside HAProxy using "dump ssl cert" on the stats socket. -The generation is not scheduled and must be triggered using the CLI command -"acme renew". See also "acme ps" in the management guide. -External Account Biding (EAB) is not supported. +External Account Binding (EAB) is not supported. + +The ACME scheduler starts at HAProxy startup, it will loop over the +certificates and start an ACME renewal task when the notAfter task is past +curtime + (notAfter - notBefore) / 12, or 7 days if notBefore is not defined. +The scheduler will then sleep and wakeup after 12 hours. +It is possible to start manually a renewal task with "acme renew'. +See also "acme ps" in the management guide. The following keywords are usable in the ACME section: diff --git a/src/acme.c b/src/acme.c index e9c1cd5f7..e59d4017e 100644 --- a/src/acme.c +++ b/src/acme.c @@ -52,6 +52,7 @@ enum acme_ret { static EVP_PKEY *acme_EVP_PKEY_gen(int keytype, int curves, int bits, char **errmsg); static int acme_start_task(struct ckch_store *store, char **errmsg); +static struct task *acme_scheduler(struct task *task, void *context, unsigned int state); /* Return an existing acme_cfg section */ struct acme_cfg *get_acme_cfg(const char *name) @@ -528,6 +529,7 @@ out: static int cfg_postparser_acme() { struct acme_cfg *tmp_acme = acme_cfgs; + struct task *task = NULL; int ret = 0; /* first check if the ID was already used */ @@ -543,6 +545,18 @@ static int cfg_postparser_acme() } + if (acme_cfgs) { + task = task_new_anywhere(); + if (!task) { + ret++; + ha_alert("acme: couldn't start the scheduler!\n"); + } + task->nice = 0; + task->process = acme_scheduler; + + task_wakeup(task, TASK_WOKEN_INIT); + } + return ret; } @@ -2001,6 +2015,65 @@ end: return task; } +/* + * Return 1 if the certificate must be regenerated + * Check if the notAfter date will append in (validity period / 12) or 7 days per default + */ +int acme_will_expire(struct ckch_store *store) +{ + int diff = 0; + time_t notAfter = 0; + time_t notBefore = 0; + + /* compute the validity period of the leaf certificate */ + if (!store->data || !store->data->cert) + return 0; + + notAfter = x509_get_notafter_time_t(store->data->cert); + notBefore = x509_get_notbefore_time_t(store->data->cert); + + if (notAfter >= 0 && notBefore >= 0) { + diff = (notAfter - notBefore) / 12; /* validity period / 12 */ + } else { + diff = 7 * 24 * 60 * 60; /* default to 7 days */ + } + + if (date.tv_sec + diff > notAfter) + return 1; + + return 0; +} + +/* Does the scheduling of the ACME tasks + */ +struct task *acme_scheduler(struct task *task, void *context, unsigned int state) +{ + struct ebmb_node *node = NULL; + struct ckch_store *store = NULL; + + if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock)) + return task; + + node = ebmb_first(&ckchs_tree); + while (node) { + store = ebmb_entry(node, struct ckch_store, node); + + if (store->conf.acme.id) { + + if (acme_will_expire(store)) { + acme_start_task(store, NULL); + } + } + node = ebmb_next(node); + } +end: + HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock); + /* call the task again in 12h */ + /* XXX: need to be configured */ + task->expire = tick_add(now_ms, 12 * 60 * 60 * 1000); + return task; +} + /* * Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry */