]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Convert rlm_crl to use coordinator thread
authorNick Porter <nick@portercomputing.co.uk>
Thu, 26 Mar 2026 16:41:39 +0000 (16:41 +0000)
committerNick Porter <nick@portercomputing.co.uk>
Mon, 6 Apr 2026 11:11:18 +0000 (12:11 +0100)
src/modules/rlm_crl/rlm_crl.c

index a762aead2b764b9154b56c51fa5b1bf3483fb7de..8b80492538018e2de0f0193bbff283117c55b376 100644 (file)
  */
 RCSID("$Id$")
 
+#include <freeradius-devel/crl/crl.h>
 #include <freeradius-devel/server/base.h>
 #include <freeradius-devel/server/module_rlm.h>
+#include <freeradius-devel/io/coord_pair.h>
 
 #include <freeradius-devel/tls/strerror.h>
 #include <freeradius-devel/tls/utils.h>
 
 #include <openssl/x509v3.h>
-#include <openssl/pem.h>
-#include <openssl/asn1.h>
-#include <openssl/bn.h>
 
 /** Thread specific structure to hold requests awaiting CRL fetching */
 typedef struct {
        fr_rb_tree_t                    pending;                        //!< Requests yielded while the CRL is being fetched.
+       fr_coord_worker_t               *cw;                            //!< Worker side of coordinator communication.
+       fr_rb_tree_t                    crls;                           //!< CRLs fetched from the coordinator.
+       fr_rb_tree_t                    fails;                          //!< Recent CRLs which have failed to fetch.
 } rlm_crl_thread_t;
 
-/** Global tree of CRLs
- *
- * Separate from the instance data because that's protected.
- */
-typedef struct {
-       fr_rb_tree_t                    *crls;                          //!< A tree of CRLs organised by CDP URL.
-       fr_timer_list_t                 *timer_list;                    //!< The timer list to use for CRL expiry.
-                                                                       ///< This gets serviced by the main loop.
-       rlm_crl_thread_t                *fetching;                      //!< Pointer to thread instance data of
-                                                                       ///< thread which is fetching a CRL.
-       pthread_mutex_t                 mutex;
-} rlm_crl_mutable_t;
-
 typedef struct {
-       CONF_SECTION                    *virtual_server;                //!< Virtual server to use when retrieving CRLs
-       fr_time_delta_t                 force_expiry;                   //!< Force expiry of CRLs after this time
-       bool                            force_expiry_is_set;
-       fr_time_delta_t                 force_delta_expiry;             //!< Force expiry of delta CRLs after this time
-       bool                            force_delta_expiry_is_set;
-       fr_time_delta_t                 early_refresh;                  //!< Time interval before nextUpdate to refresh
-       char const                      *ca_file;                       //!< File containing certs for verifying CRL signatures.
-       char const                      *ca_path;                       //!< Directory containing certs for verifying CRL signatures.
-       X509_STORE                      *verify_store;                  //!< Store of certificates to verify CRL signatures.
-       rlm_crl_mutable_t               *mutable;                       //!< Mutable data that's shared between all threads.
-       CONF_SECTION                    *cs;                            //!< Module instance config.
-       bool                            trigger_rate_limit;             //!< Rate limit triggers.
+       fr_time_delta_t                 retry_delay;                    //!< Time to hold off between CRL fetching failures.
+       fr_coord_pair_reg_t             *coord_pair_reg;                //!< coord_pair registration for fetching CRLs.
+       fr_coord_reg_t                  *coord_reg;                     //!< coord registration for fetching CRLs.
 } rlm_crl_t;
 
-/** A single CRL in the global list of CRLs */
+/** A single CRL in the thread specific list of CRLs */
 typedef struct {
        X509_CRL                        *crl;                           //!< The CRL.
        char const                      *cdp_url;                       //!< The URL of the CRL.
-       ASN1_INTEGER                    *crl_num;                       //!< The CRL number.
-       fr_timer_t                      *ev;                            //!< When to expire the CRL
        fr_rb_node_t                    node;                           //!< The node in the tree
        fr_value_box_list_t             delta_urls;                     //!< URLs from which a delta CRL can be retrieved.
-       rlm_crl_t const                 *inst;                          //!< The instance of the CRL module.
-       rlm_crl_thread_t                *thread;                        //!< The thread which fetched this entry.
 } crl_entry_t;
 
+/** Structure to record recent fetch failures
+ */
+typedef struct {
+       char const                      *cdp_url;                       //!< The URL which failed to fetch.
+       fr_rb_node_t                    node;                           //!< Node in the tree of failures.
+       fr_time_t                       fail_time;                      //!< When did the failure occur.
+} crl_fail_t;
+
 /** Structure to record a request which is waiting for CRL fetching to complete */
 typedef struct {
        request_t                       *request;
@@ -102,37 +86,41 @@ typedef struct {
 } rlm_crl_rctx_t;
 
 static conf_parser_t module_config[] = {
-       { FR_CONF_OFFSET_IS_SET("force_expiry", FR_TYPE_TIME_DELTA, 0, rlm_crl_t, force_expiry) },
-       { FR_CONF_OFFSET_IS_SET("force_delta_expiry", FR_TYPE_TIME_DELTA, 0, rlm_crl_t, force_delta_expiry) },
-       { FR_CONF_OFFSET("early_refresh", rlm_crl_t, early_refresh) },
-       { FR_CONF_OFFSET("ca_file", rlm_crl_t, ca_file) },
-       { FR_CONF_OFFSET("ca_path", rlm_crl_t, ca_path) },
-       { FR_CONF_OFFSET("trigger_rate_limit", rlm_crl_t, trigger_rate_limit), .dflt = "yes" },
+       { FR_CONF_OFFSET("retry_delay", rlm_crl_t, retry_delay), .dflt = "30s" },
        CONF_PARSER_TERMINATOR
 };
 
-static fr_dict_t const *dict_freeradius;
+/** Callback IDs used by CRL coordinator calls
+ */
+typedef enum {
+       CRL_COORD_PAIR_CALLBACK_ID = 0,
+} rlm_crl_coord_callback_t;
+
+static fr_dict_t const *dict_crl;
 
 extern fr_dict_autoload_t rlm_crl_dict[];
 fr_dict_autoload_t rlm_crl_dict[] = {
-       { .out = &dict_freeradius, .proto = "freeradius" },
+       { .out = &dict_crl, .proto = "crl" },
        DICT_AUTOLOAD_TERMINATOR
 };
 
 static fr_dict_attr_t const *attr_crl_data;
 static fr_dict_attr_t const *attr_crl_cdp_url;
+static fr_dict_attr_t const *attr_base_crl;
+static fr_dict_attr_t const *attr_delta_crl;
+static fr_dict_attr_t const *attr_packet_type;
 
 extern fr_dict_attr_autoload_t rlm_crl_dict_attr[];
 fr_dict_attr_autoload_t rlm_crl_dict_attr[] = {
-       { .out = &attr_crl_data, .name = "CRL.Data", .type = FR_TYPE_OCTETS, .dict = &dict_freeradius },
-       { .out = &attr_crl_cdp_url, .name = "CRL.CDP-URL", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
+       { .out = &attr_crl_data, .name = "CRL-Data", .type = FR_TYPE_OCTETS, .dict = &dict_crl },
+       { .out = &attr_crl_cdp_url, .name = "CDP-URL", .type = FR_TYPE_STRING, .dict = &dict_crl },
+       { .out = &attr_base_crl, .name = "Base-CRL", .type = FR_TYPE_STRING, .dict = &dict_crl },
+       { .out = &attr_delta_crl, .name = "Delta-CRL", .type = FR_TYPE_STRING, .dict = &dict_crl },
+       { .out = &attr_packet_type, .name = "Packet-Type", .type = FR_TYPE_UINT32, .dict = &dict_crl },
        DICT_AUTOLOAD_TERMINATOR
 };
 
 typedef struct {
-       tmpl_t                          *http_exp;                      //!< The xlat expansion used to retrieve the CRL via http://
-       tmpl_t                          *ldap_exp;                      //!< The xlat expansion used to retrieve the CRL via ldap://
-       tmpl_t                          *ftp_exp;                       //!< The xlat expansion used to retrieve the CRL via ftp://
        fr_value_box_t                  serial;                         //!< The serial to check
        fr_value_box_list_head_t        *cdp;                           //!< The CRL distribution points
 } rlm_crl_env_t;
@@ -150,17 +138,6 @@ typedef enum {
 static const call_env_method_t crl_env = {
        FR_CALL_ENV_METHOD_OUT(rlm_crl_env_t),
        .env = (call_env_parser_t[]){
-               { FR_CALL_ENV_SUBSECTION("source", NULL, CALL_ENV_FLAG_SUBSECTION | CALL_ENV_FLAG_PARSE_MISSING,
-                       ((call_env_parser_t[]) {
-                               { FR_CALL_ENV_SUBSECTION("dynamic", NULL, CALL_ENV_FLAG_SUBSECTION | CALL_ENV_FLAG_PARSE_MISSING,
-                               ((call_env_parser_t[]) {
-                                       { FR_CALL_ENV_PARSE_ONLY_OFFSET("http", FR_TYPE_OCTETS, CALL_ENV_FLAG_REQUIRED, rlm_crl_env_t, http_exp )},
-                                       { FR_CALL_ENV_PARSE_ONLY_OFFSET("ldap", FR_TYPE_OCTETS, CALL_ENV_FLAG_NONE, rlm_crl_env_t, ldap_exp )},
-                                       { FR_CALL_ENV_PARSE_ONLY_OFFSET("ftp", FR_TYPE_OCTETS, CALL_ENV_FLAG_NONE, rlm_crl_env_t, ftp_exp )},
-                                       CALL_ENV_TERMINATOR
-                               }))},
-                               CALL_ENV_TERMINATOR
-                       }))},
                { FR_CALL_ENV_OFFSET("serial", FR_TYPE_STRING, CALL_ENV_FLAG_ATTRIBUTE | CALL_ENV_FLAG_REQUIRED | CALL_ENV_FLAG_SINGLE, rlm_crl_env_t, serial),
                                         .pair.dflt = "session-state.TLS-Certificate.Serial", .pair.dflt_quote = T_BARE_WORD },
                { FR_CALL_ENV_OFFSET("cdp", FR_TYPE_STRING, CALL_ENV_FLAG_BARE_WORD_ATTRIBUTE| CALL_ENV_FLAG_REQUIRED | CALL_ENV_FLAG_MULTI | CALL_ENV_FLAG_NULLABLE, rlm_crl_env_t, cdp),
@@ -177,11 +154,6 @@ static int8_t crl_cmp(void const *a, void const *b)
        return CMP(strcmp(crl_a->cdp_url,  crl_b->cdp_url), 0);
 }
 
-static void crl_free(void *data)
-{
-       talloc_free(data);
-}
-
 static int8_t crl_pending_cmp(void const *a, void const *b)
 {
        crl_pending_t   const *pending_a = (crl_pending_t const *)a;
@@ -190,51 +162,12 @@ static int8_t crl_pending_cmp(void const *a, void const *b)
        return CMP(pending_a->request, pending_b->request);
 }
 
-static void crl_expire(fr_timer_list_t *tl, UNUSED fr_time_t now, void *uctx)
+static int8_t crl_fail_cmp(void const *a, void const *b)
 {
-       crl_entry_t     *crl = talloc_get_type_abort(uctx, crl_entry_t);
+       crl_fail_t      const *fail_a = (crl_fail_t const *)a;
+       crl_fail_t      const *fail_b = (crl_fail_t const *)b;
 
-       DEBUG2("CRL associated with CDP %s expired", crl->cdp_url);
-
-       /*
-        *      If the mutex is locked and this thread is fetching a CRL, asynchonously,
-        *      insert a new timer event - otherwise the mutex will never be unlocked.
-        */
-       if (pthread_mutex_trylock(&crl->inst->mutable->mutex) != 0) {
-               if (crl->inst->mutable->fetching == crl->thread) {
-                       if (fr_timer_in(crl, tl, &crl->ev, fr_time_delta_from_sec(1), false, crl_expire, crl) <0) {
-                               ERROR("Failed inserting CRL expiry event");
-                       }
-                       return;
-               }
-               pthread_mutex_lock(&crl->inst->mutable->mutex);
-       }
-       fr_rb_remove(crl->inst->mutable->crls, crl);
-       pthread_mutex_unlock(&crl->inst->mutable->mutex);
-
-       if (trigger_enabled()) {
-               fr_pair_list_t  args;
-               fr_pair_t       *vp;
-               fr_pair_list_init(&args);
-               MEM((vp = fr_pair_afrom_da_nested(crl, &args, attr_crl_cdp_url)));
-               fr_value_box_strdup_shallow(&vp->data, NULL, crl->cdp_url, true);
-               trigger(unlang_interpret_get_thread_default(), crl->inst->cs, NULL, "modules.crl.expired",
-                       crl->inst->trigger_rate_limit, &args);
-       }
-
-       talloc_free(crl);
-}
-
-/** Make sure we don't lock up the server if a request is cancelled
- */
-static void crl_signal(module_ctx_t const *mctx, UNUSED request_t *request, fr_signal_t action)
-{
-       rlm_crl_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t);
-
-       if (action == FR_SIGNAL_CANCEL) {
-               pthread_mutex_unlock(&inst->mutable->mutex);
-               pair_delete_request(attr_crl_cdp_url);
-       }
+       return CMP(strcmp(fail_a->cdp_url, fail_b->cdp_url), 0);
 }
 
 /** See if a particular serial is present in a CRL list
@@ -307,517 +240,299 @@ static crl_ret_t crl_check_serial(fr_rb_tree_t *crls, request_t *request, char c
 static int _crl_entry_free(crl_entry_t *crl_entry)
 {
        X509_CRL_free(crl_entry->crl);
-       if (crl_entry->crl_num) ASN1_INTEGER_free(crl_entry->crl_num);
        return 0;
 }
 
-/** Add an entry to the cdp_url -> crl tree
+/** Request a CRL from the coordinator
  *
- * @note Must be called with the mutex held.
+ * @param inst         Module instance
+ * @param t            Thread data
+ * @param cdp_urls     List of URLs to fetch the CRL from
+ * @param base_crl     URL of the base CRL if a delta is being requested.
+ * @return
+ *     - 0 on success
+ *     - -1 on failure
  */
-static crl_entry_t *crl_entry_create(rlm_crl_t const *inst, fr_timer_list_t *tl, char const *url, uint8_t const *data,
-                                    crl_entry_t *base_crl)
-{
-       uint8_t const   *our_data = data;
-       crl_entry_t     *crl;
-       time_t          next_update;
-       fr_time_t       now = fr_time();
-       fr_time_delta_t expiry_time;
-       int             i;
-       STACK_OF(DIST_POINT)    *dps;
-       X509_STORE_CTX  *verify_ctx = NULL;
-       X509_OBJECT     *xobj;
-       EVP_PKEY        *pkey;
-
-       MEM(crl = talloc_zero(inst->mutable->crls, crl_entry_t));
-       crl->cdp_url = talloc_bstrdup(crl, url);
-       crl->crl = d2i_X509_CRL(NULL, (const unsigned char **)&our_data, talloc_array_length(our_data));
-       if (crl->crl == NULL) {
-               fr_tls_strerror_printf("Failed to parse CRL from %s", url);
+static int crl_fetch_start(rlm_crl_t const *inst, rlm_crl_thread_t *t, fr_value_box_list_t *cdp_urls, char const *base_crl) {
+       fr_pair_list_t          list;
+       fr_pair_t               *vp;
+       TALLOC_CTX              *local = talloc_new(NULL);
+       int                     ret;
+
+       fr_pair_list_init(&list);
+       fr_pair_list_append_by_da(local, vp, &list, attr_packet_type, (uint32_t)FR_CRL_CRL_FETCH, false);
+       if (!vp) {
        error:
-               talloc_free(crl);
-               if (verify_ctx) X509_STORE_CTX_free(verify_ctx);
-               return NULL;
-       }
-       talloc_set_destructor(crl, _crl_entry_free);
-
-       verify_ctx = X509_STORE_CTX_new();
-        if (!verify_ctx || !X509_STORE_CTX_init(verify_ctx, inst->verify_store, NULL, NULL)) {
-               fr_tls_strerror_printf("Error initialising X509 store");
-               goto error;
-        }
-
-        xobj = X509_STORE_CTX_get_obj_by_subject(verify_ctx, X509_LU_X509,
-                                                 X509_CRL_get_issuer(crl->crl));
-        if (!xobj) {
-               fr_tls_strerror_printf("CRL issuer certificate not in trusted store");
-               goto error;
-        }
-        pkey = X509_get_pubkey(X509_OBJECT_get0_X509(xobj));
-        X509_OBJECT_free(xobj);
-        if (!pkey) {
-               fr_tls_strerror_printf("Error getting CRL issuer public key");
-               goto error;
-        }
-        i = X509_CRL_verify(crl->crl, pkey);
-        EVP_PKEY_free(pkey);
-
-       if (i < 0) {
-               fr_tls_strerror_printf("Could not verify CRL signature");
-               goto error;
-       }
-        if (i == 0) {
-               fr_tls_strerror_printf("CRL certificate signature failed");
-               goto error;
-       }
-
-       crl->crl_num = X509_CRL_get_ext_d2i(crl->crl, NID_crl_number, &i, NULL);
-
-       /*
-        *      If we're passed a base_crl, then this is a delta - check the delta
-        *      relates to the correct base.
-        */
-       if (base_crl) {
-               ASN1_INTEGER *base_num = X509_CRL_get_ext_d2i(crl->crl, NID_delta_crl, &i, NULL);
-               if (!base_num) {
-                       fr_tls_strerror_printf("Delta CRL missing Delta CRL Indicator extension");
-                       goto error;
-               }
-               if (ASN1_INTEGER_cmp(base_num, base_crl->crl_num) > 0) {
-                       uint64_t delta_base, crl_num;
-                       ASN1_INTEGER_get_uint64(&delta_base, base_num);
-                       ASN1_INTEGER_get_uint64(&crl_num, base_crl->crl_num);
-                       fr_tls_strerror_printf("Delta CRL referrs to base CRL number %"PRIu64", current base is %"PRIu64,
-                                               delta_base, crl_num);
-                       ASN1_INTEGER_free(base_num);
-                       goto error;
-               }
-               ASN1_INTEGER_free(base_num);
-               if (ASN1_INTEGER_cmp(crl->crl_num, base_crl->crl_num) < 0) {
-                       uint64_t delta_num, crl_num;
-                       ASN1_INTEGER_get_uint64(&delta_num, crl->crl_num);
-                       ASN1_INTEGER_get_uint64(&crl_num, base_crl->crl_num);
-                       fr_tls_strerror_printf("Delta CRL number %"PRIu64" is less than base CRL number %"PRIu64,
-                                               delta_num, crl_num);
-                       goto error;
-               }
+               talloc_free(local);
+               return -1;
        }
 
-       if (fr_tls_utils_asn1time_to_epoch(&next_update, X509_CRL_get0_nextUpdate(crl->crl)) < 0) {
-               fr_tls_strerror_printf("Failed to parse nextUpdate from CRL");
-               goto error;
+       fr_value_box_list_foreach(cdp_urls, cdp) {
+               if (fr_pair_append_by_da(local, &vp, &list, attr_crl_cdp_url) < 0) goto error;
+               if (fr_value_box_copy(vp, &vp->data, cdp) < 0) goto error;
        }
 
-       if (!fr_rb_insert(inst->mutable->crls, crl)) {
-               ERROR("Failed to insert CRL into tree of CRLs");
-               goto error;
+       if (base_crl) {
+               if (fr_pair_append_by_da(local, &vp, &list, attr_base_crl) < 0) goto error;
+               fr_value_box_strdup(vp, &vp->data, NULL, base_crl, false);
        }
-       crl->inst = inst;
 
-       /*
-        *      Check if this CRL has a Freshest CRL extension - the list of URIs to get deltas from
-        */
-       fr_value_box_list_init(&crl->delta_urls);
-       if (!base_crl && (dps = X509_CRL_get_ext_d2i(crl->crl, NID_freshest_crl, NULL, NULL))) {
-               DIST_POINT              *dp;
-               STACK_OF(GENERAL_NAME)  *names;
-               GENERAL_NAME            *name;
-               int                     j;
-               fr_value_box_t          *vb;
-
-               for (i = 0; i < sk_DIST_POINT_num(dps); i++) {
-                       dp = sk_DIST_POINT_value(dps, i);
-                       names = dp->distpoint->name.fullname;
-                       for (j = 0; j < sk_GENERAL_NAME_num(names); j++) {
-                               name = sk_GENERAL_NAME_value(names, j);
-                               if (name->type != GEN_URI) continue;
-                               MEM(vb = fr_value_box_alloc_null(crl));
-                               fr_value_box_bstrndup(vb, vb, NULL,
-                                                     (char const *)ASN1_STRING_get0_data(name->d.uniformResourceIdentifier),
-                                                     ASN1_STRING_length(name->d.uniformResourceIdentifier), true);
-                               DEBUG3("CRL references delta URI %pV", vb);
-                               fr_value_box_list_insert_tail(&crl->delta_urls, vb);
-                       }
-               }
-               CRL_DIST_POINTS_free(dps);
-       }
+       ret = fr_worker_to_coord_pair_send(t->cw, inst->coord_pair_reg, &list);
 
-       expiry_time = fr_time_delta_sub(fr_time_sub(fr_time_from_sec(next_update), now), inst->early_refresh);
-       if (base_crl && inst->force_delta_expiry_is_set) {
-               if (fr_time_delta_cmp(expiry_time, inst->force_delta_expiry)) expiry_time = inst->force_delta_expiry;
-       } else {
-               if (inst->force_expiry_is_set &&
-                   (fr_time_delta_cmp(expiry_time, inst->force_expiry) > 0)) expiry_time = inst->force_expiry;
-       }
+       talloc_free(local);
 
-       DEBUG3("CRL from %s will expire in %pVs", url, fr_box_time_delta(expiry_time));
-       if (fr_timer_in(crl, tl, &crl->ev, expiry_time, false, crl_expire, crl) <0) {
-               ERROR("Failed to set timer to expire CRL");
-       }
-
-       X509_STORE_CTX_free(verify_ctx);
-       return crl;
+       return ret;
 }
 
-static unlang_action_t CC_HINT(nonnull) crl_process_cdp_data(unlang_result_t *p_result, module_ctx_t const *mctx,
-                                                            request_t *request);
-
-/** Yield to a tmpl to retrieve CRL data
- *
- * @param request      the current request.
- * @param inst         module instance data.
- * @param thread       thread instance data.
- * @param env          the call_env for this module call.
- * @param rctx         the resume ctx for this module call.
- *
- * @returns
- *  - 1 - new tmpl pushed.
- *  - 0 - no tmpl pushed, soft fail.
- *  - -1 - no tmpl pushed, hard fail
- */
-static int crl_tmpl_yield(request_t *request, rlm_crl_t const *inst, rlm_crl_thread_t *thread, rlm_crl_env_t *env,
-                         rlm_crl_rctx_t *rctx)
+static void crl_by_url_cancel(module_ctx_t const *mctx, request_t *request, UNUSED fr_signal_t action)
 {
-       fr_pair_t       *vp;
-       tmpl_t          *vpt;
-
-       MEM(pair_update_request(&vp, attr_crl_cdp_url) >= 0);
-       MEM(fr_value_box_copy(vp, &vp->data, rctx->cdp_url) == 0);
-
-       if (strncmp(rctx->cdp_url->vb_strvalue, "http", 4) == 0) {
-               vpt = env->http_exp;
-       } else if (strncmp(rctx->cdp_url->vb_strvalue, "ldap", 4) == 0) {
-               if (!env->ldap_exp) {
-                       RWARN("CRL URI %pV requires LDAP, but the crl module ldap expansion is not configured", rctx->cdp_url);
-                       return 0;
-               }
-               vpt = env->ldap_exp;
-       } else if (strncmp(rctx->cdp_url->vb_strvalue, "ftp", 3) == 0) {
-               if (!env->ftp_exp) {
-                       RWARN("CRL URI %pV requires FTP, but the crl module ftp expansion is not configured", rctx->cdp_url);
-                       return 0;
-               }
-               vpt = env->ftp_exp;
-       } else {
-               RERROR("Unsupported URI scheme in CRL URI %pV", rctx->cdp_url);
-               return -1;
-       }
+       rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
+       crl_pending_t           find, *found;
 
-       trigger(unlang_interpret_get_thread_default(), inst->cs, NULL, "modules.crl.fetchuri", inst->trigger_rate_limit,
-               &request->request_pairs);
+       find = (crl_pending_t) {
+               .request = request
+       };
 
-       if (unlang_module_yield_to_tmpl(rctx, &rctx->crl_data, request, vpt,
-                                       NULL, crl_process_cdp_data, crl_signal, 0, rctx) < 0) return -1;
-       inst->mutable->fetching = thread;
-       return 1;
-}
+       found = fr_rb_find(&t->pending, &find);
+       if (!found) return;
 
-static unlang_action_t crl_by_url_start(unlang_result_t *p_result, module_ctx_t const *mctx, request_t *request);
+       fr_rb_remove(&t->pending, found);
+       talloc_free(found);
+}
 
-/** Process the response from evaluating the cdp_url -> crl_data expansion
- *
- * This is the resumption function when we yield to get CRL data associated with a URL
- */
-static unlang_action_t CC_HINT(nonnull) crl_process_cdp_data(unlang_result_t *p_result, module_ctx_t const *mctx,
-                                                            request_t *request)
+static unlang_action_t CC_HINT(nonnull) mod_crl_by_url(unlang_result_t *p_result, module_ctx_t const *mctx,
+                                                      request_t *request)
 {
-       rlm_crl_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t);
-       rlm_crl_thread_t *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
-       rlm_crl_env_t *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t);
-       rlm_crl_rctx_t *rctx = talloc_get_type_abort(mctx->rctx, rlm_crl_rctx_t);
-       crl_ret_t       ret = CRL_NOT_FOUND;
-       rlm_rcode_t     rcode = RLM_MODULE_FAIL;
-       crl_pending_t   *pending;
-
-       inst->mutable->fetching = NULL;
-       switch (fr_value_box_list_num_elements(&rctx->crl_data)) {
-       case 0:
-               REDEBUG("No CRL data returned from %pV, failing", rctx->cdp_url);
-               trigger(unlang_interpret_get_thread_default(), inst->cs, NULL, "modules.crl.fetchfail",
-                       inst->trigger_rate_limit, &request->request_pairs);
-       again:
-               talloc_free(rctx->cdp_url);
-
-               /*
-                *      If there are more URIs to try, push a new tmpl to expand.
-                */
-               rctx->cdp_url = fr_value_box_list_pop_head(&rctx->missing_crls);
-               if (rctx->cdp_url) {
-                       switch (crl_tmpl_yield(request, inst, t, env, rctx)) {
-                       case 0:
-                               goto again;
-                       case 1:
-                               return UNLANG_ACTION_PUSHED_CHILD;
-                       default:
-                               break;
-                       }
-               }
-       fail:
-               fr_value_box_list_talloc_free(&rctx->crl_data);
-               pair_delete_request(attr_crl_cdp_url);
-               goto finish;
-
-       case 1:
-       {
-               crl_entry_t *crl_entry;
-               fr_value_box_t  *crl_data = fr_value_box_list_pop_head(&rctx->crl_data);
-
-               crl_entry = crl_entry_create(inst, unlang_interpret_event_list(request)->tl,
-                                            rctx->cdp_url->vb_strvalue,
-                                            crl_data->vb_octets, rctx->base_crl);
-               talloc_free(crl_data);
-               if (!crl_entry) {
-                       RPERROR("Failed to process returned CRL data");
-                       trigger(unlang_interpret_get_thread_default(), inst->cs, NULL, "modules.crl.fetchbad",
-                               inst->trigger_rate_limit, &request->request_pairs);
-                       goto again;
-               }
-
-               /*
-                *      We've successfully loaded a URI - so we can clear the list of missing crls
-                *      This can then be re-used to hold missing delta CRLs if needed.
-                */
-               fr_value_box_list_talloc_free(&rctx->missing_crls);
-
-               if (fr_value_box_list_num_elements(&crl_entry->delta_urls) > 0) {
-                       crl_entry_t     *delta, find;
-                       fr_value_box_t  *vb = NULL, *delta_uri;
-
-                       rctx->status = CRL_CHECK_DELTA;
-                       while ((vb = fr_value_box_list_next(&crl_entry->delta_urls, vb))) {
-                               find.cdp_url = vb->vb_strvalue;
-                               delta = fr_rb_find(inst->mutable->crls, &find);
-                               if (delta) {
-                                       ret = crl_check_entry(delta, request, env->serial.vb_octets);
-                                       /*
-                                        *      The delta contained an entry for this serial - so this
-                                        *      is the return status.
-                                        */
-                                       if (ret != CRL_ENTRY_NOT_FOUND) break;
-                               } else {
-                                       delta_uri = fr_value_box_acopy(rctx, vb);
-                                       fr_value_box_list_insert_tail(&rctx->missing_crls, delta_uri);
-                               }
-                       }
+       rlm_crl_t const         *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t);
+       rlm_crl_env_t           *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t);
+       rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
+       fr_value_box_t          *cdp = NULL;
+       crl_entry_t             *found;
+       fr_value_box_list_t     missing;
+       fr_value_box_t          *uri;
+       char const              *base_url = NULL;
+       int                     ret;
+       crl_pending_t           *pending;
+       crl_fail_t              find, *fail;
 
-                       /*
-                        *      None of the delta CRL URIs were found, so go and get one.
-                        *      The list of URIs to fetch will now be in rctx->missing_crls
-                        */
-                       if (ret == CRL_NOT_FOUND) {
-                               rctx->status = CRL_CHECK_FETCH_DELTA;
-                               rctx->base_crl = crl_entry;
-                               goto again;
-                       }
-               }
+       fr_value_box_list_init(&missing);
 
-               if (rctx->status != CRL_CHECK_DELTA) ret = crl_check_entry(crl_entry, request, env->serial.vb_octets);
-       check_return:
-               switch (ret) {
+       while ((cdp = fr_value_box_list_next(env->cdp, cdp))) {
+               switch (crl_check_serial(&t->crls, request, cdp->vb_strvalue, env->serial.vb_octets, &found)) {
                case CRL_ENTRY_FOUND:
-                       rcode = RLM_MODULE_REJECT;
-                       goto finish;
+                       RETURN_UNLANG_REJECT;
 
                case CRL_ENTRY_NOT_FOUND:
-                       /*
-                        *      We have a CRL, but the serial is not in it.
-                        *
-                        *      If this was after fetching a delta, go check the base
-                        */
-                       if (rctx->status == CRL_CHECK_FETCH_DELTA) {
-                               RDEBUG3("Certificate not in delta CRL, checking base CRL");
-                               rctx->status = CRL_CHECK_BASE;
-                               ret = crl_check_entry(rctx->base_crl, request, env->serial.vb_octets);
-                               goto check_return;
-                       }
-                       FALL_THROUGH;
-
                case CRL_ENTRY_REMOVED:
-                       rcode = RLM_MODULE_OK;
-                       pair_delete_request(attr_crl_cdp_url);
-                       goto finish;
+                       RETURN_UNLANG_OK;
 
                case CRL_ERROR:
-                       goto fail;
+                       continue;
 
-               /*
-                *      This should never be returned by crl_check_entry because we provided the entry!
-                */
-               case CRL_MISSING_DELTA:
                case CRL_NOT_FOUND:
-                       fr_assert(0);
-                       goto fail;
-               }
+                       uri = fr_value_box_acopy(NULL, cdp);
+                       fr_value_box_list_insert_tail(&missing, uri);
+                       continue;
 
+               case CRL_MISSING_DELTA:
+                       fr_value_box_t  *vb = NULL;
+                       fr_value_box_list_talloc_free(&missing);
+                       while ((vb = fr_value_box_list_next(&found->delta_urls, vb))) {
+                               uri = fr_value_box_acopy(NULL, vb);
+                               fr_value_box_list_insert_tail(&missing, uri);
+                       }
+                       base_url = cdp->vb_strvalue;
+                       break;
+               }
        }
-               break;
 
-       default:
-               REDEBUG("Too many CRL values returned, failing");
-               goto fail;
+       if (fr_value_box_list_num_elements(&missing) == 0) RETURN_UNLANG_FAIL;
+
+       find = (crl_fail_t) {
+               .cdp_url = fr_value_box_list_head(&missing)->vb_strvalue
+       };
+
+       /*
+        *      Check to see if the missing CRL has failed to be fetched
+        *      recently.  If it has, within the retry delay time, then
+        *      fail this request.
+        */
+       fail = fr_rb_find(&t->fails, &find);
+       if (fail) {
+               if (fr_time_gt(fr_time_add(fail->fail_time, inst->retry_delay), fr_time())) {
+                       fr_value_box_list_talloc_free(&missing);
+                       RETURN_UNLANG_FAIL;
+               }
+
+               fr_rb_delete(&t->fails, fail);
        }
 
-finish:
-       pthread_mutex_unlock(&inst->mutable->mutex);
-       pending = fr_rb_first(&t->pending);
-       if (pending) unlang_interpret_mark_runnable(pending->request);
-       RETURN_UNLANG_RCODE(rcode);
-}
+       ret = crl_fetch_start(inst, t, &missing, base_url);
 
-static unlang_action_t CC_HINT(nonnull(1,2,3,4,6)) crl_by_url(unlang_result_t *p_result, rlm_crl_t const *inst,
-                                                             rlm_crl_thread_t *t, rlm_crl_env_t *env,
-                                                             rlm_crl_rctx_t *rctx, request_t *request)
-{
-       rlm_rcode_t     rcode = RLM_MODULE_NOOP;
-       crl_entry_t     *found;
+       fr_value_box_list_talloc_free(&missing);
 
-       if (!rctx) rctx = talloc_zero(unlang_interpret_frame_talloc_ctx(request), rlm_crl_rctx_t);
-       fr_value_box_list_init(&rctx->missing_crls);
+       if (ret < 0) RETURN_UNLANG_FAIL;
 
-       pthread_mutex_lock(&inst->mutable->mutex);
+       if (unlang_module_yield(request, mod_crl_by_url, crl_by_url_cancel,
+                               ~FR_SIGNAL_CANCEL, mctx->rctx) != UNLANG_ACTION_YIELD) RETURN_UNLANG_FAIL;
 
-       /*
-        *      Fast path when we have a CRL.
-        *      All distribution points are considered equivalent, so check if
-        *      if we have any of them before attempting to fetch missing ones.
-        */
-       while ((rctx->cdp_url = fr_value_box_list_pop_head(env->cdp))) {
-               switch (crl_check_serial(inst->mutable->crls, request, rctx->cdp_url->vb_strvalue,
-                                        env->serial.vb_octets, &found)) {
-               case CRL_ENTRY_FOUND:
-                       rcode = RLM_MODULE_REJECT;
-                       break;
+       MEM(pending = talloc_zero(t, crl_pending_t));
+       pending->request = request;
 
-               case CRL_ENTRY_NOT_FOUND:
-               case CRL_ENTRY_REMOVED:
-                       rcode = RLM_MODULE_OK;
-                       break;
+       if (!fr_rb_insert(&t->pending, pending)) {
+               talloc_free(pending);
+               RETURN_UNLANG_FAIL;
+       }
 
-               case CRL_ERROR:
-                       continue;
+       RDEBUG3("Yielding request until CRL fetching completed");
 
-               case CRL_NOT_FOUND:
-                       fr_value_box_list_insert_tail(&rctx->missing_crls, rctx->cdp_url);
-                       rcode = RLM_MODULE_NOTFOUND;
-                       continue;
+       return UNLANG_ACTION_YIELD;
+}
 
-               case CRL_MISSING_DELTA:
-               {
-                       /*
-                        *      We found a base CRL, but it has a delta which
-                        *      was not found.  Populate the "missing" list with
-                        *      the CDP for the delta and go get it.
-                        */
-                       fr_value_box_t  *vb = NULL, *delta_uri;
-                       rctx->base_crl = found;
-                       rctx->status = CRL_CHECK_FETCH_DELTA;
-                       fr_value_box_list_talloc_free(&rctx->missing_crls);
-                       while ((vb = fr_value_box_list_next(&found->delta_urls, vb))) {
-                               delta_uri = fr_value_box_acopy(rctx, vb);
-                               fr_value_box_list_insert_tail(&rctx->missing_crls, delta_uri);
-                       }
-                       goto fetch_missing;
-               }
-               }
+/** Resume requests waiting for a CRL fetch.
+ */
+static void crl_pending_resume(rlm_crl_thread_t *thread)
+{
+       crl_pending_t           *pending;
+       fr_rb_iter_inorder_t    iter;
+
+       for (pending = fr_rb_iter_init_inorder(&thread->pending, &iter);
+            pending;
+            pending = fr_rb_iter_next_inorder(&thread->pending, &iter)) {
+               fr_rb_iter_delete_inorder(&thread->pending, &iter);
+               unlang_interpret_mark_runnable(pending->request);
+               talloc_free(pending);
+       }
+}
+
+/** Callback for worker receiving Fetch-OK packet from coordinator
+ */
+static void recv_crl_ok(UNUSED fr_coord_worker_t *cw, UNUSED fr_coord_pair_reg_t *coord_pair_reg,
+                         fr_pair_list_t const *list, UNUSED fr_time_t now,
+                         module_ctx_t *mctx, UNUSED void *uctx)
+{
+       fr_pair_t               *url, *crl, *delta = NULL;
+       crl_entry_t             *crl_entry, find;
+       rlm_crl_thread_t        *thread = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
+       uint8_t const           *data;
+
+       url = fr_pair_find_by_da(list, NULL, attr_crl_cdp_url);
+       if (!url) {
+               ERROR("Missing URL");
+               return;
        }
 
-       if (rcode != RLM_MODULE_NOTFOUND) {
-               crl_pending_t *pending;
-       finish:
-               pthread_mutex_unlock(&inst->mutable->mutex);
+       find = (crl_entry_t) {
+               .cdp_url = url->vp_strvalue
+       };
 
-               pending = fr_rb_first(&t->pending);
-               if (pending) unlang_interpret_mark_runnable(pending->request);
+       crl_entry = fr_rb_find(&thread->crls, &find);
 
-               RETURN_UNLANG_RCODE(rcode);
+       crl = fr_pair_find_by_da(list, NULL, attr_crl_data);
+       if (!crl) {
+               ERROR("No CRL data");
+               return;
        }
 
        /*
-        *      Need to convert a missing cdp_url to a CRL entry
-        *
-        *      We yield to an expansion to allow this to happen, then parse the CRL data
-        *      and check if the serial has an entry in the CRL.
+        *      If this CRL didn't previously exist, create the entry
         */
-fetch_missing:
-       fr_value_box_list_init(&rctx->crl_data);
+       if (!crl_entry) {
+               MEM(crl_entry = talloc_zero(thread, crl_entry_t));
+               crl_entry->cdp_url = talloc_strdup(crl_entry, url->vp_strvalue);
+               fr_value_box_list_init(&crl_entry->delta_urls);
+               if (!fr_rb_insert(&thread->crls, crl_entry)) {
+                       talloc_free(crl_entry);
+                       return;
+               }
+               talloc_set_destructor(crl_entry, _crl_entry_free);
+       } else {
+               X509_CRL_free(crl_entry->crl);
+               fr_value_box_list_talloc_free(&crl_entry->delta_urls);
+       }
 
-again:
-       rctx->cdp_url = fr_value_box_list_pop_head(&rctx->missing_crls);
+       data = crl->vp_octets;
+       crl_entry->crl = d2i_X509_CRL(NULL, (const unsigned char **)&data, crl->vp_size);
+       if (unlikely(!crl_entry->crl)) {
+               ERROR("Failed to parse CRL");
+       error:
+               fr_rb_remove(&thread->crls, crl_entry);
+               talloc_free(crl_entry);
+       }
 
-       switch (crl_tmpl_yield(request, inst, t, env, rctx)) {
-       case 0:
-               goto again;
-       case 1:
-               /*
-                *      The lock is released after the pushed tmpl result is handled
-                */
-               /* coverity[missing_unlock] */
-               return UNLANG_ACTION_PUSHED_CHILD;
-       default:
-               rcode = RLM_MODULE_FAIL;
-               goto finish;
+       DEBUG3("CRL %pP refreshed", crl);
+
+       while ((delta = fr_pair_find_by_da(list, delta, attr_delta_crl))) {
+               fr_value_box_t  *vb;
+               MEM(vb = fr_value_box_alloc_null(crl_entry));
+               if (unlikely(fr_value_box_copy(vb, vb, &delta->data) < 0)) goto error;
+               fr_value_box_list_insert_tail(&crl_entry->delta_urls, vb);
        }
+
+       crl_pending_resume(thread);
 }
 
-static unlang_action_t CC_HINT(nonnull) crl_by_url_resume(unlang_result_t *p_result, module_ctx_t const *mctx, request_t *request)
+/** Callback for worker receiving Fetch-Fail packet from coordinator
+ */
+static void recv_crl_fail(UNUSED fr_coord_worker_t *cw, UNUSED fr_coord_pair_reg_t *coord_pair_reg,
+                         fr_pair_list_t const *list, fr_time_t now, module_ctx_t *mctx, UNUSED void *uctx)
 {
-       rlm_crl_t const         *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t);
-       rlm_crl_env_t           *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t);
-       rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
-       crl_pending_t           find, *found;
+       fr_pair_t               *vp;
+       rlm_crl_thread_t        *thread = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
 
-       find.request = request;
-       found = fr_rb_find(&t->pending, &find);
-       if (!found) RETURN_UNLANG_NOOP;
+       /*
+        *      Record the URL of the fetch that failed.
+        */
+       vp = fr_pair_find_by_da(list, NULL, attr_crl_cdp_url);
+       if (vp) {
+               crl_fail_t *fail;
+
+               MEM(fail = talloc_zero(thread, crl_fail_t));
+               fail->cdp_url = talloc_strdup(fail, vp->vp_strvalue);
+               fail->fail_time = now;
 
-       fr_rb_delete(&t->pending, found);
-       return crl_by_url(p_result, inst, t, env, mctx->rctx, request);
+               if (unlikely(!fr_rb_insert(&thread->fails, fail))) {
+                       talloc_free(fail);
+               }
+       }
+
+       crl_pending_resume(thread);
 }
 
-static void crl_by_url_cancel(module_ctx_t const *mctx, request_t *request, UNUSED fr_signal_t action)
+static int mod_thread_instantiate(module_thread_inst_ctx_t const *mctx)
 {
        rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
-       crl_pending_t           *found, find;
 
-       find.request = request;
-       found = fr_rb_find(&t->pending, &find);
-       if (!found) return;
+       fr_rb_inline_init(&t->pending, crl_pending_t, node, crl_pending_cmp, NULL);
+       fr_rb_inline_init(&t->crls, crl_entry_t, node, crl_cmp, NULL);
+       fr_rb_inline_init(&t->fails, crl_fail_t, node, crl_fail_cmp, NULL);
 
-       fr_rb_delete(&t->pending, found);
+       return 0;
 }
 
-static unlang_action_t CC_HINT(nonnull) crl_by_url_start(unlang_result_t *p_result, module_ctx_t const *mctx,
-                                                       request_t *request)
+static int mod_coord_attach(module_thread_inst_ctx_t const *mctx)
 {
-       rlm_crl_t const         *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t);
-       rlm_crl_env_t           *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t);
        rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
-       crl_pending_t           *pending;
+       rlm_crl_t               *inst = talloc_get_type_abort(mctx->mi->data, rlm_crl_t);
 
-       if (fr_value_box_list_num_elements(env->cdp) == 0) RETURN_UNLANG_NOOP;
+       t->cw = fr_coord_attach(t, mctx->el, inst->coord_reg);
 
-       if (inst->mutable->fetching != t) return crl_by_url(p_result, inst, t, env, mctx->rctx, request);
-
-       MEM(pending = talloc_zero(t, crl_pending_t));
-       pending->request = request;
+       if (!t->cw) {
+               ERROR("Failed to attach to coordinator");
+               return -1;
+       }
 
-       fr_rb_insert(&t->pending, pending);
-       RDEBUG3("Yielding request until CRL fetching completed");
-       return unlang_module_yield(request, crl_by_url_resume, crl_by_url_cancel, ~FR_SIGNAL_CANCEL, mctx->rctx);
+       return 0;
 }
 
-static int mod_thread_instantiate(module_thread_inst_ctx_t const *mctx)
+static int mod_thread_detach(module_thread_inst_ctx_t const *mctx)
 {
        rlm_crl_thread_t        *t = talloc_get_type_abort(mctx->thread, rlm_crl_thread_t);
 
-       fr_rb_inline_init(&t->pending, crl_pending_t, node, crl_pending_cmp, NULL);
-
-       return 0;
-}
+       if (!t->cw) return 0;
 
-static int mod_mutable_free(rlm_crl_mutable_t *mutable)
-{
-       pthread_mutex_destroy(&mutable->mutex);
+       fr_coord_detach(t->cw, true);
+       t->cw = NULL;
        return 0;
 }
 
@@ -825,10 +540,25 @@ static int mod_detach(module_detach_ctx_t const *mctx)
 {
        rlm_crl_t       *inst = talloc_get_type_abort(mctx->mi->data, rlm_crl_t);
 
-       if (inst->verify_store) X509_STORE_free(inst->verify_store);
-       talloc_free(inst->mutable);
+       fr_coord_deregister(inst->coord_reg);
        return 0;
 }
+
+static fr_coord_cb_reg_t coord_callbacks[] = {
+       FR_COORD_PAIR_CALLBACK(CRL_COORD_PAIR_CALLBACK_ID),
+       FR_COORD_CALLBACK_TERMINATOR
+};
+
+static fr_coord_worker_cb_reg_t worker_callbacks[] = {
+       FR_COORD_WORKER_PAIR_CALLBACK(CRL_COORD_PAIR_CALLBACK_ID),
+       FR_COORD_CALLBACK_TERMINATOR
+};
+
+static fr_coord_worker_pair_cb_reg_t worker_pair_callbacks[] = {
+       { .packet_type = FR_CRL_FETCH_OK, .callback = recv_crl_ok },
+       { .packet_type = FR_CRL_FETCH_FAIL, .callback = recv_crl_fail },
+       FR_COORD_CALLBACK_TERMINATOR
+};
 #endif
 
 /**    Instantiate the module
@@ -839,28 +569,27 @@ static int mod_instantiate(module_inst_ctx_t const *mctx)
 #ifdef WITH_TLS
        rlm_crl_t       *inst = talloc_get_type_abort(mctx->mi->data, rlm_crl_t);
 
-       MEM(inst->mutable = talloc_zero(NULL, rlm_crl_mutable_t));
-       MEM(inst->mutable->crls = fr_rb_inline_talloc_alloc(inst->mutable, crl_entry_t, node, crl_cmp, crl_free));
-       pthread_mutex_init(&inst->mutable->mutex, NULL);
-       talloc_set_destructor(inst->mutable, mod_mutable_free);
-
-       if (!inst->ca_file && !inst->ca_path) {
-               cf_log_err(mctx->mi->conf, "Missing ca_file / ca_path option.  One or other (or both) must be specified.");
-       fail:
-               talloc_free(inst->mutable);
-               return -1;
-       }
+       inst->coord_pair_reg = fr_coord_pair_register(inst,
+               &(fr_coord_pair_reg_ctx_t) {
+                       .worker_cb = worker_pair_callbacks,
+                       .cb_id = CRL_COORD_PAIR_CALLBACK_ID,
+                       .root = fr_dict_root(dict_crl),
+                       .cs = mctx->mi->conf,
+               }
+       );
+       if (!inst->coord_pair_reg) return -1;
 
-       inst->verify_store = X509_STORE_new();
-       if (!X509_STORE_load_locations(inst->verify_store, inst->ca_file, inst->ca_path)) {
-               cf_log_err(mctx->mi->conf, "Failed reading Trusted root CA file \"%s\" and path \"%s\"",
-                          inst->ca_file, inst->ca_path);
-               goto fail;
-       }
+       FR_COORD_PAIR_CB_CTX_SET(coord_callbacks, worker_callbacks, inst->coord_pair_reg);
 
-       X509_STORE_set_purpose(inst->verify_store, X509_PURPOSE_SSL_CLIENT);
+       inst->coord_reg = fr_coord_register(inst,
+               &(fr_coord_reg_ctx_t) {
+                       .name = mctx->mi->name,
+                       .coord_cb = coord_callbacks,
+                       .worker_cb = worker_callbacks,
+                       .mi = mctx->mi
+               });
 
-       inst->cs = mctx->mi->conf;
+       if (!inst->coord_reg) return -1;
 
        return 0;
 #else
@@ -880,13 +609,15 @@ module_rlm_t rlm_crl = {
                MODULE_THREAD_INST(rlm_crl_thread_t),
 #ifdef WITH_TLS
                .thread_instantiate     = mod_thread_instantiate,
+               .thread_detach          = mod_thread_detach,
+               .coord_attach           = mod_coord_attach,
                .detach         = mod_detach,
 #endif
        },
 #ifdef WITH_TLS
        .method_group = {
                .bindings = (module_method_binding_t[]){
-                       { .section = SECTION_NAME(CF_IDENT_ANY, CF_IDENT_ANY), .method = crl_by_url_start, .method_env = &crl_env },
+                       { .section = SECTION_NAME(CF_IDENT_ANY, CF_IDENT_ANY), .method = mod_crl_by_url, .method_env = &crl_env },
                        MODULE_BINDING_TERMINATOR
                }
        }