Compare commits

...

4 Commits

Author SHA1 Message Date
William Lallemand
2e81a95948 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.
2025-05-02 15:58:32 +02:00
William Lallemand
a279625a5a MINOR: acme: move the acme task init in a dedicated function
acme_start_task() is a dedicated function which starts an acme task
for a specified <store> certificate.

The initialization code was move from the "acme renew" command parser to
this function, in order to be called from a scheduler.
2025-05-02 15:58:32 +02:00
William Lallemand
e14508761f BUILD: acme: need HAVE_ASN1_TIME_TO_TM
Restrict the build of the ACME feature to libraries which provide
ASN1_TIME_to_tm() function.
2025-05-02 15:58:32 +02:00
William Lallemand
95772b981a MINOR: ssl: add function to extract X509 notBefore date in time_t
Add x509_get_notbefore_time_t() which returns the notBefore date in
time_t format.
2025-05-02 14:42:28 +02:00
5 changed files with 142 additions and 38 deletions

View File

@ -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:

View File

@ -132,7 +132,7 @@
#define HAVE_JWS
#endif
#if (defined(HAVE_JWS))
#if (defined(HAVE_JWS) && defined(HAVE_ASN1_TIME_TO_TM))
#define HAVE_ACME
#endif

View File

@ -50,6 +50,7 @@ const char *x509_get_notafter(X509 *cert);
#ifdef HAVE_ASN1_TIME_TO_TM
time_t ASN1_to_time_t(ASN1_TIME *asn1_time);
time_t x509_get_notafter_time_t(X509 *cert);
time_t x509_get_notbefore_time_t(X509 *cert);
#endif
int curves2nid(const char *curve);
const char *nid2nist(int nid);

View File

@ -51,6 +51,8 @@ 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)
@ -527,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 */
@ -542,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;
}
@ -2000,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
*/
@ -2104,51 +2178,32 @@ err:
return pkey;
}
static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appctx, void *private)
/* start an ACME task */
static int acme_start_task(struct ckch_store *store, char **errmsg)
{
char *err = NULL;
struct acme_cfg *cfg;
struct task *task;
struct acme_ctx *ctx = NULL;
struct ckch_store *store = NULL, *newstore = NULL;
struct acme_cfg *cfg;
struct ckch_store *newstore = NULL;
EVP_PKEY *pkey = NULL;
if (!*args[1]) {
memprintf(&err, ": not enough parameters\n");
goto err;
}
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
return cli_err(appctx, "Can't update: operations on certificates are currently locked!\n");
if ((store = ckchs_lookup(args[2])) == NULL) {
memprintf(&err, "Can't find the certificate '%s'.\n", args[2]);
goto err;
}
if (store->acme_task != NULL) {
memprintf(&err, "An ACME task is already running for certificate '%s'.\n", args[2]);
goto err;
}
if (store->conf.acme.id == NULL) {
memprintf(&err, "No ACME configuration defined for file '%s'.\n", args[2]);
memprintf(errmsg, "An ACME task is already running for certificate '%s'.\n", store->path);
goto err;
}
cfg = get_acme_cfg(store->conf.acme.id);
if (!cfg) {
memprintf(&err, "No ACME configuration found for file '%s'.\n", args[2]);
memprintf(errmsg, "No ACME configuration found for file '%s'.\n", store->path);
goto err;
}
newstore = ckchs_dup(store);
if (!newstore) {
memprintf(&err, "Out of memory.\n");
memprintf(errmsg, "Out of memory.\n");
goto err;
}
task = task_new_anywhere();
if (!task)
goto err;
@ -2160,18 +2215,17 @@ static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appct
*/
store->acme_task = task;
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
/* XXX: following init part could be done in the task */
ctx = calloc(1, sizeof *ctx);
if (!ctx) {
memprintf(&err, "Out of memory.\n");
memprintf(errmsg, "Out of memory.\n");
goto err;
}
/* set the number of remaining retries when facing an error */
ctx->retries = ACME_RETRY;
if ((pkey = acme_EVP_PKEY_gen(cfg->key.type, cfg->key.curves, cfg->key.bits, &err)) == NULL)
if ((pkey = acme_EVP_PKEY_gen(cfg->key.type, cfg->key.curves, cfg->key.bits, errmsg)) == NULL)
goto err;
EVP_PKEY_free(newstore->data->key);
@ -2180,7 +2234,7 @@ static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appct
ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains);
if (!ctx->req) {
memprintf(&err, "%sCan't generate a CSR.\n", err ? err : "");
memprintf(errmsg, "%sCan't generate a CSR.\n", *errmsg ? *errmsg : "");
goto err;
}
@ -2198,12 +2252,41 @@ static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appct
return 0;
err:
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
EVP_PKEY_free(pkey);
ckch_store_free(newstore);
acme_ctx_destroy(ctx);
memprintf(&err, "%sCan't start the ACME client.\n", err ? err : "");
return cli_dynerr(appctx, err);
memprintf(errmsg, "%sCan't start the ACME client.\n", *errmsg ? *errmsg : "");
return 1;
}
static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appctx, void *private)
{
struct ckch_store *store = NULL;
char *errmsg = NULL;
if (!*args[1]) {
memprintf(&errmsg, ": not enough parameters\n");
goto err;
}
if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
return cli_err(appctx, "Can't update: operations on certificates are currently locked!\n");
if ((store = ckchs_lookup(args[2])) == NULL) {
memprintf(&errmsg, "Can't find the certificate '%s'.\n", args[2]);
goto err;
}
if (store->conf.acme.id == NULL) {
memprintf(&errmsg, "No ACME configuration defined for file '%s'.\n", args[2]);
goto err;
}
acme_start_task(store, &errmsg);
err:
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
return cli_dynerr(appctx, errmsg);
}

View File

@ -779,6 +779,21 @@ time_t x509_get_notafter_time_t(X509 *cert)
ret = ASN1_to_time_t(asn1_time);
error:
return ret;
}
/* return the notBefore date of a X509 certificate in a time_t format */
time_t x509_get_notbefore_time_t(X509 *cert)
{
time_t ret = -1;
ASN1_TIME *asn1_time;
if ((asn1_time = X509_getm_notBefore(cert)) == NULL)
goto error;
ret = ASN1_to_time_t(asn1_time);
error:
return ret;
}