]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Merge /httpd/httpd/trunk:r1900852,1900887
authorStefan Eissing <icing@apache.org>
Mon, 16 May 2022 11:36:20 +0000 (11:36 +0000)
committerStefan Eissing <icing@apache.org>
Mon, 16 May 2022 11:36:20 +0000 (11:36 +0000)
  *) 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/branches/2.4.x@1900950 13f79535-47bb-0310-9956-ffa450edef68

28 files changed:
changes-entries/md_acme_failover.txt [new file with mode: 0644]
docs/manual/mod/mod_md.xml
modules/md/md.h
modules/md/md_acme_acct.c
modules/md/md_acme_drive.c
modules/md/md_core.c
modules/md/md_ocsp.c
modules/md/md_ocsp.h
modules/md/md_reg.c
modules/md/md_reg.h
modules/md/md_status.c
modules/md/md_status.h
modules/md/md_tailscale.c
modules/md/md_version.h
modules/md/mod_md.c
modules/md/mod_md_config.c
modules/md/mod_md_config.h
modules/md/mod_md_drive.c
modules/md/mod_md_status.c
test/modules/md/md_conf.py
test/modules/md/md_env.py
test/modules/md/test_001_store.py
test/modules/md/test_100_reg_add.py
test/modules/md/test_110_reg_update.py
test/modules/md/test_120_reg_list.py
test/modules/md/test_300_conf_validate.py
test/modules/md/test_702_auto.py
test/modules/md/test_790_failover.py [new file with mode: 0644]

diff --git a/changes-entries/md_acme_failover.txt b/changes-entries/md_acme_failover.txt
new file mode 100644 (file)
index 0000000..bb1999c
--- /dev/null
@@ -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]
index 800abbaa2fdf1e04bcbec4bed30654fe5403d181..18e2554154451f3c3643e83b0aae3f69d19f7754 100644 (file)
@@ -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>
index 5a3aaecdac63ab7bf39b500e6c34c90c548dfc4c..af695f1458898c10d9670f57fdb986e5a2938568 100644 (file)
@@ -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"
index 94dd83190dcae35047933e37c20d06fe011f9390..f3e043e87c05b1ff69cef4c0d4b32c301f1e0457 100644 (file)
@@ -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;
         }
index bc0f17f271004b0140cdfff2b8329b322690b2cd..abe7d644e66ead341fc8fb34d43b57da4cf8bb3b 100644 (file)
@@ -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;
 }
index f82f950503a6691f5a8609b18fde800f960593aa..8c7c4536251fbe6c85271735fff1ccc16e6b1c9c 100644 (file)
@@ -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);
index 67c6e12d800cb7f0f6f3a5e7825bd7cc6bfb7533..8cbf05b3e1c255938085b7d0f54aa5aaeb7e5a63 100644 (file)
@@ -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(&reg->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);
 }
index d6ee0f1d7dd20a473d8ba3c652ef6482f229c82d..c91dc5490638793f182c137f1e52e4f3d6a52643 100644 (file)
@@ -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);
 
index 0c59aeb737d71912de827c0f603b9d4cb55b3884..21374fc1af95fa5d8af51c1726e2a653f6d17f93 100644 (file)
@@ -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(&reg->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF); 
     md_timeslice_create(&reg->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);
 }
index aa626c9276843e34b43baf53d565c2b3f7736566..ccaf10253ad18af9e3ee86cfe671dca84b87911f 100644 (file)
@@ -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);
 
 /**
index 32efc19a67b8ba1306403084327484ac538be6bf..936c65349f9b2ed103da7a37a36cae2bf9c65b6a 100644 (file)
@@ -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;
         }
index cd358b0e8e1affcb5ae06ba2539a4904d5756248..f4d09bd90f7a6c67d126a2b3a157de14815a166f 100644 (file)
@@ -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);
 
index dd3b1458ad37239be024449f958b07ea858e4531..c8d2bad64c5bb09314fd7196938cb821a3272897 100644 (file)
@@ -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;
 }
index d634538e1a3e529d69c08ed0446becfa180d97db..4b8aef13d67bfdbc29c3aa09dd85c854745c2fff 100644 (file)
@@ -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"
index bb939d26294b6220de63bbf15fc131902d29daba..558669657cc1b2b24eca4ae0450fef5392cd4334 100644 (file)
@@ -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;
index 82c7191768da9ddd0f18a4ade188c5817a168a2b..f096ad238fe641472d0fd873cd7086448547dea0 100644 (file)
@@ -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:
index 35c315298105e7c87276a53f0413865f3eaaf411..5d7da4b8d1de662f3a2454436bb7d541865dae39 100644 (file)
@@ -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 */ 
index 14c43d550191ea437639e2534fc961c02c179c32..5565f44d758c1c7cd5ff5ca6316cd2bf44ead640 100644 (file)
@@ -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) {
index 6891ef832eeb4398765b34a3c541ed4872c4847c..22860515ffdcd71c827d6a6001b4970a45ba9579 100644 (file)
@@ -349,32 +349,65 @@ 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);
+    }
+    else {
+        return;
+    }
+    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 +706,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 },
index 0b4502a7ac8307c0687c9948abc3a46194205127..19d4977f004582e4f8130506e2d1573841e077b3 100755 (executable)
@@ -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>",
index ca07f969374cd8e284ff70c2f9d5e7274991a6a2..e8e36e5b1bc3d4d5b98131db1a1565b41d03d78b 100755 (executable)
@@ -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):
index c888db9b8ee86d3b0078d4d9013d07e04ff293b9..995d40dc7be941cac6e5401d314f2ed18b240f1e 100644 (file)
@@ -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):
index 2b5bd23aa296b1fd5b83759e8a06c94bb791aea5..1a6d3fe838e9ff0b2bc242b59ff8e74a0e680307 100644 (file)
@@ -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
index 3120ced6c405660c2b64c601b5174f362d26602b..71b50f8ea311dc86b487a1db4c3ac0a6181a6bef 100644 (file)
@@ -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
         })
index 0c1ce8a664da16d5a884c4cabe14acad81e44f92..82e109f723e50da096126134bddb1e1e631b5bff 100644 (file)
@@ -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
index 0efbb115ea4dba6a872568fd0a5f485f30579c18..85371ba227b9a5635c2c12db58ff47ca74349a96 100644 (file)
@@ -340,5 +340,51 @@ 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):
+        assert env.apache_stop() == 0
+        conf = MDConf(env)
+        domain = f"t300_026.{env.http_tld}"
+        conf.add(f"""
+            MDomain {domain}
+            """)
+        conf.add_vhost(port=env.http_port, domains=[domain], with_ssl=False)
+        conf.add(f"""
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName {domain}
+              ServerAlias xxx.{env.http_tld}
+              SSLEngine on
+            </VirtualHost>
+            <VirtualHost 10.0.0.1:12345>
+              ServerName {domain}
+              SSLEngine on
+            </VirtualHost>
+            """)
+        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)
index 6864b0d2bce271bbc499a916e0d82a47eb1291cb..00d535b57efc4ad21c41c17e438df2a67f93d651 100644 (file)
@@ -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 (file)
index 0000000..a939912
--- /dev/null
@@ -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)