summaryrefslogtreecommitdiffstats
path: root/modules/md/md_tailscale.c
diff options
context:
space:
mode:
Diffstat (limited to 'modules/md/md_tailscale.c')
-rw-r--r--modules/md/md_tailscale.c383
1 files changed, 383 insertions, 0 deletions
diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c
new file mode 100644
index 0000000..c8d2bad
--- /dev/null
+++ b/modules/md/md_tailscale.c
@@ -0,0 +1,383 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_tailscale.h"
+
+typedef struct {
+ apr_pool_t *pool;
+ md_proto_driver_t *driver;
+ const char *unix_socket_path;
+ md_t *md;
+ apr_array_header_t *chain;
+ md_pkey_t *pkey;
+} ts_ctx_t;
+
+static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result)
+{
+ ts_ctx_t *ts_ctx;
+ apr_uri_t uri;
+ const char *ca_url;
+ apr_status_t rv = APR_SUCCESS;
+
+ md_result_set(result, APR_SUCCESS, NULL);
+ ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx));
+ ts_ctx->pool = d->p;
+ ts_ctx->driver = d;
+ ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *));
+
+ 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;
+ }
+ rv = apr_uri_parse(d->p, ca_url, &uri);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url);
+ goto leave;
+ }
+ if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ ts_ctx->unix_socket_path = uri.path;
+ d->baton = ts_ctx;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result)
+{
+ return ts_init(d, result);
+}
+
+static apr_status_t ts_preload(md_proto_driver_t *d,
+ md_store_group_t load_group, md_result_t *result)
+{
+ apr_status_t rv;
+ md_t *md;
+ md_credentials_t *creds;
+ md_pkey_spec_t *pkspec;
+ apr_array_header_t *all_creds;
+ const char *name;
+ int i;
+
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
+ /* Load data from MD_SG_STAGING and save it into "load_group".
+ */
+ if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) {
+ md_result_set(result, rv, "loading staged md.json");
+ goto leave;
+ }
+
+ /* tailscale generates one cert+key with key specification being whatever
+ * it chooses. Use the NULL spec here.
+ */
+ all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*));
+ pkspec = NULL;
+ if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) {
+ md_result_printf(result, rv, "loading staged credentials");
+ goto leave;
+ }
+ if (!creds->chain) {
+ rv = APR_ENOENT;
+ md_result_printf(result, rv, "no certificate in staged credentials");
+ goto leave;
+ }
+ if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) {
+ md_result_printf(result, rv, "certificate and private key do not match in staged credentials");
+ goto leave;
+ }
+ APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds;
+
+ md_result_activity_setn(result, "purging store tmp space");
+ rv = md_store_purge(d->store, d->p, load_group, name);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, NULL);
+ goto leave;
+ }
+
+ md_result_activity_setn(result, "saving staged md/privkey/pubcert");
+ if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
+ md_result_set(result, rv, "writing md.json");
+ goto leave;
+ }
+
+ for (i = 0; i < all_creds->nelts; ++i) {
+ creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*);
+ if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) {
+ md_result_printf(result, rv, "writing credentials #%d", i);
+ goto leave;
+ }
+ }
+
+ md_result_set(result, APR_SUCCESS, "saved staged data successfully");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t rv_of_response(const md_http_response_t *res)
+{
+ switch (res->status) {
+ case 200:
+ return APR_SUCCESS;
+ case 400:
+ return APR_EINVAL;
+ case 401: /* sectigo returns this instead of 403 */
+ case 403:
+ return APR_EACCES;
+ case 404:
+ return APR_ENOENT;
+ default:
+ return APR_EGENERAL;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t on_get_cert(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ apr_array_clear(ts_ctx->chain);
+ rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t on_get_key(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result)
+{
+ const char *name, *domain, *url;
+ apr_status_t rv = APR_ENOENT;
+ ts_ctx_t *ts_ctx = d->baton;
+ md_http_t *http;
+ const md_pubcert_t *pubcert;
+ md_cert_t *old_cert, *new_cert;
+ int reset_staging = d->reset;
+
+ /* "renewing" the certificate from tailscale. Since tailscale has its
+ * own ideas on when to do this, we can only inspect the certificate
+ * it gives us and see if it is different from the current one we have.
+ * (if we have any. first time, lacking a cert, any it gives us is
+ * considered as 'renewed'.)
+ */
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name);
+
+ /* When not explicitly told to reset, we check the existing data. If
+ * it is incomplete or old, we trigger the reset for a clean start. */
+ if (!reset_staging) {
+ md_result_activity_setn(result, "Checking staging area");
+ rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->md, d->p);
+ if (APR_SUCCESS == rv) {
+ /* So, we have a copy in staging, but is it a recent or an old one? */
+ if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
+ reset_staging = 1;
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ reset_staging = 1;
+ rv = APR_SUCCESS;
+ }
+ }
+
+ 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", d->md->name);
+ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
+ md_result_printf(result, rv, "resetting staging area");
+ goto leave;
+ }
+ rv = APR_SUCCESS;
+ ts_ctx->md = NULL;
+ }
+
+ 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);
+ md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+ ts_ctx->md = md_copy(d->p, d->md);
+ rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Saving MD information in staging area.");
+ md_result_log(result, MD_LOG_ERR);
+ goto leave;
+ }
+ }
+
+ if (!ts_ctx->unix_socket_path) {
+ rv = APR_ENOTIMPL;
+ md_result_set(result, rv, "only unix sockets are supported for tailscale connections");
+ goto leave;
+ }
+
+ rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "tailscale socket not available, may not be up: %s",
+ ts_ctx->unix_socket_path);
+ goto leave;
+ }
+
+ rv = md_http_create(&http, d->p,
+ apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION),
+ NULL);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "creating http context");
+ goto leave;
+ }
+ md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path);
+
+ domain = (d->md->domains->nelts > 0)?
+ APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL;
+ if (!domain) {
+ rv = APR_EINVAL;
+ md_result_set(result, rv, "no domain names available");
+ }
+
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving certificate from tailscale");
+ goto leave;
+ }
+ if (ts_ctx->chain->nelts <= 0) {
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale returned no certificates");
+ goto leave;
+ }
+
+ /* Got the key and the chain, is it new? */
+ rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p);
+ if (APR_SUCCESS == rv) {
+ old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*);
+ new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*);
+ if (md_certs_are_equal(old_cert, new_cert)) {
+ /* tailscale has not renewed the certificate, yet */
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale has not renewed the certificate yet");
+ /* let's check this daily */
+ md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY));
+ goto leave;
+ }
+ }
+
+ /* We have a new certificate (or had none before).
+ * Get the key and store both in STAGING.
+ */
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving key from tailscale");
+ goto leave;
+ }
+
+ rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "saving private key");
+ goto leave;
+ }
+
+ rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name,
+ NULL, ts_ctx->chain, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "saving new certificate chain.");
+ goto leave;
+ }
+
+ md_result_set(result, APR_SUCCESS,
+ "A new tailscale certificate has been retrieved successfully and can "
+ "be used. A graceful server restart is recommended.");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p)
+{
+ (void)p;
+ 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;
+}
+
+
+static md_proto_t TAILSCALE_PROTO = {
+ MD_PROTO_TAILSCALE, ts_init, ts_renew,
+ ts_preload_init, ts_preload, ts_complete_md,
+};
+
+apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p)
+{
+ (void)p;
+ apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO);
+ return APR_SUCCESS;
+}