diff options
author | Stefan Eissing <icing@apache.org> | 2022-05-13 11:03:51 +0000 |
---|---|---|
committer | Stefan Eissing <icing@apache.org> | 2022-05-13 11:03:51 +0000 |
commit | e6e83f275f4f7e66023a0dec83dfe2ca147bb536 (patch) | |
tree | 06894ac4fa8570dc4c845bcbb99fd9783c1c3e6f | |
parent | cda87408aeafb96af7f7972ff9157a9e656e9b7f (diff) | |
download | httpd-e6e83f275f4f7e66023a0dec83dfe2ca147bb536.tar.gz |
*) mod_md: the `MDCertificateAuthority` directive can take more than one URL/name of
an ACME CA. This gives a failover for renewals when several consecutive attempts
to get a certificate failed.
A new directive was added: `MDRetryDelay` sets the delay of retries.
A new directive was added: `MDRetryFailover` sets the number of errored
attempts before an alternate CA is selected for certificate renewals.
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1900852 13f79535-47bb-0310-9956-ffa450edef68
28 files changed, 499 insertions, 144 deletions
diff --git a/changes-entries/md_acme_failover.txt b/changes-entries/md_acme_failover.txt new file mode 100644 index 0000000000..bb1999c9a9 --- /dev/null +++ b/changes-entries/md_acme_failover.txt @@ -0,0 +1,7 @@ + *) mod_md: the `MDCertificateAuthority` directive can take more than one URL/name of + an ACME CA. This gives a failover for renewals when several consecutive attempts + to get a certificate failed. + A new directive was added: `MDRetryDelay` sets the delay of retries. + A new directive was added: `MDRetryFailover` sets the number of errored + attempts before an alternate CA is selected for certificate renewals. + [Stefan Eissing] diff --git a/docs/manual/mod/mod_md.xml b/docs/manual/mod/mod_md.xml index 800abbaa2f..18e2554154 100644 --- a/docs/manual/mod/mod_md.xml +++ b/docs/manual/mod/mod_md.xml @@ -471,27 +471,34 @@ MDomain example2.org auto <directivesynopsis> <name>MDCertificateAuthority</name> - <description>The URL of the ACME Certificate Authority service.</description> + <description>The URL(s) of the ACME Certificate Authority to use.</description> <syntax>MDCertificateAuthority <var>url</var></syntax> - <default>MDCertificateAuthority https://acme-v02.api.letsencrypt.org/directory</default> + <default>MDCertificateAuthority letsencrypt</default> <contextlist> <context>server config</context> </contextlist> <usage> <p> - The URL where the CA offers its service. + The URL(s) where the CA offers its service. + Instead of the actual URL, you may use 'letsencrypt' or 'buypass'. </p><p> - Let's Encrypt offers, right now, four such URLs. Two for - the own legacy version of the ACME protocol, commonly named ACMEv1. - And two for the RFC 8555 version, named ACMEv2. + If you configure more than one URL, each one is tried in a round-robin + fashion after a number of failures. You can configure how quickly or + delayed that happens via the <directive>MDRetryDelay</directive> and + <directive>MDRetryFailover</directive> directives. The default setting + makes a failover after about half a day of trying. </p><p> - Each version has 2 endpoints, as their is a production endpoint and a - "staging" endpoint for testing. The testing endpoint works the same, but will - not give you certificates recognized by browsers. However, it also has - very relaxed rate limits. This allows testing of the service repeatedly - without you blocking yourself. + All other settings apply to each of these URLs. It is therefore + not possible to have two with different + <directive>MDExternalAccountBinding</directive>s, for example. + </p><p> + For testing, CAs commonly offer a second service URL. + The 'test' service does not give certificates valid in a browser, + but are more relaxed in regard to rate limits. + This allows for verfication of your own setup before switching + to the production service URL. </p> - <example><title>LE Staging Setup</title> + <example><title>LE Test Setup</title> <highlight language="config"> MDCertificateAuthority https://acme-staging-v02.api.letsencrypt.org/directory </highlight> @@ -1376,4 +1383,45 @@ MDMessageCmd /etc/apache/md-message </usage> </directivesynopsis> + <directivesynopsis> + <name>MDRetryDelay</name> + <description></description> + <syntax>MDRetryDelay <var>duration</var></syntax> + <default>MDRetryDelay 5s</default> + <contextlist> + <context>server config</context> + </contextlist> + <compatibility>Available in version 2.4.54 and later</compatibility> + <usage> + <p> + The amount of time to wait after an error before trying + to renew a certificate again. This duration is doubled after + each consecutive error with a maximum of 24 hours. + </p> + <p> + It is kept separate for each certificate renewal. Meaning an error + on one MDomain does not delay the renewals of other domains. + </p> + </usage> + </directivesynopsis> + + <directivesynopsis> + <name>MDRetryFailover</name> + <description></description> + <syntax>MDRetryFailover <var>number</var></syntax> + <default>MDRetryFailover 13</default> + <contextlist> + <context>server config</context> + </contextlist> + <compatibility>Available in version 2.4.54 and later</compatibility> + <usage> + <p> + The number of consecutive errors on renewing a certificate before + another CA is selected. This only applies to configurations that + have more than one <directive>MDCertificateAuthority</directive> + specified. + </p> + </usage> + </directivesynopsis> + </modulesynopsis> diff --git a/modules/md/md.h b/modules/md/md.h index 5a3aaecdac..af695f1458 100644 --- a/modules/md/md.h +++ b/modules/md/md.h @@ -87,8 +87,9 @@ struct md_t { md_timeslice_t *renew_window; /* time before expiration that starts renewal */ md_timeslice_t *warn_window; /* time before expiration that warnings are sent out */ - const char *ca_url; /* url of CA certificate service */ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ + struct apr_array_header_t *ca_urls; /* urls of CAs */ + const char *ca_effective; /* url of CA used */ const char *ca_account; /* account used at CA */ const char *ca_agreement; /* accepted agreement uri between CA and user */ struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */ @@ -203,6 +204,7 @@ struct md_t { #define MD_KEY_UNKNOWN "unknown" #define MD_KEY_UNTIL "until" #define MD_KEY_URL "url" +#define MD_KEY_URLS "urls" #define MD_KEY_URI "uri" #define MD_KEY_VALID "valid" #define MD_KEY_VALID_FROM "valid-from" diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c index 94dd83190d..f3e043e87c 100644 --- a/modules/md/md_acme_acct.c +++ b/modules/md/md_acme_acct.c @@ -243,7 +243,7 @@ int md_acme_acct_matches_url(md_acme_acct_t *acct, const char *url) int md_acme_acct_matches_md(md_acme_acct_t *acct, const md_t *md) { - if (!md_acme_acct_matches_url(acct, md->ca_url)) return 0; + if (!md_acme_acct_matches_url(acct, md->ca_effective)) return 0; /* if eab values are not mentioned, we match an account regardless * if it was registered with eab or not */ if (!md->ca_eab_kid || !md->ca_eab_hmac) { @@ -285,7 +285,7 @@ static int find_acct(void *baton, const char *name, const char *aspect, && (!ctx->md || md_acme_acct_matches_md(acct, ctx->md))) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, "found account %s for %s: %s, status=%d", - acct->id, ctx->md->ca_url, aspect, acct->status); + acct->id, ctx->md->ca_effective, aspect, acct->status); ctx->id = apr_pstrdup(ctx->p, name); return 0; } diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c index bc0f17f271..abe7d644e6 100644 --- a/modules/md/md_acme_drive.c +++ b/modules/md/md_acme_drive.c @@ -45,7 +45,7 @@ /**************************************************************************************************/ /* account setup */ -static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store, +static apr_status_t use_staged_acct(md_acme_t *acme, struct md_store_t *store, const md_t *md, apr_pool_t *p) { md_acme_acct_t *acct; @@ -654,13 +654,15 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) apr_status_t rv = APR_SUCCESS; apr_time_t now, t, t2; md_credentials_t *cred; + const char *ca_effective = NULL; char ts[APR_RFC822_DATE_LEN]; int i, first = 0; - - if (md_log_is_level(d->p, MD_LOG_DEBUG)) { - md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " - "state=%d, challenges='%s'", d->md->name, d->md->state, - apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + + if (!d->md->ca_urls || d->md->ca_urls->nelts <= 0) { + /* No CA defined? This is checked in several other places, but lets be sure */ + md_result_printf(result, APR_INCOMPLETE, + "The managed domain %s is missing MDCertificateAuthority", d->md->name); + goto out; } /* When not explicitly told to reset, we check the existing data. If @@ -679,13 +681,46 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) rv = APR_SUCCESS; } } - + + /* What CA are we using this time? */ + if (ad->md && ad->md->ca_effective) { + /* There was one chosen on the previous run. Do we stick to it? */ + ca_effective = ad->md->ca_effective; + if (d->md->ca_urls->nelts > 1 && d->attempt >= d->retry_failover) { + /* We have more than one CA to choose from and this is the (at least) + * third attempt with the same CA. Let's switch to the next one. */ + int last_idx = md_array_str_index(d->md->ca_urls, ca_effective, 0, 1); + if (last_idx >= 0) { + int next_idx = (last_idx+1) % d->md->ca_urls->nelts; + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, next_idx, const char*); + } + else { + /* not part of current configuration? */ + ca_effective = NULL; + } + /* switching CA means we need to wipe the staging area */ + reset_staging = 1; + } + } + + if (!ca_effective) { + /* None chosen yet, pick the first one configured */ + ca_effective = APR_ARRAY_IDX(d->md->ca_urls, 0, const char*); + } + + if (md_log_is_level(d->p, MD_LOG_DEBUG)) { + md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, " + "state=%d, attempt=%d, acme=%s, challenges='%s'", + d->md->name, d->md->state, d->attempt, ca_effective, + apr_array_pstrcat(d->p, ad->ca_challenges, ' ')); + } + if (reset_staging) { md_result_activity_setn(result, "Resetting staging area"); /* reset the staging area for this domain */ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, - "%s: reset staging area, will", d->md->name); + "%s: reset staging area", d->md->name); if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) { md_result_printf(result, rv, "resetting staging area"); goto out; @@ -709,24 +744,14 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) } /* Need to renew */ - md_result_activity_printf(result, "Contacting ACME server for %s at %s", - d->md->name, d->md->ca_url); - if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, d->md->ca_url, d->proxy_url, d->ca_file))) { - md_result_printf(result, rv, "setup ACME communications"); - md_result_log(result, MD_LOG_ERR); - goto out; - } - if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) { - md_result_log(result, MD_LOG_ERR); - goto out; - } - - if (!ad->md || strcmp(ad->md->ca_url, d->md->ca_url)) { + if (!ad->md || !md_array_str_eq(ad->md->ca_urls, d->md->ca_urls, 1)) { md_result_activity_printf(result, "Resetting staging for %s", d->md->name); /* re-initialize staging */ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name); ad->md = md_copy(d->p, d->md); + ad->md->ca_effective = ca_effective; + ad->md->ca_account = NULL; ad->order = NULL; rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0); if (APR_SUCCESS != rv) { @@ -739,6 +764,19 @@ static apr_status_t acme_renew(md_proto_driver_t *d, md_result_t *result) ad->domains = md_dns_make_minimal(d->p, ad->md->domains); } + md_result_activity_printf(result, "Contacting ACME server for %s at %s", + d->md->name, ca_effective); + if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, ca_effective, + d->proxy_url, d->ca_file))) { + md_result_printf(result, rv, "setup ACME communications"); + md_result_log(result, MD_LOG_ERR); + goto out; + } + if (APR_SUCCESS != (rv = md_acme_setup(ad->acme, result))) { + md_result_log(result, MD_LOG_ERR); + goto out; + } + if (APR_SUCCESS != load_missing_creds(d)) { for (i = 0; i < ad->creds->nelts; ++i) { ad->cred = APR_ARRAY_IDX(ad->creds, i, md_credentials_t*); @@ -922,7 +960,12 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro md_result_set(result, rv, "loading staged md.json"); goto leave; } - + if (!md->ca_effective) { + rv = APR_ENOENT; + md_result_set(result, rv, "effective CA url not set"); + goto leave; + } + all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*)); for (i = 0; i < md_pkeys_spec_count(md->pks); ++i) { pkspec = md_pkeys_spec_get(md->pks, i); @@ -985,7 +1028,8 @@ static apr_status_t acme_preload(md_proto_driver_t *d, md_store_group_t load_gro } } - if (APR_SUCCESS != (rv = md_acme_create(&acme, d->p, md->ca_url, d->proxy_url, d->ca_file))) { + if (APR_SUCCESS != (rv = md_acme_create(&acme, d->p, md->ca_effective, + d->proxy_url, d->ca_file))) { md_result_set(result, rv, "error setting up acme"); goto leave; } @@ -1039,8 +1083,9 @@ static apr_status_t acme_driver_preload(md_proto_driver_t *d, static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p) { (void)p; - if (!md->ca_url) { - md->ca_url = MD_ACME_DEF_URL; + if (!md->ca_urls || apr_is_empty_array(md->ca_urls)) { + md->ca_urls = apr_array_make(p, 3, sizeof(const char *)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_ACME_DEF_URL; } return APR_SUCCESS; } diff --git a/modules/md/md_core.c b/modules/md/md_core.c index f82f950503..8c7c453625 100644 --- a/modules/md/md_core.c +++ b/modules/md/md_core.c @@ -241,8 +241,11 @@ md_t *md_clone(apr_pool_t *p, const md_t *src) md->renew_window = src->renew_window; md->warn_window = src->warn_window; md->contacts = md_array_str_clone(p, src->contacts); - if (src->ca_url) md->ca_url = apr_pstrdup(p, src->ca_url); if (src->ca_proto) md->ca_proto = apr_pstrdup(p, src->ca_proto); + if (src->ca_urls) { + md->ca_urls = md_array_str_clone(p, src->ca_urls); + } + if (src->ca_effective) md->ca_effective = apr_pstrdup(p, src->ca_effective); if (src->ca_account) md->ca_account = apr_pstrdup(p, src->ca_account); if (src->ca_agreement) md->ca_agreement = apr_pstrdup(p, src->ca_agreement); if (src->defn_name) md->defn_name = apr_pstrdup(p, src->defn_name); @@ -272,7 +275,10 @@ md_json_t *md_to_json(const md_t *md, apr_pool_t *p) md_json_setl(md->transitive, json, MD_KEY_TRANSITIVE, NULL); md_json_sets(md->ca_account, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL); md_json_sets(md->ca_proto, json, MD_KEY_CA, MD_KEY_PROTO, NULL); - md_json_sets(md->ca_url, json, MD_KEY_CA, MD_KEY_URL, NULL); + md_json_sets(md->ca_effective, json, MD_KEY_CA, MD_KEY_URL, NULL); + if (md->ca_urls && !apr_is_empty_array(md->ca_urls)) { + md_json_setsa(md->ca_urls, json, MD_KEY_CA, MD_KEY_URLS, NULL); + } md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); if (!md_pkeys_spec_is_empty(md->pks)) { md_json_setj(md_pkeys_spec_to_json(md->pks, p), json, MD_KEY_PKEY, NULL); @@ -324,7 +330,16 @@ md_t *md_from_json(md_json_t *json, apr_pool_t *p) md_json_dupsa(md->contacts, p, json, MD_KEY_CONTACTS, NULL); md->ca_account = md_json_dups(p, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL); md->ca_proto = md_json_dups(p, json, MD_KEY_CA, MD_KEY_PROTO, NULL); - md->ca_url = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL); + md->ca_effective = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL); + if (md_json_has_key(json, MD_KEY_CA, MD_KEY_URLS, NULL)) { + md->ca_urls = apr_array_make(p, 5, sizeof(const char*)); + md_json_dupsa(md->ca_urls, p, json, MD_KEY_CA, MD_KEY_URLS, NULL); + } + else if (md->ca_effective) { + /* compat for old format where we had only a single url */ + md->ca_urls = apr_array_make(p, 5, sizeof(const char*)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = md->ca_effective; + } md->ca_agreement = md_json_dups(p, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL); if (md_json_has_key(json, MD_KEY_PKEY, NULL)) { md->pks = md_pkeys_spec_from_json(md_json_getj(json, MD_KEY_PKEY, NULL), p); diff --git a/modules/md/md_ocsp.c b/modules/md/md_ocsp.c index 67c6e12d80..8cbf05b3e1 100644 --- a/modules/md/md_ocsp.c +++ b/modules/md/md_ocsp.c @@ -65,6 +65,7 @@ struct md_ocsp_reg_t { md_timeslice_t renew_window; md_job_notify_cb *notify; void *notify_ctx; + apr_time_t min_delay; }; typedef struct md_ocsp_status_t md_ocsp_status_t; @@ -279,7 +280,8 @@ static apr_status_t ocsp_reg_cleanup(void *data) apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, md_store_t *store, const md_timeslice_t *renew_window, - const char *user_agent, const char *proxy_url) + const char *user_agent, const char *proxy_url, + apr_time_t min_delay) { md_ocsp_reg_t *reg; apr_status_t rv = APR_SUCCESS; @@ -296,6 +298,7 @@ apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, md_store_t *s reg->id_by_external_id = apr_hash_make(p); reg->ostat_by_id = apr_hash_make(p); reg->renew_window = *renew_window; + reg->min_delay = min_delay; rv = apr_thread_mutex_create(®->mutex, APR_THREAD_MUTEX_NESTED, p); if (APR_SUCCESS != rv) goto cleanup; @@ -1056,5 +1059,5 @@ void md_ocsp_get_status_all(md_json_t **pjson, md_ocsp_reg_t *reg, apr_pool_t *p md_job_t *md_ocsp_job_make(md_ocsp_reg_t *ocsp, const char *mdomain, apr_pool_t *p) { - return md_job_make(p, ocsp->store, MD_SG_OCSP, mdomain); + return md_job_make(p, ocsp->store, MD_SG_OCSP, mdomain, ocsp->min_delay); } diff --git a/modules/md/md_ocsp.h b/modules/md/md_ocsp.h index d6ee0f1d7d..c91dc54906 100644 --- a/modules/md/md_ocsp.h +++ b/modules/md/md_ocsp.h @@ -38,7 +38,8 @@ typedef struct md_ocsp_reg_t md_ocsp_reg_t; apr_status_t md_ocsp_reg_make(md_ocsp_reg_t **preg, apr_pool_t *p, struct md_store_t *store, const md_timeslice_t *renew_window, - const char *user_agent, const char *proxy_url); + const char *user_agent, const char *proxy_url, + apr_time_t min_delay); apr_status_t md_ocsp_init_id(struct md_data_t *id, apr_pool_t *p, const md_cert_t *cert); diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c index 0c59aeb737..21374fc1af 100644 --- a/modules/md/md_reg.c +++ b/modules/md/md_reg.c @@ -53,6 +53,8 @@ struct md_reg_t { md_timeslice_t *warn_window; md_job_notify_cb *notify; void *notify_ctx; + apr_time_t min_delay; + int retry_failover; }; /**************************************************************************************************/ @@ -80,7 +82,8 @@ static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p) } apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store, - const char *proxy_url, const char *ca_file) + const char *proxy_url, const char *ca_file, + apr_time_t min_delay, int retry_failover) { md_reg_t *reg; apr_status_t rv; @@ -95,6 +98,8 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL; reg->ca_file = (ca_file && apr_strnatcasecmp("none", ca_file))? apr_pstrdup(p, ca_file) : NULL; + reg->min_delay = min_delay; + reg->retry_failover = retry_failover; md_timeslice_create(®->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); md_timeslice_create(®->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF); @@ -165,12 +170,17 @@ static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, i } } - if ((MD_UPD_CA_URL & fields) && md->ca_url) { /* setting to empty is ok */ - rv = md_util_abs_uri_check(p, md->ca_url, &err); - if (err) { - md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, - "CA url for %s invalid (%s): %s", md->name, err, md->ca_url); - return APR_EINVAL; + if ((MD_UPD_CA_URL & fields) && md->ca_urls) { /* setting to empty is ok */ + int i; + const char *url; + for (i = 0; i < md->ca_urls->nelts; ++i) { + url = APR_ARRAY_IDX(md->ca_urls, i, const char*); + rv = md_util_abs_uri_check(p, url, &err); + if (err) { + md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, + "CA url for %s invalid (%s): %s", md->name, err, url); + return APR_EINVAL; + } } } @@ -451,7 +461,8 @@ static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, v md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update domains: %s", name); } if (MD_UPD_CA_URL & fields) { - nmd->ca_url = updates->ca_url; + nmd->ca_urls = (updates->ca_urls? + apr_array_copy(p, updates->ca_urls) : NULL); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca url: %s", name); } if (MD_UPD_CA_PROTO & fields) { @@ -934,13 +945,16 @@ apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0); } } + if (!md->ca_effective && old->ca_effective) { + md->ca_effective = apr_pstrdup(p, old->ca_effective); + } if (!md->ca_account && old->ca_account) { md->ca_account = apr_pstrdup(p, old->ca_account); } /* if everything remains the same, spare the write back */ if (!MD_VAL_UPDATE(md, old, state) - && !MD_SVAL_UPDATE(md, old, ca_url) + && md_array_str_eq(md->ca_urls, old->ca_urls, 0) && !MD_SVAL_UPDATE(md, old, ca_proto) && !MD_SVAL_UPDATE(md, old, ca_agreement) && !MD_VAL_UPDATE(md, old, transitive) @@ -1118,8 +1132,9 @@ apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t static apr_status_t run_renew(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) { + md_reg_t *reg = baton; const md_t *md; - int reset; + int reset, attempt; md_proto_driver_t *driver; apr_table_t *env; apr_status_t rv; @@ -1129,12 +1144,15 @@ static apr_status_t run_renew(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_ md = va_arg(ap, const md_t *); env = va_arg(ap, apr_table_t *); reset = va_arg(ap, int); - result = va_arg(ap, md_result_t *); + attempt = va_arg(ap, int); + result = va_arg(ap, md_result_t *); - rv = run_init(baton, ptemp, &driver, md, 0, env, result, NULL); + rv = run_init(reg, ptemp, &driver, md, 0, env, result, NULL); if (APR_SUCCESS == rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name); driver->reset = reset; + driver->attempt = attempt; + driver->retry_failover = reg->retry_failover; rv = driver->proto->renew(driver, result); } md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: staging done", md->name); @@ -1142,9 +1160,10 @@ static apr_status_t run_renew(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_ } apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, apr_table_t *env, - int reset, md_result_t *result, apr_pool_t *p) + int reset, int attempt, + md_result_t *result, apr_pool_t *p) { - return md_util_pool_vdo(run_renew, reg, p, md, env, reset, result, NULL); + return md_util_pool_vdo(run_renew, reg, p, md, env, reset, attempt, result, NULL); } static apr_status_t run_load_staging(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap) @@ -1249,5 +1268,5 @@ void md_reg_set_warn_window_default(md_reg_t *reg, md_timeslice_t *warn_window) md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p) { - return md_job_make(p, reg->store, MD_SG_STAGING, mdomain); + return md_job_make(p, reg->store, MD_SG_STAGING, mdomain, reg->min_delay); } diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h index aa626c9276..ccaf10253a 100644 --- a/modules/md/md_reg.h +++ b/modules/md/md_reg.h @@ -36,7 +36,8 @@ typedef struct md_reg_t md_reg_t; * Create the MD registry, using the pool and store. */ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *pm, md_store_t *store, - const char *proxy_url, const char *ca_file); + const char *proxy_url, const char *ca_file, + apr_time_t min_delay, int retry_failover); md_store_t *md_reg_store_get(md_reg_t *reg); @@ -212,6 +213,8 @@ struct md_proto_driver_t { int can_http; int can_https; int reset; + int attempt; + int retry_failover; apr_interval_time_t activation_delay; }; @@ -242,11 +245,17 @@ apr_status_t md_reg_test_init(md_reg_t *reg, const md_t *md, struct apr_table_t /** * Obtain new credentials for the given managed domain in STAGING. - * + * @param reg the registry instance + * @param md the mdomain to renew + * @param env global environment of settings + * @param reset != 0 if any previous, partial information should be wiped + * @param attempt the number of attempts made this far (for this md) + * @param result for reporting results of the renewal + * @param p the memory pool to use * @return APR_SUCCESS if new credentials have been staged successfully */ apr_status_t md_reg_renew(md_reg_t *reg, const md_t *md, - struct apr_table_t *env, int reset, + struct apr_table_t *env, int reset, int attempt, struct md_result_t *result, apr_pool_t *p); /** diff --git a/modules/md/md_status.c b/modules/md/md_status.c index 32efc19a67..936c65349f 100644 --- a/modules/md/md_status.c +++ b/modules/md/md_status.c @@ -286,7 +286,8 @@ apr_status_t md_status_get_json(md_json_t **pjson, apr_array_header_t *mds, /* drive job persistence */ md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, - md_store_group_t group, const char *name) + md_store_group_t group, const char *name, + apr_time_t min_delay) { md_job_t *job = apr_pcalloc(p, sizeof(*job)); job->group = group; @@ -294,6 +295,7 @@ md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, job->store = store; job->p = p; job->max_log = 128; + job->min_delay = min_delay; return job; } @@ -588,7 +590,7 @@ apr_time_t md_job_delay_on_errors(md_job_t *job, int err_count, const char *last } else if (err_count > 0) { /* back off duration, depending on the errors we encounter in a row */ - delay = apr_time_from_sec(5 << (err_count - 1)); + delay = job->min_delay << (err_count - 1); if (delay > max_delay) { delay = max_delay; } diff --git a/modules/md/md_status.h b/modules/md/md_status.h index cd358b0e8e..f4d09bd90f 100644 --- a/modules/md/md_status.h +++ b/modules/md/md_status.h @@ -68,6 +68,7 @@ struct md_job_t { apr_size_t max_log; /* max number of log entries, new ones replace oldest */ int dirty; struct md_result_t *observing; + apr_time_t min_delay; /* smallest delay a repeated attempt should have */ }; /** @@ -75,7 +76,8 @@ struct md_job_t { * Job load/save will work using the name. */ md_job_t *md_job_make(apr_pool_t *p, md_store_t *store, - md_store_group_t group, const char *name); + md_store_group_t group, const char *name, + apr_time_t min_delay); void md_job_set_group(md_job_t *job, md_store_group_t group); diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c index dd3b1458ad..c8d2bad64c 100644 --- a/modules/md/md_tailscale.c +++ b/modules/md/md_tailscale.c @@ -56,7 +56,8 @@ static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result) ts_ctx->driver = d; ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *)); - ca_url = d->md->ca_url; + ca_url = (d->md->ca_urls && !apr_is_empty_array(d->md->ca_urls))? + APR_ARRAY_IDX(d->md->ca_urls, 0, const char*) : NULL; if (!ca_url) { ca_url = MD_TAILSCALE_DEF_URL; } @@ -254,7 +255,7 @@ static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result) ts_ctx->md = NULL; } - if (!ts_ctx->md || strcmp(ts_ctx->md->ca_url, d->md->ca_url)) { + if (!ts_ctx->md || !md_array_str_eq(ts_ctx->md->ca_urls, d->md->ca_urls, 1)) { md_result_activity_printf(result, "Resetting staging for %s", d->md->name); /* re-initialize staging */ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name); @@ -361,8 +362,9 @@ leave: static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p) { (void)p; - if (!md->ca_url) { - md->ca_url = MD_TAILSCALE_DEF_URL; + if (!md->ca_urls) { + md->ca_urls = apr_array_make(p, 3, sizeof(const char *)); + APR_ARRAY_PUSH(md->ca_urls, const char*) = MD_TAILSCALE_DEF_URL; } return APR_SUCCESS; } diff --git a/modules/md/md_version.h b/modules/md/md_version.h index d634538e1a..4b8aef13d6 100644 --- a/modules/md/md_version.h +++ b/modules/md/md_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the md module as c string */ -#define MOD_MD_VERSION "2.4.15" +#define MOD_MD_VERSION "2.4.16" /** * @macro @@ -35,7 +35,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_MD_VERSION_NUM 0x02040f +#define MOD_MD_VERSION_NUM 0x020410 #define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory" #define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock" diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c index 34f43311e3..d5237f4699 100644 --- a/modules/md/mod_md.c +++ b/modules/md/mod_md.c @@ -313,8 +313,8 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p) md->sc = base_sc; } - if (!md->ca_url) { - md->ca_url = md_config_gets(md->sc, MD_CONFIG_CA_URL); + if (!md->ca_urls && md->sc->ca_urls) { + md->ca_urls = apr_array_copy(p, md->sc->ca_urls); } if (!md->ca_proto) { md->ca_proto = md_config_gets(md->sc, MD_CONFIG_CA_PROTO); @@ -705,7 +705,7 @@ static apr_status_t merge_mds_with_conf(md_mod_conf_t *mc, apr_pool_t *p, ap_log_error(APLOG_MARK, log_level, 0, base_server, APLOGNO(10039) "Completed MD[%s, CA=%s, Proto=%s, Agreement=%s, renew-mode=%d " "renew_window=%s, warn_window=%s", - md->name, md->ca_url, md->ca_proto, md->ca_agreement, md->renew_mode, + md->name, md->ca_effective, md->ca_proto, md->ca_agreement, md->renew_mode, md->renew_window? md_timeslice_format(md->renew_window, p) : "unset", md->warn_window? md_timeslice_format(md->warn_window, p) : "unset"); } @@ -886,16 +886,21 @@ static apr_status_t md_post_config_before_ssl(apr_pool_t *p, apr_pool_t *plog, md_event_init(p); md_event_subscribe(on_event, mc); - - if (APR_SUCCESS != (rv = setup_store(&store, mc, p, s)) - || APR_SUCCESS != (rv = md_reg_create(&mc->reg, p, store, mc->proxy_url, mc->ca_certs))) { + + rv = setup_store(&store, mc, p, s); + if (APR_SUCCESS != rv) goto leave; + + rv = md_reg_create(&mc->reg, p, store, mc->proxy_url, mc->ca_certs, + mc->min_delay, mc->retry_failover); + if (APR_SUCCESS != rv) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072) "setup md registry"); goto leave; } /* renew on 30% remaining /*/ rv = md_ocsp_reg_make(&mc->ocsp, p, store, mc->ocsp_renew_window, - AP_SERVER_BASEVERSION, mc->proxy_url); + AP_SERVER_BASEVERSION, mc->proxy_url, + mc->min_delay); if (APR_SUCCESS != rv) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10196) "setup ocsp registry"); goto leave; diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c index 82c7191768..f096ad238f 100644 --- a/modules/md/mod_md_config.c +++ b/modules/md/mod_md_config.c @@ -84,6 +84,8 @@ static md_mod_conf_t defmc = { "crt.sh", /* default cert checker site name */ "https://crt.sh?q=", /* default cert checker site url */ NULL, /* CA cert file to use */ + apr_time_from_sec(5), /* minimum delay for retries */ + 13, /* retry_failover after 14 errors, with 5s delay ~ half a day */ }; static md_timeslice_t def_renew_window = { @@ -107,7 +109,7 @@ static md_srv_conf_t defconf = { NULL, /* pkey spec */ &def_renew_window, /* renew window */ &def_warn_window, /* warn window */ - NULL, /* ca url */ + NULL, /* ca urls */ NULL, /* ca contact (email) */ MD_PROTO_ACME, /* ca protocol */ NULL, /* ca agreemnent */ @@ -161,7 +163,7 @@ static void srv_conf_props_clear(md_srv_conf_t *sc) sc->pks = NULL; sc->renew_window = NULL; sc->warn_window = NULL; - sc->ca_url = NULL; + sc->ca_urls = NULL; sc->ca_contact = NULL; sc->ca_proto = NULL; sc->ca_agreement = NULL; @@ -181,7 +183,7 @@ static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from) to->pks = from->pks; to->warn_window = from->warn_window; to->renew_window = from->renew_window; - to->ca_url = from->ca_url; + to->ca_urls = from->ca_urls; to->ca_contact = from->ca_contact; to->ca_proto = from->ca_proto; to->ca_agreement = from->ca_agreement; @@ -201,7 +203,7 @@ static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t if (from->pks) md->pks = md_pkeys_spec_clone(p, from->pks); if (from->renew_window) md->renew_window = from->renew_window; if (from->warn_window) md->warn_window = from->warn_window; - if (from->ca_url) md->ca_url = from->ca_url; + if (from->ca_urls) md->ca_urls = apr_array_copy(p, from->ca_urls); if (from->ca_proto) md->ca_proto = from->ca_proto; if (from->ca_agreement) md->ca_agreement = from->ca_agreement; if (from->ca_contact) { @@ -247,7 +249,8 @@ static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv) nsc->renew_window = add->renew_window? add->renew_window : base->renew_window; nsc->warn_window = add->warn_window? add->warn_window : base->warn_window; - nsc->ca_url = add->ca_url? add->ca_url : base->ca_url; + nsc->ca_urls = add->ca_urls? apr_array_copy(pool, add->ca_urls) + : (base->ca_urls? apr_array_copy(pool, base->ca_urls) : NULL); nsc->ca_contact = add->ca_contact? add->ca_contact : base->ca_contact; nsc->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto; nsc->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement; @@ -475,19 +478,29 @@ static const char *md_config_set_names(cmd_parms *cmd, void *dc, return NULL; } -static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value) +static const char *md_config_set_ca(cmd_parms *cmd, void *dc, + int argc, char *const argv[]) { md_srv_conf_t *sc = md_config_get(cmd->server); const char *err, *url; + int i; (void)dc; if ((err = md_conf_check_location(cmd, MD_LOC_ALL))) { return err; } - if (APR_SUCCESS != md_get_ca_url_from_name(&url, cmd->pool, value)) { - return url; + if (!sc->ca_urls) { + sc->ca_urls = apr_array_make(cmd->pool, 3, sizeof(const char *)); + } + else { + apr_array_clear(sc->ca_urls); + } + for (i = 0; i < argc; ++i) { + if (APR_SUCCESS != md_get_ca_url_from_name(&url, cmd->pool, argv[i])) { + return url; + } + APR_ARRAY_PUSH(sc->ca_urls, const char *) = url; } - sc->ca_url = url; return NULL; } @@ -603,6 +616,37 @@ static const char *md_config_set_base_server(cmd_parms *cmd, void *dc, const cha return set_on_off(&config->mc->manage_base_server, value, cmd->pool); } +static const char *md_config_set_min_delay(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); + apr_time_t delay; + + (void)dc; + if (err) return err; + if (md_duration_parse(&delay, value, "s") != APR_SUCCESS) { + return "unrecognized duration format"; + } + config->mc->min_delay = delay; + return NULL; +} + +static const char *md_config_set_retry_failover(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *config = md_config_get(cmd->server); + const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD); + int retry_failover; + + (void)dc; + if (err) return err; + retry_failover = atoi(value); + if (retry_failover <= 0) { + return "invalid argument, must be a number > 0"; + } + config->mc->retry_failover = retry_failover; + return NULL; +} + static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value) { md_srv_conf_t *config = md_config_get(cmd->server); @@ -1090,8 +1134,8 @@ leave: } const command_rec md_cmds[] = { - AP_INIT_TAKE1("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, - "URL or known name of CA issuing the certificates"), + AP_INIT_TAKE_ARGV("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, + "URL(s) or known name(s) of CA issuing the certificates"), AP_INIT_TAKE1("MDCertificateAgreement", md_config_set_agreement, NULL, RSRC_CONF, "either 'accepted' or the URL of CA Terms-of-Service agreement you accept"), AP_INIT_TAKE_ARGV("MDCAChallenges", md_config_set_cha_tyes, NULL, RSRC_CONF, @@ -1167,6 +1211,10 @@ const command_rec md_cmds[] = { "Set the CA file to use for connections"), AP_INIT_TAKE12("MDExternalAccountBinding", md_config_set_eab, NULL, RSRC_CONF, "Set the external account binding keyid and hmac values to use at CA"), + AP_INIT_TAKE1("MDRetryDelay", md_config_set_min_delay, NULL, RSRC_CONF, + "Time length for first retry, doubled on every consecutive error."), + AP_INIT_TAKE1("MDRetryFailover", md_config_set_retry_failover, NULL, RSRC_CONF, + "The number of errors before a failover to another CA is triggered."), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; @@ -1226,8 +1274,6 @@ md_srv_conf_t *md_config_cget(conn_rec *c) const char *md_config_gets(const md_srv_conf_t *sc, md_config_var_t var) { switch (var) { - case MD_CONFIG_CA_URL: - return sc->ca_url? sc->ca_url : defconf.ca_url; case MD_CONFIG_CA_CONTACT: return sc->ca_contact? sc->ca_contact : defconf.ca_contact; case MD_CONFIG_CA_PROTO: diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h index 35c3152981..5d7da4b8d1 100644 --- a/modules/md/mod_md_config.h +++ b/modules/md/mod_md_config.h @@ -24,7 +24,6 @@ struct md_ocsp_reg_t; struct md_pkeys_spec_t; typedef enum { - MD_CONFIG_CA_URL, MD_CONFIG_CA_CONTACT, MD_CONFIG_CA_PROTO, MD_CONFIG_BASE_DIR, @@ -71,6 +70,8 @@ struct md_mod_conf_t { const char *cert_check_name; /* name of the linked certificate check site */ const char *cert_check_url; /* url "template for" checking a certificate */ const char *ca_certs; /* root certificates to use for connections */ + apr_time_t min_delay; /* minimum delay for retries */ + int retry_failover; /* number of errors to trigger CA failover */ }; typedef struct md_srv_conf_t { @@ -86,7 +87,7 @@ typedef struct md_srv_conf_t { md_timeslice_t *renew_window; /* time before expiration that starts renewal */ md_timeslice_t *warn_window; /* time before expiration that warning are sent out */ - const char *ca_url; /* url of CA certificate service */ + struct apr_array_header_t *ca_urls; /* urls of CAs */ const char *ca_contact; /* contact email registered to account */ const char *ca_proto; /* protocol used vs CA (e.g. ACME) */ const char *ca_agreement; /* accepted agreement uri between CA and user */ diff --git a/modules/md/mod_md_drive.c b/modules/md/mod_md_drive.c index 14c43d5501..5565f44d75 100644 --- a/modules/md/mod_md_drive.c +++ b/modules/md/mod_md_drive.c @@ -125,7 +125,7 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p } md_job_start_run(job, result, md_reg_store_get(dctx->mc->reg)); - md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, result, ptemp); + md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, job->error_runs, result, ptemp); md_job_end_run(job, result); if (APR_SUCCESS == result->status) { diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c index 6891ef832e..96c988782b 100644 --- a/modules/md/mod_md_status.c +++ b/modules/md/mod_md_status.c @@ -349,32 +349,62 @@ static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status if (jcert) si_val_valid_time(ctx, jcert, &sub); } -static void si_val_ca_url(status_ctx *ctx, md_json_t *mdj, const status_info *info) +static void val_url_print(status_ctx *ctx, const status_info *info, + const char*url, const char *proto, int i) +{ + const char *s; + + if (proto && !strcmp(proto, "tailscale")) { + s = "tailscale"; + } + else if (url) { + s = md_get_ca_name_from_url(ctx->p, url); + } + if (HTML_STATUS(ctx)) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s<a href='%s'>%s</a>", + i? " " : "", + ap_escape_html2(ctx->p, url, 1), + ap_escape_html2(ctx->p, s, 1)); + } + else if (i == 0) { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", + ctx->prefix, info->label, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", + ctx->prefix, info->label, url); + } + else { + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName%d: %s\n", + ctx->prefix, info->label, i, s); + apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL%d: %s\n", + ctx->prefix, info->label, i, url); + } +} + +static void si_val_ca_urls(status_ctx *ctx, md_json_t *mdj, const status_info *info) { md_json_t *jcert; + const char *proto, *url; + apr_array_header_t *urls; + int i; jcert = md_json_getj(mdj, info->key, NULL); - if (jcert) { - const char *proto, *s, *url; + if (!jcert) { + return; + } - proto = md_json_gets(jcert, MD_KEY_PROTO, NULL); - s = url = md_json_gets(jcert, MD_KEY_URL, NULL); - if (proto && !strcmp(proto, "tailscale")) { - s = "tailscale"; - } - else if (url) { - s = md_get_ca_name_from_url(ctx->p, url); - } - if (HTML_STATUS(ctx)) { - apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>", - ap_escape_html2(ctx->p, url, 1), - ap_escape_html2(ctx->p, s, 1)); - } - else { - apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n", - ctx->prefix, info->label, s); - apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n", - ctx->prefix, info->label, url); + proto = md_json_gets(jcert, MD_KEY_PROTO, NULL); + url = md_json_gets(jcert, MD_KEY_URL, NULL); + if (url) { + /* print the effective CA url used, if set */ + val_url_print(ctx, info, url, proto, 0); + } + else { + /* print the available CA urls configured */ + urls = apr_array_make(ctx->p, 3, sizeof(const char*)); + md_json_getsa(urls, jcert, MD_KEY_URLS, NULL); + for (i = 0; i < urls->nelts; ++i) { + url = APR_ARRAY_IDX(urls, i, const char*); + val_url_print(ctx, info, url, proto, i); } } } @@ -673,7 +703,7 @@ static const status_info status_infos[] = { { "Names", MD_KEY_DOMAINS, si_val_names }, { "Status", MD_KEY_STATE, si_val_status }, { "Valid", MD_KEY_CERT, si_val_cert_valid_time }, - { "CA", MD_KEY_CA, si_val_ca_url }, + { "CA", MD_KEY_CA, si_val_ca_urls }, { "Stapling", MD_KEY_STAPLING, si_val_stapling }, { "CheckAt", MD_KEY_SHA256_FINGERPRINT, si_val_remote_check }, { "Activity", MD_KEY_NOTIFIED, si_val_activity }, diff --git a/test/modules/md/md_conf.py b/test/modules/md/md_conf.py index 0b4502a7ac..19d4977f00 100755 --- a/test/modules/md/md_conf.py +++ b/test/modules/md/md_conf.py @@ -13,7 +13,9 @@ class MDConf(HttpdConf): admin = f"admin@{env.http_tld}" if len(admin.strip()): self.add_admin(admin) - + self.add([ + "MDRetryDelay 1s", # speed up testing a little + ]) if local_ca: self.add([ f"MDCertificateAuthority {env.acme_url}", @@ -23,7 +25,7 @@ class MDConf(HttpdConf): ]) if std_ports: self.add(f"MDPortMap 80:{env.http_port} 443:{env.https_port}") - if env.ssl_module == "tls": + if env.ssl_module == "mod_tls": self.add(f"TLSListen {env.https_port}") self.add([ "<Location /server-status>", diff --git a/test/modules/md/md_env.py b/test/modules/md/md_env.py index ca07f96937..e8e36e5b1b 100755 --- a/test/modules/md/md_env.py +++ b/test/modules/md/md_env.py @@ -313,7 +313,8 @@ class MDTestEnv(HttpdTestEnv): if state >= 0: assert md['state'] == state if ca: - assert md['ca']['url'] == ca + assert len(md['ca']['urls']) == 1 + assert md['ca']['urls'][0] == ca if protocol: assert md['ca']['proto'] == protocol if agreement: @@ -343,6 +344,7 @@ class MDTestEnv(HttpdTestEnv): assert False, f"pkey missing: {pkey_file}: {r.stdout}" if not os.path.isfile(cert_file): assert False, f"cert missing: {cert_file}: {r.stdout}" + return md def check_md_credentials(self, domain): if isinstance(domain, list): diff --git a/test/modules/md/test_001_store.py b/test/modules/md/test_001_store.py index c888db9b8e..995d40dc7b 100644 --- a/test/modules/md/test_001_store.py +++ b/test/modules/md/test_001_store.py @@ -39,7 +39,7 @@ class TestStore: "domains": [dns], "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": 0 @@ -55,7 +55,7 @@ class TestStore: "domains": dns, "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": 0 @@ -76,7 +76,7 @@ class TestStore: "domains": dns2, "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": 0 @@ -129,7 +129,7 @@ class TestStore: "domains": domains[i], "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": 0 @@ -186,10 +186,10 @@ class TestStore: def test_md_001_402(self, env: MDTestEnv): dns = "test000-402.com" args = ["store", "add", dns] - assert env.a2md(args).json['output'][0]['ca']['url'] == env.acme_url + assert env.a2md(args).json['output'][0]['ca']['urls'][0] == env.acme_url nurl = "https://foo.com/" args = [env.a2md_bin, "-a", nurl, "-d", env.store_dir, "-j", "store", "update", dns] - assert env.run(args).json['output'][0]['ca']['url'] == nurl + assert env.run(args).json['output'][0]['ca']['urls'][0] == nurl # test case: update nonexisting managed domain def test_md_001_403(self, env: MDTestEnv): diff --git a/test/modules/md/test_100_reg_add.py b/test/modules/md/test_100_reg_add.py index 2b5bd23aa2..1a6d3fe838 100644 --- a/test/modules/md/test_100_reg_add.py +++ b/test/modules/md/test_100_reg_add.py @@ -23,7 +23,7 @@ class TestRegAdd: "domains": [dns], "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -39,7 +39,7 @@ class TestRegAdd: "domains": dns, "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -60,7 +60,7 @@ class TestRegAdd: "domains": dns2, "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE diff --git a/test/modules/md/test_110_reg_update.py b/test/modules/md/test_110_reg_update.py index 3120ced6c4..71b50f8ea3 100644 --- a/test/modules/md/test_110_reg_update.py +++ b/test/modules/md/test_110_reg_update.py @@ -37,7 +37,7 @@ class TestRegUpdate: "domains": dns, "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -104,7 +104,7 @@ class TestRegUpdate: "domains": [self.NAME1, "www.greenbytes2.de", "mail.greenbytes2.de"], "contacts": [], "ca": { - "url": url, + "urls": [url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -121,7 +121,7 @@ class TestRegUpdate: def test_md_110_102(self, env): md = env.a2md(["update", self.NAME1, "ca", env.acme_url, "FOO"]).json['output'][0] env.check_json_contains(md['ca'], { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "FOO" }) assert md['state'] == 1 @@ -137,7 +137,7 @@ class TestRegUpdate: "contacts": [], "ca": { "account": acc_id, - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -148,7 +148,7 @@ class TestRegUpdate: assert env.a2md(["update", self.NAME1, "account", "test.account.id"]).exit_code == 0 md = env.a2md(["update", self.NAME1, "account"]).json['output'][0] env.check_json_contains(md['ca'], { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }) assert md['state'] == 1 @@ -159,7 +159,7 @@ class TestRegUpdate: md = env.a2md(["update", self.NAME1, "account", "foo.test.com"]).json['output'][0] env.check_json_contains(md['ca'], { "account": "foo.test.com", - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }) assert md['state'] == 1 @@ -170,7 +170,7 @@ class TestRegUpdate: "test2.account.id"]).json['output'][0] env.check_json_contains(md['ca'], { "account": "test.account.id", - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }) assert md['state'] == 1 @@ -185,7 +185,7 @@ class TestRegUpdate: "domains": [self.NAME1, "www.greenbytes2.de", "mail.greenbytes2.de"], "contacts": ["mailto:" + mail], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE @@ -237,7 +237,7 @@ class TestRegUpdate: "domains": [self.NAME1, "www.greenbytes2.de", "mail.greenbytes2.de"], "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME", "agreement": env.acme_tos }, @@ -249,7 +249,7 @@ class TestRegUpdate: assert env.a2md(["update", self.NAME1, "agreement", env.acme_tos]).exit_code == 0 md = env.a2md(["update", self.NAME1, "agreement"]).json['output'][0] env.check_json_contains(md['ca'], { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }) assert md['state'] == 1 @@ -259,7 +259,7 @@ class TestRegUpdate: md = env.a2md(["update", self.NAME1, "agreement", env.acme_tos, "http://invalid.tos/"]).json['output'][0] env.check_json_contains(md['ca'], { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME", "agreement": env.acme_tos }) diff --git a/test/modules/md/test_120_reg_list.py b/test/modules/md/test_120_reg_list.py index 0c1ce8a664..82e109f723 100644 --- a/test/modules/md/test_120_reg_list.py +++ b/test/modules/md/test_120_reg_list.py @@ -39,7 +39,7 @@ class TestRegAdd: "domains": domains[i], "contacts": [], "ca": { - "url": env.acme_url, + "urls": [env.acme_url], "proto": "ACME" }, "state": env.MD_S_INCOMPLETE diff --git a/test/modules/md/test_300_conf_validate.py b/test/modules/md/test_300_conf_validate.py index e6b0561f82..25b033e393 100644 --- a/test/modules/md/test_300_conf_validate.py +++ b/test/modules/md/test_300_conf_validate.py @@ -340,7 +340,7 @@ class TestConf: conf.install() assert env.apache_restart() == 0, "Server did not accepted CA '{}'".format(ca) md = env.get_md_status(domain) - assert md['ca']['url'] == url + assert md['ca']['urls'][0] == url, f"CA url '{url}' not set in {md}" # vhost on another address, see #278 def test_md_300_026(self, env): @@ -365,3 +365,27 @@ class TestConf: conf.install() assert env.apache_restart() == 0 + # test case: configure more than 1 CA + @pytest.mark.parametrize("cas, should_work", [ + (["https://acme-v02.api.letsencrypt.org/directory"], True), + (["https://acme-v02.api.letsencrypt.org/directory", "buypass"], True), + (["x", "buypass"], False), + (["letsencrypt", "abc"], False), + (["letsencrypt", "buypass"], True), + ]) + def test_md_300_027(self, env, cas, should_work): + domain = f"test1.{env.http_tld}" + conf = MDConf(env, text=f""" + MDCertificateAuthority {' '.join(cas)} + MDRenewMode manual + """) + conf.add_md([domain]) + conf.install() + rv = env.apache_restart() + if should_work: + assert rv == 0, "Server did not accepted CAs '{}'".format(cas) + md = env.get_md_status(domain) + assert len(md['ca']['urls']) == len(cas) + else: + assert rv != 0, "Server should not have accepted CAs '{}'".format(cas) + diff --git a/test/modules/md/test_702_auto.py b/test/modules/md/test_702_auto.py index 57187ad749..8e8f5f155c 100644 --- a/test/modules/md/test_702_auto.py +++ b/test/modules/md/test_702_auto.py @@ -1,4 +1,6 @@ import os +import time + import pytest from pyhttpd.conf import HttpdConf @@ -131,7 +133,8 @@ class TestAutov2: assert env.apache_restart() == 0 env.check_md(domains) assert env.await_completion([domain]) - env.check_md_complete(domain) + md = env.check_md_complete(domain) + assert md['ca']['url'], f"URL of CA used not set in md: {md}" # # check: SSL is running OK cert_a = env.get_cert(name_a) diff --git a/test/modules/md/test_790_failover.py b/test/modules/md/test_790_failover.py new file mode 100644 index 0000000000..a93991233d --- /dev/null +++ b/test/modules/md/test_790_failover.py @@ -0,0 +1,87 @@ +import pytest + +from .md_env import MDTestEnv +from .md_conf import MDConf + + +@pytest.mark.skipif(condition=not MDTestEnv.has_acme_server(), + reason="no ACME test server configured") +class TestFailover: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env, acme): + acme.start(config='default') + env.check_acme() + env.clear_store() + conf = MDConf(env) + conf.install() + + assert env.apache_restart() == 0 + + @pytest.fixture(autouse=True, scope='function') + def _method_scope(self, env, request): + env.clear_store() + self.test_domain = env.get_request_domain(request) + + # set 2 ACME certificata authority, valid + invalid + def test_md_790_001(self, env): + domain = self.test_domain + # generate config with one MD + domains = [domain, "www." + domain] + conf = MDConf(env) + conf.add([ + "MDRetryDelay 200ms", # speed up failovers + ]) + conf.start_md(domains) + conf.add([ + f"MDCertificateAuthority {env.acme_url} https://does-not-exist/dir" + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + assert env.apache_restart() == 0 + assert env.await_completion([domain]) + env.check_md_complete(domain) + + # set 2 ACME certificata authority, invalid + valid + def test_md_790_002(self, env): + domain = self.test_domain + # generate config with one MD + domains = [domain, "www." + domain] + conf = MDConf(env) + conf.add([ + "MDRetryDelay 100ms", # speed up failovers + "MDRetryFailover 2", + ]) + conf.start_md(domains) + conf.add([ + f"MDCertificateAuthority https://does-not-exist/dir {env.acme_url} " + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + assert env.apache_restart() == 0 + assert env.await_completion([domain]) + env.check_md_complete(domain) + + # set 3 ACME certificata authority, invalid + invalid + valid + def test_md_790_003(self, env): + domain = self.test_domain + # generate config with one MD + domains = [domain, "www." + domain] + conf = MDConf(env) + conf.add([ + "MDRetryDelay 100ms", # speed up failovers + "MDRetryFailover 2", + ]) + conf.start_md(domains) + conf.add([ + f"MDCertificateAuthority https://does-not-exist/dir https://does-not-either/ " + f"{env.acme_url} " + ]) + conf.end_md() + conf.add_vhost(domains) + conf.install() + assert env.apache_restart() == 0 + assert env.await_completion([domain]) + env.check_md_complete(domain) |