From f34ef796962aef9d666bd8c0e4db61479c4c8027 Mon Sep 17 00:00:00 2001 From: Fernando Alexandre Date: Fri, 17 Jul 2026 16:43:06 +0100 Subject: [PATCH 1/5] oauth2: support client_secret_file with on-change reload Add a new oauth2.client_secret_file option so the client secret can be read from a file on disk instead of being provided inline. The file is re-read whenever its modification time changes, so a rotated secret is picked up on the next token refresh without restarting Fluent Bit. When both client_secret and client_secret_file are set, the file takes precedence. A missing or empty file fails cleanly at context creation. Add internal unit tests covering file load with newline trimming, change detection, file-only validation, precedence, and error handling. Signed-off-by: Fernando Alexandre --- include/fluent-bit/flb_oauth2.h | 2 + src/flb_oauth2.c | 134 +++++++++++++++ tests/internal/oauth2.c | 283 ++++++++++++++++++++++++++++++++ 3 files changed, 419 insertions(+) diff --git a/include/fluent-bit/flb_oauth2.h b/include/fluent-bit/flb_oauth2.h index 62777abcfe5..17990429f23 100644 --- a/include/fluent-bit/flb_oauth2.h +++ b/include/fluent-bit/flb_oauth2.h @@ -42,6 +42,7 @@ struct flb_oauth2_config { flb_sds_t token_url; flb_sds_t client_id; flb_sds_t client_secret; + flb_sds_t client_secret_file; flb_sds_t user_agent; flb_sds_t scope; flb_sds_t audience; @@ -75,6 +76,7 @@ struct flb_oauth2 { int payload_manual; flb_lock_t lock; time_t expires_at; + time_t client_secret_file_mtime; /* last-loaded mtime of client_secret_file */ int refresh_skew; /* Token info after successful auth */ diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 90e63ecae58..e24d8df453b 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,8 @@ #include #include #include +#include +#include #include #include @@ -67,6 +70,11 @@ struct flb_config_map oauth2_config_map[] = { 0, FLB_TRUE, offsetof(struct flb_oauth2_config, client_secret), "OAuth2 client_secret" }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_secret_file", NULL, + 0, FLB_TRUE, offsetof(struct flb_oauth2_config, client_secret_file), + "Path to a file containing the OAuth2 client_secret (re-read when it changes)" + }, { FLB_CONFIG_MAP_STR, "oauth2.user_agent", NULL, 0, FLB_TRUE, offsetof(struct flb_oauth2_config, user_agent), @@ -190,6 +198,7 @@ static void oauth2_apply_defaults(struct flb_oauth2_config *cfg) cfg->token_url = NULL; cfg->client_id = NULL; cfg->client_secret = NULL; + cfg->client_secret_file = NULL; cfg->user_agent = NULL; cfg->scope = NULL; cfg->audience = NULL; @@ -249,6 +258,15 @@ static int oauth2_clone_config(struct flb_oauth2_config *dst, } } + if (src->client_secret_file) { + dst->client_secret_file = flb_sds_create(src->client_secret_file); + if (!dst->client_secret_file) { + flb_errno(); + flb_oauth2_config_destroy(dst); + return -1; + } + } + if (src->user_agent) { dst->user_agent = flb_sds_create(src->user_agent); if (!dst->user_agent) { @@ -339,6 +357,8 @@ void flb_oauth2_config_destroy(struct flb_oauth2_config *cfg) cfg->client_id = NULL; flb_sds_destroy(cfg->client_secret); cfg->client_secret = NULL; + flb_sds_destroy(cfg->client_secret_file); + cfg->client_secret_file = NULL; flb_sds_destroy(cfg->user_agent); cfg->user_agent = NULL; flb_sds_destroy(cfg->scope); @@ -1175,11 +1195,104 @@ static int oauth2_http_request(struct flb_oauth2 *ctx, flb_sds_t body) return -1; } +/* + * Read the client_secret from a file, trimming trailing CR/LF characters. + * Returns 0 and a newly-allocated flb_sds_t in *out on success, -1 otherwise. + */ +static int oauth2_read_secret_file(const char *path, flb_sds_t *out) +{ + int ret; + size_t len; + size_t size = 0; + char *buf = NULL; + + ret = flb_utils_read_file((char *) path, &buf, &size); + if (ret != 0 || !buf) { + flb_error("[oauth2] cannot read client_secret_file '%s'", path); + return -1; + } + + /* trim trailing new lines */ + for (len = size; len > 0; len--) { + if (buf[len - 1] != '\n' && buf[len - 1] != '\r') { + break; + } + } + + if (len == 0) { + flb_free(buf); + flb_error("[oauth2] client_secret_file '%s' is empty", path); + return -1; + } + + *out = flb_sds_create_len(buf, len); + flb_free(buf); + + if (!*out) { + flb_errno(); + return -1; + } + + return 0; +} + +/* + * Reload client_secret from client_secret_file when the file's mtime changed + * (or on first load, since the tracked mtime starts at 0). On success the new + * value replaces ctx->cfg.client_secret. No-op when client_secret_file is unset. + */ +static int oauth2_refresh_client_secret_from_file(struct flb_oauth2 *ctx) +{ + int ret; + struct stat st; + flb_sds_t secret = NULL; + + if (!ctx->cfg.client_secret_file) { + return 0; + } + + ret = stat(ctx->cfg.client_secret_file, &st); + if (ret != 0) { + flb_errno(); + flb_error("[oauth2] cannot stat client_secret_file '%s'", + ctx->cfg.client_secret_file); + return -1; + } + + /* only reload when the file changed since the last load */ + if (st.st_mtime == ctx->client_secret_file_mtime) { + return 0; + } + + ret = oauth2_read_secret_file(ctx->cfg.client_secret_file, &secret); + if (ret != 0) { + return -1; + } + + if (ctx->cfg.client_secret) { + flb_sds_destroy(ctx->cfg.client_secret); + } + ctx->cfg.client_secret = secret; + ctx->client_secret_file_mtime = st.st_mtime; + + return 0; +} + static int oauth2_refresh_locked(struct flb_oauth2 *ctx) { int ret; flb_sds_t body; + /* + * Pick up a rotated secret file before building the request so both the + * POST body and the BASIC auth header use the current value. + */ + ret = oauth2_refresh_client_secret_from_file(ctx); + if (ret != 0) { + flb_error("[oauth2] could not refresh client_secret from file"); + return -1; + } + body = oauth2_build_body(ctx); if (!body) { flb_error("[oauth2] could not build request body"); @@ -1274,6 +1387,27 @@ struct flb_oauth2 *flb_oauth2_create_from_config(struct flb_config *config, return NULL; } } + + /* + * When a client_secret_file is configured, load it now so failures are + * surfaced at context creation. If both client_secret and + * client_secret_file are set, the file takes precedence. + */ + if (ctx->cfg.client_secret_file) { + if (ctx->cfg.client_secret) { + flb_debug("[oauth2] both client_secret and client_secret_file are " + "set; client_secret_file takes precedence"); + } + + ret = oauth2_refresh_client_secret_from_file(ctx); + if (ret != 0) { + flb_error("[oauth2] failed to load client_secret_file '%s'", + ctx->cfg.client_secret_file); + flb_oauth2_destroy(ctx); + return NULL; + } + } + ctx->auth_url = flb_sds_create(ctx->cfg.token_url); if (!ctx->auth_url) { flb_errno(); diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index 9df0817d821..52c8828d64f 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -30,6 +30,7 @@ #define MOCK_BODY_SIZE 16384 #define TEST_CERT_FILENAME "oauth2_private_key_jwt_test_cert.pem" #define TEST_KEY_FILENAME "oauth2_private_key_jwt_test_key.pem" +#define TEST_SECRET_FILENAME "oauth2_client_secret_file_test.txt" static const char *TEST_PRIVATE_KEY_PEM = "-----BEGIN PRIVATE KEY-----\n" @@ -547,6 +548,42 @@ static struct flb_oauth2 *create_oauth_ctx(struct flb_config *config, return create_oauth_ctx_with_user_agent(config, server, refresh_skew, NULL); } +/* + * Create an OAuth2 context using the POST auth method so the client_secret is + * carried in the token request body (captured by the mock server), which lets + * the tests assert the exact secret value that was used. Either or both of + * inline_secret and secret_file may be provided. + */ +static struct flb_oauth2 *create_oauth_ctx_secret_file(struct flb_config *config, + struct oauth2_mock_server *server, + const char *secret_file, + const char *inline_secret) +{ + struct flb_oauth2_config cfg; + struct flb_oauth2 *ctx; + + memset(&cfg, 0, sizeof(cfg)); + cfg.enabled = FLB_TRUE; + cfg.token_url = flb_sds_create_size(64); + cfg.auth_method = FLB_OAUTH2_AUTH_METHOD_POST; + cfg.refresh_skew = 58; + cfg.client_id = flb_sds_create("id"); + if (inline_secret) { + cfg.client_secret = flb_sds_create(inline_secret); + } + if (secret_file) { + cfg.client_secret_file = flb_sds_create(secret_file); + } + + flb_sds_printf(&cfg.token_url, "http://127.0.0.1:%d/token", server->port); + + ctx = flb_oauth2_create_from_config(config, &cfg); + + flb_oauth2_config_destroy(&cfg); + + return ctx; +} + void test_user_agent_header_optional(void) { int ret; @@ -640,6 +677,27 @@ static int write_text_file(const char *path, const char *content) return 0; } +static int test_secret_file_path(char *path, size_t path_size) +{ + char *tmpdir; + int ret; + + tmpdir = flb_test_env_tmpdir(); + TEST_CHECK(tmpdir != NULL); + if (!tmpdir) { + return -1; + } + + ret = snprintf(path, path_size, "%s/%s.%d", + tmpdir, TEST_SECRET_FILENAME, (int) getpid()); + flb_free(tmpdir); + if (ret < 0 || (size_t) ret >= path_size) { + return -1; + } + + return 0; +} + static int test_setup_private_key_jwt_files(char *key_path, size_t key_path_size, char *cert_path, size_t cert_path_size) { @@ -1251,6 +1309,224 @@ void test_private_key_jwt_x5t_header(void) flb_config_exit(config); } +/* AC2/AC4: secret read from a file is used, with trailing newlines trimmed. */ +void test_client_secret_from_file(void) +{ + int ret; + char secret_path[256]; + char value[128]; + flb_sds_t token = NULL; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = test_secret_file_path(secret_path, sizeof(secret_path)); + TEST_CHECK(ret == 0); + + ret = write_text_file(secret_path, "filesecret\n"); + TEST_CHECK(ret == 0); + + ret = oauth2_mock_server_start(&server, 3600, 0); + TEST_CHECK(ret == 0); + + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, NULL); + TEST_CHECK(ctx != NULL); + +#ifdef FLB_SYSTEM_MACOS + ret = oauth2_mock_server_wait_ready(&server); + TEST_CHECK(ret == 0); +#endif + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + TEST_CHECK(token != NULL); + + ret = extract_form_value(server.latest_token_request, "client_secret", + value, sizeof(value)); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(value, "filesecret") == 0); + + flb_oauth2_destroy(ctx); + oauth2_mock_server_stop(&server); + unlink(secret_path); + flb_config_exit(config); +} + +/* AC3: rewriting the file yields the new secret on the next refresh. */ +void test_client_secret_file_change_detection(void) +{ + int ret; + char secret_path[256]; + char value[128]; + flb_sds_t token = NULL; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = test_secret_file_path(secret_path, sizeof(secret_path)); + TEST_CHECK(ret == 0); + + ret = write_text_file(secret_path, "secret-v1\n"); + TEST_CHECK(ret == 0); + + ret = oauth2_mock_server_start(&server, 3600, 0); + TEST_CHECK(ret == 0); + + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, NULL); + TEST_CHECK(ctx != NULL); + +#ifdef FLB_SYSTEM_MACOS + ret = oauth2_mock_server_wait_ready(&server); + TEST_CHECK(ret == 0); +#endif + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + ret = extract_form_value(server.latest_token_request, "client_secret", + value, sizeof(value)); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(value, "secret-v1") == 0); + + /* + * Ensure the filesystem mtime advances (>= 1s resolution) before the + * rewrite so the change is detected on the next refresh. + */ + sleep(2); + + ret = write_text_file(secret_path, "secret-v2\n"); + TEST_CHECK(ret == 0); + + /* Force a refresh; the reload happens inside oauth2_refresh_locked. */ + flb_oauth2_invalidate_token(ctx); + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + ret = extract_form_value(server.latest_token_request, "client_secret", + value, sizeof(value)); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(value, "secret-v2") == 0); + + flb_oauth2_destroy(ctx); + oauth2_mock_server_stop(&server); + unlink(secret_path); + flb_config_exit(config); +} + +/* AC5: context creation succeeds with only client_secret_file (no inline secret). */ +void test_client_secret_file_only_validation(void) +{ + int ret; + char secret_path[256]; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = test_secret_file_path(secret_path, sizeof(secret_path)); + TEST_CHECK(ret == 0); + + ret = write_text_file(secret_path, "only-file-secret\n"); + TEST_CHECK(ret == 0); + + ret = oauth2_mock_server_start(&server, 3600, 0); + TEST_CHECK(ret == 0); + + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, NULL); + TEST_CHECK(ctx != NULL); + + flb_oauth2_destroy(ctx); + oauth2_mock_server_stop(&server); + unlink(secret_path); + flb_config_exit(config); +} + +/* Confirmed decision: when both are set, client_secret_file wins. */ +void test_client_secret_file_precedence(void) +{ + int ret; + char secret_path[256]; + char value[128]; + flb_sds_t token = NULL; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = test_secret_file_path(secret_path, sizeof(secret_path)); + TEST_CHECK(ret == 0); + + ret = write_text_file(secret_path, "file-wins\n"); + TEST_CHECK(ret == 0); + + ret = oauth2_mock_server_start(&server, 3600, 0); + TEST_CHECK(ret == 0); + + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, + "inline-secret"); + TEST_CHECK(ctx != NULL); + +#ifdef FLB_SYSTEM_MACOS + ret = oauth2_mock_server_wait_ready(&server); + TEST_CHECK(ret == 0); +#endif + + ret = flb_oauth2_get_access_token(ctx, &token, FLB_FALSE); + TEST_CHECK(ret == 0); + ret = extract_form_value(server.latest_token_request, "client_secret", + value, sizeof(value)); + TEST_CHECK(ret == 0); + TEST_CHECK(strcmp(value, "file-wins") == 0); + + flb_oauth2_destroy(ctx); + oauth2_mock_server_stop(&server); + unlink(secret_path); + flb_config_exit(config); +} + +/* AC6: missing or empty (newline-only) file fails cleanly at creation. */ +void test_client_secret_file_errors(void) +{ + int ret; + char secret_path[256]; + struct flb_config *config; + struct flb_oauth2 *ctx; + struct oauth2_mock_server server; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + + ret = oauth2_mock_server_start(&server, 3600, 0); + TEST_CHECK(ret == 0); + + ret = test_secret_file_path(secret_path, sizeof(secret_path)); + TEST_CHECK(ret == 0); + + /* Nonexistent file -> creation fails. */ + unlink(secret_path); + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, NULL); + TEST_CHECK(ctx == NULL); + + /* Empty (newline-only) file -> creation fails. */ + ret = write_text_file(secret_path, "\n\n"); + TEST_CHECK(ret == 0); + ctx = create_oauth_ctx_secret_file(config, &server, secret_path, NULL); + TEST_CHECK(ctx == NULL); + + oauth2_mock_server_stop(&server); + unlink(secret_path); + flb_config_exit(config); +} + TEST_LIST = { {"parse_refreshes_token_transactionally", test_parse_refreshes_token_transactionally}, @@ -1264,5 +1540,12 @@ TEST_LIST = { {"legacy_create_manual_payload_flow", test_legacy_create_manual_payload_flow}, {"private_key_jwt_body", test_private_key_jwt_body}, {"private_key_jwt_x5t_header", test_private_key_jwt_x5t_header}, + {"client_secret_from_file", test_client_secret_from_file}, + {"client_secret_file_change_detection", + test_client_secret_file_change_detection}, + {"client_secret_file_only_validation", + test_client_secret_file_only_validation}, + {"client_secret_file_precedence", test_client_secret_file_precedence}, + {"client_secret_file_errors", test_client_secret_file_errors}, {0} }; From 303c89611ac417c4d6df343bf062d57deb8e090f Mon Sep 17 00:00:00 2001 From: Fernando Alexandre Date: Fri, 17 Jul 2026 16:43:40 +0100 Subject: [PATCH 2/5] out_http: accept oauth2.client_secret_file option Register the oauth2.client_secret_file property in the plugin config map and relax the basic/post validation to accept client_secret or client_secret_file. Signed-off-by: Fernando Alexandre --- plugins/out_http/http.c | 5 +++++ plugins/out_http/http_conf.c | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/out_http/http.c b/plugins/out_http/http.c index 79f7d9dfa1a..d3c49e700cc 100644 --- a/plugins/out_http/http.c +++ b/plugins/out_http/http.c @@ -743,6 +743,11 @@ static struct flb_config_map config_map[] = { 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.client_secret), "OAuth2 client_secret" }, + { + FLB_CONFIG_MAP_STR, "oauth2.client_secret_file", NULL, + 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.client_secret_file), + "Path to a file containing the OAuth2 client_secret (re-read when it changes)" + }, { FLB_CONFIG_MAP_STR, "oauth2.user_agent", NULL, 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.user_agent), diff --git a/plugins/out_http/http_conf.c b/plugins/out_http/http_conf.c index 47d39c9fc91..669831864ff 100644 --- a/plugins/out_http/http_conf.c +++ b/plugins/out_http/http_conf.c @@ -367,8 +367,10 @@ struct flb_out_http *flb_http_conf_create(struct flb_output_instance *ins, return NULL; } } - else if (!ctx->oauth2_config.client_secret) { - flb_plg_error(ctx->ins, "oauth2 basic/post require client_secret"); + else if (!ctx->oauth2_config.client_secret && + !ctx->oauth2_config.client_secret_file) { + flb_plg_error(ctx->ins, "oauth2 basic/post require client_secret " + "or client_secret_file"); flb_http_conf_destroy(ctx); return NULL; } From 32b3b18ff61ab9dfe09be631cf768e27a1a437cc Mon Sep 17 00:00:00 2001 From: Fernando Alexandre Date: Fri, 17 Jul 2026 16:44:05 +0100 Subject: [PATCH 3/5] out_opentelemetry: accept oauth2.client_secret_file option Relax the basic/post oauth2 validation to accept client_secret or client_secret_file. The option itself is exposed through the shared oauth2 config map. Code comment cleanup for consistency Signed-off-by: Fernando Alexandre --- plugins/out_http/http.c | 2 +- plugins/out_opentelemetry/opentelemetry_conf.c | 6 ++++-- src/flb_oauth2.c | 2 +- tests/internal/oauth2.c | 11 ----------- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/plugins/out_http/http.c b/plugins/out_http/http.c index d3c49e700cc..caae3f03229 100644 --- a/plugins/out_http/http.c +++ b/plugins/out_http/http.c @@ -746,7 +746,7 @@ static struct flb_config_map config_map[] = { { FLB_CONFIG_MAP_STR, "oauth2.client_secret_file", NULL, 0, FLB_TRUE, offsetof(struct flb_out_http, oauth2_config.client_secret_file), - "Path to a file containing the OAuth2 client_secret (re-read when it changes)" + "Optional OAuth2 client_secret file path" }, { FLB_CONFIG_MAP_STR, "oauth2.user_agent", NULL, diff --git a/plugins/out_opentelemetry/opentelemetry_conf.c b/plugins/out_opentelemetry/opentelemetry_conf.c index c09cbf9eb2a..e5170f6c654 100644 --- a/plugins/out_opentelemetry/opentelemetry_conf.c +++ b/plugins/out_opentelemetry/opentelemetry_conf.c @@ -449,8 +449,10 @@ struct opentelemetry_context *flb_opentelemetry_context_create(struct flb_output return NULL; } } - else if (!ctx->oauth2_config.client_secret) { - flb_plg_error(ctx->ins, "oauth2 basic/post require client_secret"); + else if (!ctx->oauth2_config.client_secret && + !ctx->oauth2_config.client_secret_file) { + flb_plg_error(ctx->ins, "oauth2 basic/post require client_secret " + "or client_secret_file"); flb_opentelemetry_context_destroy(ctx); return NULL; } diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index e24d8df453b..1f570c732d8 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -73,7 +73,7 @@ struct flb_config_map oauth2_config_map[] = { { FLB_CONFIG_MAP_STR, "oauth2.client_secret_file", NULL, 0, FLB_TRUE, offsetof(struct flb_oauth2_config, client_secret_file), - "Path to a file containing the OAuth2 client_secret (re-read when it changes)" + "Optional OAuth2 client_secret file path" }, { FLB_CONFIG_MAP_STR, "oauth2.user_agent", NULL, diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index 52c8828d64f..aad303fac96 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -548,12 +548,6 @@ static struct flb_oauth2 *create_oauth_ctx(struct flb_config *config, return create_oauth_ctx_with_user_agent(config, server, refresh_skew, NULL); } -/* - * Create an OAuth2 context using the POST auth method so the client_secret is - * carried in the token request body (captured by the mock server), which lets - * the tests assert the exact secret value that was used. Either or both of - * inline_secret and secret_file may be provided. - */ static struct flb_oauth2 *create_oauth_ctx_secret_file(struct flb_config *config, struct oauth2_mock_server *server, const char *secret_file, @@ -1309,7 +1303,6 @@ void test_private_key_jwt_x5t_header(void) flb_config_exit(config); } -/* AC2/AC4: secret read from a file is used, with trailing newlines trimmed. */ void test_client_secret_from_file(void) { int ret; @@ -1355,7 +1348,6 @@ void test_client_secret_from_file(void) flb_config_exit(config); } -/* AC3: rewriting the file yields the new secret on the next refresh. */ void test_client_secret_file_change_detection(void) { int ret; @@ -1418,7 +1410,6 @@ void test_client_secret_file_change_detection(void) flb_config_exit(config); } -/* AC5: context creation succeeds with only client_secret_file (no inline secret). */ void test_client_secret_file_only_validation(void) { int ret; @@ -1448,7 +1439,6 @@ void test_client_secret_file_only_validation(void) flb_config_exit(config); } -/* Confirmed decision: when both are set, client_secret_file wins. */ void test_client_secret_file_precedence(void) { int ret; @@ -1493,7 +1483,6 @@ void test_client_secret_file_precedence(void) flb_config_exit(config); } -/* AC6: missing or empty (newline-only) file fails cleanly at creation. */ void test_client_secret_file_errors(void) { int ret; From 8492282b0b0a1ea46004c48fc09a1966a1d96b58 Mon Sep 17 00:00:00 2001 From: Fernando Alexandre Date: Mon, 20 Jul 2026 18:02:50 +0100 Subject: [PATCH 4/5] Added a check to ensure refresh only happens with `basic` and `post` authentication methods Signed-off-by: Fernando Alexandre --- src/flb_oauth2.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index 1f570c732d8..fdcc4be05e7 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -1247,6 +1247,11 @@ static int oauth2_refresh_client_secret_from_file(struct flb_oauth2 *ctx) struct stat st; flb_sds_t secret = NULL; + if (ctx->cfg.auth_method != FLB_OAUTH2_AUTH_METHOD_BASIC && + ctx->cfg.auth_method != FLB_OAUTH2_AUTH_METHOD_POST) { + return 0; + } + if (!ctx->cfg.client_secret_file) { return 0; } From 2e9711c1512cbc106ee4575af6353860d66657e9 Mon Sep 17 00:00:00 2001 From: Fernando Alexandre Date: Mon, 20 Jul 2026 18:19:19 +0100 Subject: [PATCH 5/5] Re-reads token on every refresh Signed-off-by: Fernando Alexandre --- include/fluent-bit/flb_oauth2.h | 1 - src/flb_oauth2.c | 25 ++++--------------------- tests/internal/oauth2.c | 6 ------ 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/include/fluent-bit/flb_oauth2.h b/include/fluent-bit/flb_oauth2.h index 17990429f23..94374660bf5 100644 --- a/include/fluent-bit/flb_oauth2.h +++ b/include/fluent-bit/flb_oauth2.h @@ -76,7 +76,6 @@ struct flb_oauth2 { int payload_manual; flb_lock_t lock; time_t expires_at; - time_t client_secret_file_mtime; /* last-loaded mtime of client_secret_file */ int refresh_skew; /* Token info after successful auth */ diff --git a/src/flb_oauth2.c b/src/flb_oauth2.c index fdcc4be05e7..2c1d0cfe76e 100644 --- a/src/flb_oauth2.c +++ b/src/flb_oauth2.c @@ -29,7 +29,6 @@ #include #include #include -#include #include #include @@ -37,8 +36,6 @@ #include #include #include -#include -#include #include #include @@ -1237,14 +1234,14 @@ static int oauth2_read_secret_file(const char *path, flb_sds_t *out) } /* - * Reload client_secret from client_secret_file when the file's mtime changed - * (or on first load, since the tracked mtime starts at 0). On success the new - * value replaces ctx->cfg.client_secret. No-op when client_secret_file is unset. + * Load client_secret from client_secret_file, replacing the current value. The + * file is re-read on every refresh so a rotated secret is always picked up, + * independent of filesystem mtime resolution. No-op for auth methods that do + * not use client_secret, or when client_secret_file is unset. */ static int oauth2_refresh_client_secret_from_file(struct flb_oauth2 *ctx) { int ret; - struct stat st; flb_sds_t secret = NULL; if (ctx->cfg.auth_method != FLB_OAUTH2_AUTH_METHOD_BASIC && @@ -1256,19 +1253,6 @@ static int oauth2_refresh_client_secret_from_file(struct flb_oauth2 *ctx) return 0; } - ret = stat(ctx->cfg.client_secret_file, &st); - if (ret != 0) { - flb_errno(); - flb_error("[oauth2] cannot stat client_secret_file '%s'", - ctx->cfg.client_secret_file); - return -1; - } - - /* only reload when the file changed since the last load */ - if (st.st_mtime == ctx->client_secret_file_mtime) { - return 0; - } - ret = oauth2_read_secret_file(ctx->cfg.client_secret_file, &secret); if (ret != 0) { return -1; @@ -1278,7 +1262,6 @@ static int oauth2_refresh_client_secret_from_file(struct flb_oauth2 *ctx) flb_sds_destroy(ctx->cfg.client_secret); } ctx->cfg.client_secret = secret; - ctx->client_secret_file_mtime = st.st_mtime; return 0; } diff --git a/tests/internal/oauth2.c b/tests/internal/oauth2.c index aad303fac96..7e9da37530d 100644 --- a/tests/internal/oauth2.c +++ b/tests/internal/oauth2.c @@ -1385,12 +1385,6 @@ void test_client_secret_file_change_detection(void) TEST_CHECK(ret == 0); TEST_CHECK(strcmp(value, "secret-v1") == 0); - /* - * Ensure the filesystem mtime advances (>= 1s resolution) before the - * rewrite so the change is detected on the next refresh. - */ - sleep(2); - ret = write_text_file(secret_path, "secret-v2\n"); TEST_CHECK(ret == 0);