Compare commits
4 Commits
master
...
20250502-a
Author | SHA1 | Date | |
---|---|---|---|
|
2e81a95948 | ||
|
a279625a5a | ||
|
e14508761f | ||
|
95772b981a |
@ -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
|
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
|
that the certificates and keys generated from HAProxy will need to be dumped
|
||||||
from outside HAProxy using "dump ssl cert" on the stats socket.
|
from outside HAProxy using "dump ssl cert" on the stats socket.
|
||||||
The generation is not scheduled and must be triggered using the CLI command
|
External Account Binding (EAB) is not supported.
|
||||||
"acme renew". See also "acme ps" in the management guide.
|
|
||||||
External Account Biding (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:
|
The following keywords are usable in the ACME section:
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
#define HAVE_JWS
|
#define HAVE_JWS
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if (defined(HAVE_JWS))
|
#if (defined(HAVE_JWS) && defined(HAVE_ASN1_TIME_TO_TM))
|
||||||
#define HAVE_ACME
|
#define HAVE_ACME
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ const char *x509_get_notafter(X509 *cert);
|
|||||||
#ifdef HAVE_ASN1_TIME_TO_TM
|
#ifdef HAVE_ASN1_TIME_TO_TM
|
||||||
time_t ASN1_to_time_t(ASN1_TIME *asn1_time);
|
time_t ASN1_to_time_t(ASN1_TIME *asn1_time);
|
||||||
time_t x509_get_notafter_time_t(X509 *cert);
|
time_t x509_get_notafter_time_t(X509 *cert);
|
||||||
|
time_t x509_get_notbefore_time_t(X509 *cert);
|
||||||
#endif
|
#endif
|
||||||
int curves2nid(const char *curve);
|
int curves2nid(const char *curve);
|
||||||
const char *nid2nist(int nid);
|
const char *nid2nist(int nid);
|
||||||
|
151
src/acme.c
151
src/acme.c
@ -51,6 +51,8 @@ enum acme_ret {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static EVP_PKEY *acme_EVP_PKEY_gen(int keytype, int curves, int bits, char **errmsg);
|
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 */
|
/* Return an existing acme_cfg section */
|
||||||
struct acme_cfg *get_acme_cfg(const char *name)
|
struct acme_cfg *get_acme_cfg(const char *name)
|
||||||
@ -527,6 +529,7 @@ out:
|
|||||||
static int cfg_postparser_acme()
|
static int cfg_postparser_acme()
|
||||||
{
|
{
|
||||||
struct acme_cfg *tmp_acme = acme_cfgs;
|
struct acme_cfg *tmp_acme = acme_cfgs;
|
||||||
|
struct task *task = NULL;
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
|
|
||||||
/* first check if the ID was already used */
|
/* 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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2000,6 +2015,65 @@ end:
|
|||||||
return task;
|
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
|
* Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry
|
||||||
*/
|
*/
|
||||||
@ -2104,51 +2178,32 @@ err:
|
|||||||
return pkey;
|
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 task *task;
|
||||||
struct acme_ctx *ctx = NULL;
|
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;
|
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) {
|
if (store->acme_task != NULL) {
|
||||||
memprintf(&err, "An ACME task is already running for certificate '%s'.\n", args[2]);
|
memprintf(errmsg, "An ACME task is already running for certificate '%s'.\n", store->path);
|
||||||
goto err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store->conf.acme.id == NULL) {
|
|
||||||
memprintf(&err, "No ACME configuration defined for file '%s'.\n", args[2]);
|
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg = get_acme_cfg(store->conf.acme.id);
|
cfg = get_acme_cfg(store->conf.acme.id);
|
||||||
if (!cfg) {
|
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;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
newstore = ckchs_dup(store);
|
newstore = ckchs_dup(store);
|
||||||
if (!newstore) {
|
if (!newstore) {
|
||||||
memprintf(&err, "Out of memory.\n");
|
memprintf(errmsg, "Out of memory.\n");
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
task = task_new_anywhere();
|
task = task_new_anywhere();
|
||||||
if (!task)
|
if (!task)
|
||||||
goto err;
|
goto err;
|
||||||
@ -2160,18 +2215,17 @@ static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appct
|
|||||||
*/
|
*/
|
||||||
store->acme_task = task;
|
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);
|
ctx = calloc(1, sizeof *ctx);
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
memprintf(&err, "Out of memory.\n");
|
memprintf(errmsg, "Out of memory.\n");
|
||||||
goto err;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* set the number of remaining retries when facing an error */
|
/* set the number of remaining retries when facing an error */
|
||||||
ctx->retries = ACME_RETRY;
|
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;
|
goto err;
|
||||||
|
|
||||||
EVP_PKEY_free(newstore->data->key);
|
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);
|
ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains);
|
||||||
if (!ctx->req) {
|
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;
|
goto err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2198,12 +2252,41 @@ static int cli_acme_renew_parse(char **args, char *payload, struct appctx *appct
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
err:
|
err:
|
||||||
HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
|
|
||||||
EVP_PKEY_free(pkey);
|
EVP_PKEY_free(pkey);
|
||||||
ckch_store_free(newstore);
|
ckch_store_free(newstore);
|
||||||
acme_ctx_destroy(ctx);
|
acme_ctx_destroy(ctx);
|
||||||
memprintf(&err, "%sCan't start the ACME client.\n", err ? err : "");
|
memprintf(errmsg, "%sCan't start the ACME client.\n", *errmsg ? *errmsg : "");
|
||||||
return cli_dynerr(appctx, err);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -779,6 +779,21 @@ time_t x509_get_notafter_time_t(X509 *cert)
|
|||||||
|
|
||||||
ret = ASN1_to_time_t(asn1_time);
|
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:
|
error:
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user