diff --git a/include/fluent-bit/tls/flb_tls.h b/include/fluent-bit/tls/flb_tls.h index 018231218e2..3492dcad861 100644 --- a/include/fluent-bit/tls/flb_tls.h +++ b/include/fluent-bit/tls/flb_tls.h @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #define FLB_TLS_ALPN_MAX_LENGTH 16 @@ -50,6 +52,13 @@ struct flb_tls; struct flb_connection; +struct flb_tls_file_status { + int exists; + uint64_t size; + uint64_t mtime; + uint64_t ctime; +}; + struct flb_tls_session { /* opaque data type for backend session context */ void *ptr; @@ -71,6 +80,9 @@ struct flb_tls_backend { const char *, const char *, const char *, const char *); + /* reload backend context */ + int (*context_reload) (struct flb_tls *); + /* destroy backend context */ void (*context_destroy) (void *); @@ -108,16 +120,32 @@ struct flb_tls { int verify_client; /* Verify client certificate */ int debug; /* Debug level */ char *vhost; /* Virtual hostname for SNI */ + char *ca_path; /* Path to certificates */ + char *ca_file; /* CA root cert */ + char *crt_file; /* Certificate */ + char *key_file; /* Cert Key */ + char *key_passwd; /* Cert Key Password */ + char *alpn; /* ALPN protocol list */ + char *min_version; /* Minimum TLS version */ + char *max_version; /* Maximum TLS version */ + char *ciphers; /* TLS ciphers */ + struct flb_tls_file_status ca_path_status; + struct flb_tls_file_status ca_file_status; + struct flb_tls_file_status crt_file_status; + struct flb_tls_file_status key_file_status; int mode; /* Client or Server */ int verify_hostname; /* Verify hostname */ + int system_certificates_loaded; /* System certs loaded */ #if defined(FLB_SYSTEM_WINDOWS) char *certstore_name; /* Windows CertStore Name */ int use_enterprise_store; /* Use Enterprise store or not */ + char *client_thumbprints; /* Allowed client thumbprints */ #endif /* Bakend library for TLS */ void *ctx; /* TLS context created */ struct flb_tls_backend *api; /* backend API */ + pthread_mutex_t reload_mutex; /* protects reload state */ }; int flb_tls_init(); @@ -132,6 +160,8 @@ struct flb_tls *flb_tls_create(int mode, int flb_tls_destroy(struct flb_tls *tls); +int flb_tls_reload_if_needed(struct flb_tls *tls); + int flb_tls_set_alpn(struct flb_tls *tls, const char *alpn); int flb_tls_set_verify_client(struct flb_tls *tls, int verify_client); diff --git a/src/tls/flb_tls.c b/src/tls/flb_tls.c index 40961b6a12a..3db717d22ef 100644 --- a/src/tls/flb_tls.c +++ b/src/tls/flb_tls.c @@ -20,6 +20,9 @@ #include #include #include +#include + +#include #include "openssl.c" @@ -182,7 +185,124 @@ static inline int io_tls_event_switch(struct flb_tls_session *session, int flb_tls_load_system_certificates(struct flb_tls *tls) { - return load_system_certificates(tls->ctx); + int ret; + + ret = load_system_certificates(tls->ctx); + if (ret == 0) { + tls->system_certificates_loaded = FLB_TRUE; + } + + return ret; +} + +static int tls_file_status_get(const char *path, + struct flb_tls_file_status *status) +{ + struct stat st; + + memset(status, 0, sizeof(struct flb_tls_file_status)); + + if (path == NULL) { + return 0; + } + + if (stat(path, &st) != 0) { + status->exists = FLB_FALSE; + return -1; + } + + status->exists = FLB_TRUE; + status->size = (uint64_t) st.st_size; + status->mtime = (uint64_t) st.st_mtime; + status->ctime = (uint64_t) st.st_ctime; + + return 0; +} + +static int tls_file_status_changed(const char *path, + struct flb_tls_file_status *status) +{ + struct flb_tls_file_status current; + + if (path == NULL) { + return FLB_FALSE; + } + + tls_file_status_get(path, ¤t); + + if (current.exists != status->exists || + current.size != status->size || + current.mtime != status->mtime || + current.ctime != status->ctime) { + return FLB_TRUE; + } + + return FLB_FALSE; +} + +static void tls_file_status_refresh(struct flb_tls *tls) +{ + tls_file_status_get(tls->ca_path, &tls->ca_path_status); + tls_file_status_get(tls->ca_file, &tls->ca_file_status); + tls_file_status_get(tls->crt_file, &tls->crt_file_status); + tls_file_status_get(tls->key_file, &tls->key_file_status); +} + +static int tls_file_status_has_changed(struct flb_tls *tls) +{ + if (tls_file_status_changed(tls->ca_path, &tls->ca_path_status) == FLB_TRUE || + tls_file_status_changed(tls->ca_file, &tls->ca_file_status) == FLB_TRUE || + tls_file_status_changed(tls->crt_file, &tls->crt_file_status) == FLB_TRUE || + tls_file_status_changed(tls->key_file, &tls->key_file_status) == FLB_TRUE) { + return FLB_TRUE; + } + + return FLB_FALSE; +} + +static int tls_should_reload_context(struct flb_tls *tls) +{ + if (tls_file_status_has_changed(tls) == FLB_TRUE) { + return FLB_TRUE; + } + +#if defined(FLB_SYSTEM_WINDOWS) || defined(FLB_SYSTEM_MACOS) + /* + * macOS Keychain and Windows CertStore do not expose a portable file + * metadata handle for us to watch. Refresh store-backed contexts before + * each new TLS session so rotations/imports become visible without a + * process restart. + */ + if (tls->system_certificates_loaded == FLB_TRUE) { + return FLB_TRUE; + } +#endif + + return FLB_FALSE; +} + +static int tls_store_string(char **slot, const char *value) +{ + char *tmp; + + if (*slot != NULL) { + flb_free(*slot); + *slot = NULL; + } + + if (value == NULL) { + return 0; + } + + tmp = flb_strdup(value); + if (tmp == NULL) { + flb_errno(); + return -1; + } + + *slot = tmp; + + return 0; } struct flb_tls *flb_tls_create(int mode, @@ -217,30 +337,88 @@ struct flb_tls *flb_tls_create(int mode, return NULL; } + tls->ctx = backend; + tls->api = &tls_openssl; + pthread_mutex_init(&tls->reload_mutex, NULL); + tls->verify = verify; tls->debug = debug; tls->mode = mode; tls->verify_hostname = FLB_FALSE; + tls->system_certificates_loaded = FLB_FALSE; +#if defined(FLB_SYSTEM_WINDOWS) || defined(FLB_SYSTEM_MACOS) + if (ca_path == NULL && ca_file == NULL && mode == FLB_TLS_CLIENT_MODE) { + tls->system_certificates_loaded = FLB_TRUE; + } +#endif #if defined(FLB_SYSTEM_WINDOWS) tls->certstore_name = NULL; tls->use_enterprise_store = FLB_FALSE; + tls->client_thumbprints = NULL; #endif - if (vhost != NULL) { - tls->vhost = flb_strdup(vhost); + if (tls_store_string(&tls->vhost, vhost) != 0 || + tls_store_string(&tls->ca_path, ca_path) != 0 || + tls_store_string(&tls->ca_file, ca_file) != 0 || + tls_store_string(&tls->crt_file, crt_file) != 0 || + tls_store_string(&tls->key_file, key_file) != 0 || + tls_store_string(&tls->key_passwd, key_passwd) != 0) { + flb_tls_destroy(tls); + return NULL; } - tls->ctx = backend; - tls->api = &tls_openssl; + tls_file_status_refresh(tls); return tls; } +int flb_tls_reload_if_needed(struct flb_tls *tls) +{ + int ret; + + if (tls == NULL || tls->ctx == NULL || tls->api == NULL || + tls->api->context_reload == NULL) { + return 0; + } + + pthread_mutex_lock(&tls->reload_mutex); + + if (tls_should_reload_context(tls) == FLB_FALSE) { + pthread_mutex_unlock(&tls->reload_mutex); + return 0; + } + + ret = tls->api->context_reload(tls); + if (ret != 0) { + pthread_mutex_unlock(&tls->reload_mutex); + flb_error("[tls] detected certificate file changes but reload failed"); + return -1; + } + + tls_file_status_refresh(tls); + pthread_mutex_unlock(&tls->reload_mutex); + flb_info("[tls] reloaded TLS certificate configuration"); + + return 1; +} + int flb_tls_set_minmax_proto(struct flb_tls *tls, const char *min_version, const char *max_version) { + int ret; + if (tls->ctx) { - return tls->api->set_minmax_proto(tls, min_version, max_version); + ret = tls->api->set_minmax_proto(tls, min_version, max_version); + if (ret != 0) { + return ret; + } + + if (tls_store_string(&tls->min_version, min_version) != 0 || + tls_store_string(&tls->max_version, max_version) != 0) { + return -1; + } + + return ret; } return 0; @@ -248,8 +426,19 @@ int flb_tls_set_minmax_proto(struct flb_tls *tls, int flb_tls_set_ciphers(struct flb_tls *tls, const char *ciphers) { + int ret; + if (tls->ctx) { - return tls->api->set_ciphers(tls, ciphers); + ret = tls->api->set_ciphers(tls, ciphers); + if (ret != 0) { + return ret; + } + + if (tls_store_string(&tls->ciphers, ciphers) != 0) { + return -1; + } + + return ret; } return 0; @@ -269,13 +458,45 @@ int flb_tls_destroy(struct flb_tls *tls) if (tls->vhost != NULL) { flb_free(tls->vhost); } + if (tls->ca_path != NULL) { + flb_free(tls->ca_path); + } + if (tls->ca_file != NULL) { + flb_free(tls->ca_file); + } + if (tls->crt_file != NULL) { + flb_free(tls->crt_file); + } + if (tls->key_file != NULL) { + flb_free(tls->key_file); + } + if (tls->key_passwd != NULL) { + flb_free(tls->key_passwd); + } + if (tls->alpn != NULL) { + flb_free(tls->alpn); + } + if (tls->min_version != NULL) { + flb_free(tls->min_version); + } + if (tls->max_version != NULL) { + flb_free(tls->max_version); + } + if (tls->ciphers != NULL) { + flb_free(tls->ciphers); + } #if defined(FLB_SYSTEM_WINDOWS) if (tls->certstore_name) { flb_free(tls->certstore_name); } + if (tls->client_thumbprints) { + flb_free(tls->client_thumbprints); + } #endif + pthread_mutex_destroy(&tls->reload_mutex); + flb_free(tls); return 0; @@ -283,8 +504,19 @@ int flb_tls_destroy(struct flb_tls *tls) int flb_tls_set_alpn(struct flb_tls *tls, const char *alpn) { + int ret; + if (tls->ctx) { - return tls->api->context_alpn_set(tls->ctx, alpn); + ret = tls->api->context_alpn_set(tls->ctx, alpn); + if (ret != 0) { + return ret; + } + + if (tls_store_string(&tls->alpn, alpn) != 0) { + return -1; + } + + return ret; } return 0; @@ -297,6 +529,11 @@ int flb_tls_set_verify_client(struct flb_tls *tls, int verify_client) } tls->verify_client = verify_client; +#if defined(FLB_SYSTEM_WINDOWS) || defined(FLB_SYSTEM_MACOS) + if (verify_client == FLB_TRUE && tls->ca_path == NULL && tls->ca_file == NULL) { + tls->system_certificates_loaded = FLB_TRUE; + } +#endif if (tls->ctx && tls->api->context_set_verify_client) { return tls->api->context_set_verify_client(tls->ctx, verify_client); @@ -319,8 +556,19 @@ int flb_tls_set_verify_hostname(struct flb_tls *tls, int verify_hostname) #if defined(FLB_SYSTEM_WINDOWS) int flb_tls_set_certstore_name(struct flb_tls *tls, const char *certstore_name) { + int ret; + if (tls) { - return tls->api->set_certstore_name(tls, certstore_name); + ret = tls->api->set_certstore_name(tls, certstore_name); + if (ret != 0) { + return ret; + } + + if (tls_store_string(&tls->certstore_name, certstore_name) != 0) { + return -1; + } + + return ret; } return 0; @@ -328,16 +576,36 @@ int flb_tls_set_certstore_name(struct flb_tls *tls, const char *certstore_name) int flb_tls_set_use_enterprise_store(struct flb_tls *tls, int use_enterprise) { + int ret; + if (tls) { - return tls->api->set_use_enterprise_store(tls, use_enterprise); + ret = tls->api->set_use_enterprise_store(tls, use_enterprise); + if (ret != 0) { + return ret; + } + + tls->use_enterprise_store = use_enterprise; + + return ret; } return 0; } int flb_tls_set_client_thumbprints(struct flb_tls *tls, const char *thumbprints) { + int ret; + if (tls && tls->api->set_client_thumbprints) { - return tls->api->set_client_thumbprints(tls, thumbprints); + ret = tls->api->set_client_thumbprints(tls, thumbprints); + if (ret != 0) { + return ret; + } + + if (tls_store_string(&tls->client_thumbprints, thumbprints) != 0) { + return -1; + } + + return ret; } return -1; } @@ -608,6 +876,8 @@ int flb_tls_session_create(struct flb_tls *tls, char *vhost; int flag; + flb_tls_reload_if_needed(tls); + session = flb_calloc(1, sizeof(struct flb_tls_session)); if (session == NULL) { diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 53e3574ea8a..c51b35c7de3 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1316,6 +1316,99 @@ static int tls_set_client_thumbprints(struct flb_tls *tls, const char *thumbprin #endif +static int tls_context_reload(struct flb_tls *tls) +{ + SSL_CTX *old_ssl_ctx; + struct tls_context *ctx; + struct tls_context *new_ctx; + struct flb_tls tmp_tls; + + ctx = tls->ctx; + + new_ctx = tls_context_create(tls->verify, + tls->debug, + tls->mode, + tls->vhost, + tls->ca_path, + tls->ca_file, + tls->crt_file, + tls->key_file, + tls->key_passwd); + if (new_ctx == NULL) { + return -1; + } + + tmp_tls = *tls; + tmp_tls.ctx = new_ctx; + + if (tls->verify_client == FLB_TRUE && + tls_context_set_verify_client(new_ctx, tls->verify_client) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + if ((tls->min_version != NULL || tls->max_version != NULL) && + tls_set_minmax_proto(&tmp_tls, tls->min_version, tls->max_version) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + if (tls->ciphers != NULL && + tls_set_ciphers(&tmp_tls, tls->ciphers) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + if (tls->alpn != NULL && + tls_context_alpn_set(new_ctx, tls->alpn) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + +#if defined(FLB_SYSTEM_WINDOWS) + if (tls->certstore_name != NULL && + tls_set_certstore_name(&tmp_tls, tls->certstore_name) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + if (tls->use_enterprise_store == FLB_TRUE && + tls_set_use_enterprise_store(&tmp_tls, tls->use_enterprise_store) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + if (tls->client_thumbprints != NULL && + tls_set_client_thumbprints(&tmp_tls, tls->client_thumbprints) != 0) { + tls_context_destroy(new_ctx); + return -1; + } +#endif + + if (tls->system_certificates_loaded == FLB_TRUE && + load_system_certificates(new_ctx) != 0) { + tls_context_destroy(new_ctx); + return -1; + } + + pthread_mutex_lock(&ctx->mutex); + old_ssl_ctx = ctx->ctx; + ctx->ctx = new_ctx->ctx; + new_ctx->ctx = old_ssl_ctx; + + if (tls->alpn != NULL && tls->mode == FLB_TLS_SERVER_MODE) { + SSL_CTX_set_alpn_select_cb(ctx->ctx, + tls_context_server_alpn_select_callback, + ctx); + } + + pthread_mutex_unlock(&ctx->mutex); + + tls_context_destroy(new_ctx); + + return 0; +} + static void *tls_session_create(struct flb_tls *tls, int fd) { @@ -1770,6 +1863,7 @@ static int tls_net_handshake(struct flb_tls *tls, static struct flb_tls_backend tls_openssl = { .name = "openssl", .context_create = tls_context_create, + .context_reload = tls_context_reload, .context_destroy = tls_context_destroy, .context_alpn_set = tls_context_alpn_set, .context_set_verify_client = tls_context_set_verify_client, diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index b847b056b79..9d86393a17b 100644 --- a/tests/internal/upstream_tls.c +++ b/tests/internal/upstream_tls.c @@ -10,6 +10,9 @@ #include "flb_tests_internal.h" +#include +#include + #ifdef FLB_HAVE_TLS #ifdef FLB_SYSTEM_WINDOWS @@ -21,6 +24,63 @@ struct test_backend_ctx { int destroy_calls; }; +static int copy_file(const char *src, const char *dst) +{ + FILE *in; + FILE *out; + char buf[4096]; + size_t bytes; + + in = fopen(src, "rb"); + if (in == NULL) { + return -1; + } + + out = fopen(dst, "wb"); + if (out == NULL) { + fclose(in); + return -1; + } + + while ((bytes = fread(buf, 1, sizeof(buf), in)) > 0) { + if (fwrite(buf, 1, bytes, out) != bytes) { + fclose(out); + fclose(in); + return -1; + } + } + + if (ferror(in)) { + fclose(out); + fclose(in); + return -1; + } + + fclose(out); + fclose(in); + + return 0; +} + +static int append_file(const char *path, const char *data) +{ + FILE *out; + + out = fopen(path, "ab"); + if (out == NULL) { + return -1; + } + + if (fwrite(data, 1, strlen(data), out) != strlen(data)) { + fclose(out); + return -1; + } + + fclose(out); + + return 0; +} + static void test_session_invalidate(void *session) { struct test_backend_ctx *ctx = session; @@ -161,12 +221,59 @@ void test_tls_session_destroy_no_double_free(void) #endif } +void test_tls_reload_when_certificate_file_changes(void) +{ + int ret; + char src_crt[4096]; + char src_key[4096]; + char *dst_crt; + char *dst_key; + struct flb_tls *tls; + + snprintf(src_crt, sizeof(src_crt), "%sdata/tls/certificate.pem", + FLB_TESTS_DATA_PATH); + snprintf(src_key, sizeof(src_key), "%sdata/tls/private_key.pem", + FLB_TESTS_DATA_PATH); + + dst_crt = flb_test_tmpdir_cat("/flb_tls_reload_certificate.pem"); + dst_key = flb_test_tmpdir_cat("/flb_tls_reload_private_key.pem"); + TEST_CHECK(dst_crt != NULL); + TEST_CHECK(dst_key != NULL); + + TEST_CHECK(copy_file(src_crt, dst_crt) == 0); + TEST_CHECK(copy_file(src_key, dst_key) == 0); + + tls = flb_tls_create(FLB_TLS_SERVER_MODE, + FLB_TRUE, + 0, + NULL, + NULL, + NULL, + dst_crt, + dst_key, + NULL); + TEST_CHECK(tls != NULL); + + TEST_CHECK(flb_tls_reload_if_needed(tls) == 0); + + TEST_CHECK(append_file(dst_key, "\n") == 0); + ret = flb_tls_reload_if_needed(tls); + TEST_CHECK(ret == 1); + + flb_tls_destroy(tls); + remove(dst_crt); + remove(dst_key); + flb_free(dst_crt); + flb_free(dst_key); +} + #endif TEST_LIST = { #ifdef FLB_HAVE_TLS {"prepare_destroy_conn_marks_tls_session_stale", test_prepare_destroy_conn_marks_tls_session_stale}, {"tls_session_destroy_no_double_free", test_tls_session_destroy_no_double_free}, + {"tls_reload_when_certificate_file_changes", test_tls_reload_when_certificate_file_changes}, #endif {0} };