From 0f68e86157411fbbe968046047fd647d23aa8321 Mon Sep 17 00:00:00 2001 From: SagiROosto Date: Thu, 14 Aug 2025 16:35:16 +0300 Subject: [PATCH] aws: add IoT provider support and related credentials definitions This update introduces the IoT provider creation function and adds necessary environment variable definitions for IoT credentials in the AWS module. Additionally, the CMake configuration is updated to include the new IoT credentials source file. Signed-off-by: SagiROosto --- include/fluent-bit/flb_aws_credentials.h | 19 + src/aws/CMakeLists.txt | 1 + src/aws/flb_aws_credentials.c | 30 +- src/aws/flb_aws_credentials_iot.c | 1018 ++++++++++++++++++++++ src/aws/flb_aws_credentials_log.h | 2 +- 5 files changed, 1065 insertions(+), 5 deletions(-) create mode 100644 src/aws/flb_aws_credentials_iot.c diff --git a/include/fluent-bit/flb_aws_credentials.h b/include/fluent-bit/flb_aws_credentials.h index d7e9eaacc0c..9a6277f50ad 100644 --- a/include/fluent-bit/flb_aws_credentials.h +++ b/include/fluent-bit/flb_aws_credentials.h @@ -34,6 +34,20 @@ /* 5 second timeout for credential related http requests */ #define FLB_AWS_CREDENTIAL_NET_TIMEOUT 5 +/* IoT Credentials Environment Variables */ +#define AWS_IOT_KEY_FILE "AWS_IOT_KEY_FILE" +#define AWS_IOT_CERT_FILE "AWS_IOT_CERT_FILE" +#define AWS_IOT_CA_CERT_FILE "AWS_IOT_CA_CERT_FILE" +#define AWS_IOT_CREDENTIALS_ENDPOINT "AWS_IOT_CREDENTIALS_ENDPOINT" +#define AWS_IOT_THING_NAME "AWS_IOT_THING_NAME" +#define AWS_IOT_ROLE_ALIAS "AWS_IOT_ROLE_ALIAS" + +/* Greengrass V2 Config File - fallback source for IoT configuration */ +#define AWS_IOT_GREENGRASS_V2_CONFIG "AWS_IOT_GREENGRASS_V2_CONFIG_PATH" + +/* Greengrass V2 Component Environment Variable - fallback for CA cert */ +#define AWS_GG_ROOT_CA_PATH "GG_ROOT_CA_PATH" + /* * A structure that wraps the sensitive data needed to sign an AWS request */ @@ -225,6 +239,11 @@ struct flb_aws_provider *flb_eks_provider_create(struct flb_config *config, flb_aws_client_generator *generator); +/* + * IoT Provider + */ +struct flb_aws_provider *flb_iot_provider_create(struct flb_config *config, + struct flb_aws_client_generator *generator); /* * STS Assume Role Provider. diff --git a/src/aws/CMakeLists.txt b/src/aws/CMakeLists.txt index a8d1bdf7bbb..530c95d6cc1 100644 --- a/src/aws/CMakeLists.txt +++ b/src/aws/CMakeLists.txt @@ -16,6 +16,7 @@ set(src "flb_aws_credentials_http.c" "flb_aws_credentials_profile.c" "flb_aws_aggregation.c" + "flb_aws_credentials_iot.c" ) message(STATUS "=== AWS Credentials ===") diff --git a/src/aws/flb_aws_credentials.c b/src/aws/flb_aws_credentials.c index 37310676863..014e6bd2f0d 100644 --- a/src/aws/flb_aws_credentials.c +++ b/src/aws/flb_aws_credentials.c @@ -51,14 +51,14 @@ static struct flb_aws_provider *standard_chain_create(struct flb_config int eks_irsa, char *profile); - /* * The standard credential provider chain: * 1. Environment variables - * 2. Shared credentials file (AWS Profile) - * 3. EKS OIDC - * 4. EC2 IMDS + * 2. IoT credentials endpoint (AWS_IOT_* env vars / Greengrass V2 config) + * 3. Shared credentials file (AWS Profile) + * 4. EKS OIDC * 5. ECS HTTP credentials endpoint + * 6. EC2 IMDS * * This provider will evaluate each provider in order, returning the result * from the first provider that returns valid credentials. @@ -566,6 +566,28 @@ static struct flb_aws_provider *standard_chain_create(struct flb_config mk_list_add(&sub_provider->_head, &implementation->sub_providers); + /* + * IoT Provider - placed after environment provider but before profile provider. + * + * Rationale for this position in the credential chain: + * 1. Standard AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars take precedence + * (handled by env provider above) - explicit credentials always win. + * 2. IoT-specific env vars (AWS_IOT_*) or Greengrass V2 config indicate the user + * explicitly wants IoT credentials on devices like AWS Greengrass. + * 3. IoT provider comes before profile/EKS/ECS/EC2 because when IoT config + * is present, the device is specifically configured for IoT credentials. + * + * Configuration sources (in priority order): + * - AWS_IOT_* environment variables (explicit) + * - AWS_IOT_GREENGRASS_V2_CONFIG_PATH -> config.yaml (Greengrass V2) + * - GG_ROOT_CA_PATH fallback for CA certificate + */ + sub_provider = flb_iot_provider_create(config, generator); + if (sub_provider) { + mk_list_add(&sub_provider->_head, &implementation->sub_providers); + flb_debug("[aws_credentials] Initialized IoT Provider in standard chain"); + } + flb_debug("[aws_credentials] creating profile %s provider", profile); sub_provider = flb_profile_provider_create(profile); if (sub_provider) { diff --git a/src/aws/flb_aws_credentials_iot.c b/src/aws/flb_aws_credentials_iot.c new file mode 100644 index 00000000000..edab77f84cb --- /dev/null +++ b/src/aws/flb_aws_credentials_iot.c @@ -0,0 +1,1018 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2026 The Fluent Bit Authors + * + * Licensed 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 "flb_aws_credentials_log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef FLB_HAVE_LIBYAML +#include +#endif + +#include +#include +#include +#include +#include + +struct gg_config { + char *cert_file; + char *key_file; + char *ca_cert_file; + char *thing_name; + char *cred_endpoint; + char *role_alias; +}; + +#ifdef FLB_HAVE_LIBYAML +static void free_gg_config(struct gg_config *config) +{ + if (config) { + if (config->cert_file) { + flb_free(config->cert_file); + } + if (config->key_file) { + flb_free(config->key_file); + } + if (config->ca_cert_file) { + flb_free(config->ca_cert_file); + } + if (config->thing_name) { + flb_free(config->thing_name); + } + if (config->cred_endpoint) { + flb_free(config->cred_endpoint); + } + if (config->role_alias) { + flb_free(config->role_alias); + } + } +} + +/* + * Parse Greengrass V2 config.yaml to extract IoT configuration. + * The config.yaml has the following relevant structure: + * + * system: + * certificateFilePath: "/path/to/cert" + * privateKeyPath: "/path/to/key" + * rootCaPath: "/path/to/ca" + * thingName: "thing-name" + * services: + * aws.greengrass.Nucleus: + * configuration: + * iotCredEndpoint: "xxx.credentials.iot.region.amazonaws.com" + * iotRoleAlias: "role-alias-name" + * + * Returns 0 on success, -1 on failure. + */ +static int parse_greengrass_config(const char *config_path, + struct gg_config *config) +{ + FILE *fh = NULL; + yaml_parser_t parser; + yaml_event_t event; + int done = 0; + int ret = -1; + int depth = 0; + int in_system = 0; + int in_services = 0; + int in_nucleus = 0; + int in_configuration = 0; + char *last_key = NULL; + char *value = NULL; + + if (!config_path || !config) { + return -1; + } + + memset(config, 0, sizeof(struct gg_config)); + + fh = fopen(config_path, "r"); + if (!fh) { + AWS_CREDS_DEBUG("Could not open Greengrass config file: %s", config_path); + return -1; + } + + if (!yaml_parser_initialize(&parser)) { + AWS_CREDS_DEBUG("Failed to initialize YAML parser"); + fclose(fh); + return -1; + } + + yaml_parser_set_input_file(&parser, fh); + + while (!done) { + if (!yaml_parser_parse(&parser, &event)) { + AWS_CREDS_DEBUG("YAML parsing error in Greengrass config"); + yaml_event_delete(&event); + goto cleanup; + } + + switch (event.type) { + case YAML_STREAM_END_EVENT: + done = 1; + break; + + case YAML_MAPPING_START_EVENT: + depth++; + break; + + case YAML_MAPPING_END_EVENT: + depth--; + if (depth == 1) { + in_system = 0; + in_services = 0; + } + if (depth == 2 && in_services) { + in_nucleus = 0; + } + if (depth == 3 && in_nucleus) { + in_configuration = 0; + } + break; + + case YAML_SCALAR_EVENT: + value = (char *)event.data.scalar.value; + + if (depth == 1) { + /* Top level keys */ + if (strcmp(value, "system") == 0) { + in_system = 1; + in_services = 0; + } + else if (strcmp(value, "services") == 0) { + in_services = 1; + in_system = 0; + } + if (last_key) { + flb_free(last_key); + } + last_key = flb_strdup(value); + } + else if (depth == 2 && in_system) { + /* Inside system section */ + if (last_key) { + if (strcmp(last_key, "certificateFilePath") == 0) { + config->cert_file = flb_strdup(value); + } + else if (strcmp(last_key, "privateKeyPath") == 0) { + config->key_file = flb_strdup(value); + } + else if (strcmp(last_key, "rootCaPath") == 0) { + config->ca_cert_file = flb_strdup(value); + } + else if (strcmp(last_key, "thingName") == 0) { + config->thing_name = flb_strdup(value); + } + flb_free(last_key); + } + last_key = flb_strdup(value); + } + else if (depth == 2 && in_services) { + /* Service name */ + if (strcmp(value, "aws.greengrass.Nucleus") == 0) { + in_nucleus = 1; + } + if (last_key) { + flb_free(last_key); + } + last_key = flb_strdup(value); + } + else if (depth == 3 && in_nucleus) { + /* Inside Nucleus service */ + if (strcmp(value, "configuration") == 0) { + in_configuration = 1; + } + if (last_key) { + flb_free(last_key); + } + last_key = flb_strdup(value); + } + else if (depth == 4 && in_configuration) { + /* Inside configuration */ + if (last_key) { + if (strcmp(last_key, "iotCredEndpoint") == 0) { + config->cred_endpoint = flb_strdup(value); + } + else if (strcmp(last_key, "iotRoleAlias") == 0) { + config->role_alias = flb_strdup(value); + } + flb_free(last_key); + } + last_key = flb_strdup(value); + } + else { + /* Track keys at any other level */ + if (last_key) { + flb_free(last_key); + } + last_key = flb_strdup(value); + } + break; + + default: + break; + } + + yaml_event_delete(&event); + } + + ret = 0; + +cleanup: + if (ret != 0) { + free_gg_config(config); + } + if (last_key) { + flb_free(last_key); + } + yaml_parser_delete(&parser); + fclose(fh); + return ret; +} +#endif /* FLB_HAVE_LIBYAML */ + +/* IoT Provider */ +struct flb_aws_provider_iot { + struct flb_aws_credentials *creds; + time_t next_refresh; + + struct flb_aws_client *client; + + /* IoT specific configuration */ + char *key_file; + char *cert_file; + char *ca_cert_file; + char *credentials_endpoint; + char *thing_name; + char *role_alias; + + /* TLS configuration for IoT certificates */ + struct flb_tls *tls; + + /* Static header for thing name */ + struct flb_aws_header thing_name_header; +}; + +/* Forward declarations */ +static int iot_credentials_request(struct flb_aws_provider_iot *implementation); +static struct flb_aws_credentials *flb_parse_iot_credentials(char *response, + size_t response_len, + time_t *expiration); + +struct flb_aws_credentials *get_credentials_fn_iot(struct flb_aws_provider *provider) +{ + struct flb_aws_credentials *creds = NULL; + int refresh = FLB_FALSE; + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("Requesting credentials from the IoT provider.."); + + /* a negative next_refresh means that auto-refresh is disabled */ + if (implementation->next_refresh > 0 + && time(NULL) > implementation->next_refresh) { + refresh = FLB_TRUE; + } + if (!implementation->creds || refresh == FLB_TRUE) { + if (try_lock_provider(provider)) { + AWS_CREDS_DEBUG("IoT Provider: Refreshing credential cache."); + iot_credentials_request(implementation); + unlock_provider(provider); + } + } + + if (!implementation->creds) { + /* + * We failed to lock the provider and creds are unset. This means that + * another co-routine is performing the refresh. + */ + AWS_CREDS_WARN("No cached credentials are available and " + "a credential refresh is already in progress. The current " + "co-routine will retry."); + + return NULL; + } + + creds = flb_calloc(1, sizeof(struct flb_aws_credentials)); + if (!creds) { + goto error; + } + + creds->access_key_id = flb_sds_create(implementation->creds->access_key_id); + if (!creds->access_key_id) { + goto error; + } + + creds->secret_access_key = flb_sds_create(implementation->creds-> + secret_access_key); + if (!creds->secret_access_key) { + goto error; + } + + if (implementation->creds->session_token) { + creds->session_token = flb_sds_create(implementation->creds-> + session_token); + if (!creds->session_token) { + goto error; + } + } else { + creds->session_token = NULL; + } + + return creds; + +error: + flb_errno(); + flb_aws_credentials_destroy(creds); + return NULL; +} + +int refresh_fn_iot(struct flb_aws_provider *provider) +{ + int ret = -1; + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("Refresh called on the IoT provider"); + + if (try_lock_provider(provider)) { + ret = iot_credentials_request(implementation); + unlock_provider(provider); + } + return ret; +} + +int init_fn_iot(struct flb_aws_provider *provider) +{ + int ret = -1; + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("Init called on the IoT provider"); + + implementation->client->debug_only = FLB_TRUE; + + if (try_lock_provider(provider)) { + ret = iot_credentials_request(implementation); + unlock_provider(provider); + } + + implementation->client->debug_only = FLB_FALSE; + return ret; +} + +void sync_fn_iot(struct flb_aws_provider *provider) +{ + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("Sync called on the IoT provider"); + /* Remove async flag */ + flb_stream_disable_async_mode(&implementation->client->upstream->base); +} + +void async_fn_iot(struct flb_aws_provider *provider) +{ + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("Async called on the IoT provider"); + /* Add async flag */ + flb_stream_enable_async_mode(&implementation->client->upstream->base); +} + +void upstream_set_fn_iot(struct flb_aws_provider *provider, + struct flb_output_instance *ins) +{ + struct flb_aws_provider_iot *implementation = provider->implementation; + + AWS_CREDS_DEBUG("upstream_set called on the IoT provider"); + /* Associate output and upstream */ + flb_output_upstream_set(implementation->client->upstream, ins); +} + +void destroy_fn_iot(struct flb_aws_provider *provider) +{ + struct flb_aws_provider_iot *implementation = provider->implementation; + + if (implementation) { + if (implementation->creds) { + flb_aws_credentials_destroy(implementation->creds); + } + + if (implementation->client) { + flb_aws_client_destroy(implementation->client); + } + + if (implementation->tls) { + flb_tls_destroy(implementation->tls); + } + + if (implementation->key_file) { + flb_free(implementation->key_file); + } + if (implementation->cert_file) { + flb_free(implementation->cert_file); + } + if (implementation->ca_cert_file) { + flb_free(implementation->ca_cert_file); + } + if (implementation->credentials_endpoint) { + flb_free(implementation->credentials_endpoint); + } + if (implementation->thing_name) { + flb_free(implementation->thing_name); + } + if (implementation->role_alias) { + flb_free(implementation->role_alias); + } + + flb_free(implementation); + provider->implementation = NULL; + } + + return; +} + +static struct flb_aws_provider_vtable iot_provider_vtable = { + .get_credentials = get_credentials_fn_iot, + .init = init_fn_iot, + .refresh = refresh_fn_iot, + .destroy = destroy_fn_iot, + .sync = sync_fn_iot, + .async = async_fn_iot, + .upstream_set = upstream_set_fn_iot, +}; + +struct flb_aws_provider *flb_iot_provider_create(struct flb_config *config, + struct flb_aws_client_generator + *generator) +{ + struct flb_aws_provider_iot *implementation = NULL; + struct flb_aws_provider *provider = NULL; + struct flb_upstream *upstream = NULL; + flb_sds_t endpoint_path = NULL; + flb_sds_t protocol = NULL; + flb_sds_t host = NULL; + flb_sds_t port_sds = NULL; + int port = 443; + int ret; +#ifdef FLB_HAVE_LIBYAML + struct gg_config gg_cfg; + char *gg_config_path = NULL; + int gg_parsed = 0; +#endif + + /* + * Configuration priority (highest to lowest): + * 1. Explicit environment variables (AWS_IOT_*) + * 2. Greengrass V2 config.yaml (if AWS_IOT_GREENGRASS_V2_CONFIG_PATH is set) + * 3. GG_ROOT_CA_PATH fallback for CA cert + */ + char *key_file = getenv(AWS_IOT_KEY_FILE); + char *cert_file = getenv(AWS_IOT_CERT_FILE); + char *ca_cert_file = getenv(AWS_IOT_CA_CERT_FILE); + char *credentials_endpoint = getenv(AWS_IOT_CREDENTIALS_ENDPOINT); + char *thing_name = getenv(AWS_IOT_THING_NAME); + char *role_alias = getenv(AWS_IOT_ROLE_ALIAS); + +#ifdef FLB_HAVE_LIBYAML + /* + * If any required values are missing, try Greengrass V2 config.yaml + */ + if (!key_file || !cert_file || !ca_cert_file || !credentials_endpoint || + !thing_name || !role_alias) { + gg_config_path = getenv(AWS_IOT_GREENGRASS_V2_CONFIG); + if (gg_config_path) { + AWS_CREDS_DEBUG("Attempting to read IoT config from " + "Greengrass V2 config: %s", gg_config_path); + memset(&gg_cfg, 0, sizeof(struct gg_config)); + if (parse_greengrass_config(gg_config_path, &gg_cfg) == 0) { + gg_parsed = 1; + /* Use Greengrass values for any missing env vars */ + if (!key_file && gg_cfg.key_file) { + key_file = gg_cfg.key_file; + AWS_CREDS_DEBUG("Using privateKeyPath from Greengrass config"); + } + if (!cert_file && gg_cfg.cert_file) { + cert_file = gg_cfg.cert_file; + AWS_CREDS_DEBUG("Using certificateFilePath from " + "Greengrass config"); + } + if (!ca_cert_file && gg_cfg.ca_cert_file) { + ca_cert_file = gg_cfg.ca_cert_file; + AWS_CREDS_DEBUG("Using rootCaPath from Greengrass config"); + } + if (!thing_name && gg_cfg.thing_name) { + thing_name = gg_cfg.thing_name; + AWS_CREDS_DEBUG("Using thingName from Greengrass config"); + } + if (!credentials_endpoint && gg_cfg.cred_endpoint) { + credentials_endpoint = gg_cfg.cred_endpoint; + AWS_CREDS_DEBUG("Using iotCredEndpoint from " + "Greengrass config"); + } + if (!role_alias && gg_cfg.role_alias) { + role_alias = gg_cfg.role_alias; + AWS_CREDS_DEBUG("Using iotRoleAlias from Greengrass config"); + } + } + } + } +#endif + + /* Fallback: use GG_ROOT_CA_PATH for CA cert if still missing */ + if (!ca_cert_file) { + ca_cert_file = getenv(AWS_GG_ROOT_CA_PATH); + if (ca_cert_file) { + AWS_CREDS_DEBUG("Using GG_ROOT_CA_PATH as fallback for CA cert"); + } + } + + /* Check if we have all required values now */ + if (!key_file || !cert_file || !ca_cert_file || !credentials_endpoint || + !thing_name || !role_alias) { + AWS_CREDS_DEBUG("Not initializing IoT provider because " + "required configuration is not available"); +#ifdef FLB_HAVE_LIBYAML + if (gg_parsed) { + free_gg_config(&gg_cfg); + } +#endif + return NULL; + } + + provider = flb_calloc(1, sizeof(struct flb_aws_provider)); + if (!provider) { + flb_errno(); + goto cleanup; + } + + pthread_mutex_init(&provider->lock, NULL); + + implementation = flb_calloc(1, sizeof(struct flb_aws_provider_iot)); + if (!implementation) { + flb_errno(); + flb_free(provider); + provider = NULL; + goto cleanup; + } + + provider->provider_vtable = &iot_provider_vtable; + provider->implementation = implementation; + + /* Store IoT configuration */ + implementation->key_file = flb_strdup(key_file); + implementation->cert_file = flb_strdup(cert_file); + implementation->ca_cert_file = flb_strdup(ca_cert_file); + implementation->credentials_endpoint = flb_strdup(credentials_endpoint); + implementation->thing_name = flb_strdup(thing_name); + implementation->role_alias = flb_strdup(role_alias); + + if (!implementation->key_file || !implementation->cert_file || + !implementation->ca_cert_file || !implementation->credentials_endpoint || + !implementation->thing_name || !implementation->role_alias) { + flb_errno(); + goto error; + } + + /* + * Ensure credentials_endpoint has http or https scheme, + * default to https:// if missing + */ + if (strncmp(credentials_endpoint, "http://", 7) != 0 && + strncmp(credentials_endpoint, "https://", 8) != 0) { + flb_sds_t tmp = flb_sds_create_size(strlen(credentials_endpoint) + 8 + 1); + if (!tmp) { + AWS_CREDS_ERROR("Failed to allocate memory for credentials_endpoint"); + goto error; + } + tmp = flb_sds_cat(tmp, "https://", 8); + if (!tmp) { + goto error; + } + tmp = flb_sds_cat(tmp, credentials_endpoint, strlen(credentials_endpoint)); + if (!tmp) { + goto error; + } + flb_free(implementation->credentials_endpoint); + implementation->credentials_endpoint = flb_strdup(tmp); + flb_sds_destroy(tmp); + if (!implementation->credentials_endpoint) { + flb_errno(); + goto error; + } + credentials_endpoint = implementation->credentials_endpoint; + } + + /* Parse the credentials endpoint URL */ + ret = flb_utils_url_split_sds(credentials_endpoint, &protocol, &host, + &port_sds, &endpoint_path); + if (ret < 0) { + AWS_CREDS_ERROR("Invalid IoT credentials endpoint URL: %s", credentials_endpoint); + goto error; + } + + /* + * Warn if the endpoint URL contains a path component. + * The IoT credentials provider uses a fixed request path + * (/role-aliases//credentials) derived from the role alias, + * so any user-supplied path in the endpoint URL is ignored. + */ + if (endpoint_path != NULL && strlen(endpoint_path) > 0 + && strcmp(endpoint_path, "/") != 0) { + AWS_CREDS_WARN("IoT credentials endpoint '%s' contains a path " + "component '%s' which will be ignored. " + "Only the host and port are used; the request path " + "is built from the role alias " + "(/role-aliases//credentials).", + credentials_endpoint, endpoint_path); + } + + if (port_sds != NULL) { + port = atoi(port_sds); + if (port == 0) { + AWS_CREDS_ERROR("Invalid port in IoT credentials endpoint: %s", port_sds); + goto error; + } + } + + /* Create TLS configuration for IoT certificates */ + AWS_CREDS_DEBUG("Creating TLS instance with cert: %s, key: %s, ca: %s", + implementation->cert_file, + implementation->key_file, + implementation->ca_cert_file); + + implementation->tls = flb_tls_create(FLB_TLS_CLIENT_MODE, + FLB_TRUE, + -1, + NULL, /* vhost */ + NULL, /* ca_path */ + implementation->ca_cert_file, + implementation->cert_file, + implementation->key_file, + NULL); /* key_passwd */ + if (!implementation->tls) { + AWS_CREDS_ERROR("Failed to create TLS instance for IoT Provider"); + goto error; + } + + AWS_CREDS_DEBUG("TLS instance created successfully"); + + /* Create upstream connection */ + AWS_CREDS_DEBUG("Creating upstream connection to %s:%d", host, port); + upstream = flb_upstream_create(config, host, port, FLB_IO_TLS, implementation->tls); + if (!upstream) { + AWS_CREDS_ERROR("IoT Provider: connection initialization error"); + goto error; + } + + AWS_CREDS_DEBUG("Upstream connection created successfully"); + + upstream->base.net.connect_timeout = FLB_AWS_CREDENTIAL_NET_TIMEOUT; + + implementation->client = generator->create(); + if (!implementation->client) { + flb_upstream_destroy(upstream); + upstream = NULL; + AWS_CREDS_ERROR("IoT Provider: client creation error"); + goto error; + } + + implementation->client->name = "iot_provider_client"; + implementation->client->has_auth = FLB_FALSE; + implementation->client->provider = NULL; + implementation->client->region = NULL; + implementation->client->service = NULL; + implementation->client->port = port; + implementation->client->flags = 0; + implementation->client->proxy = NULL; + implementation->client->upstream = upstream; + + AWS_CREDS_DEBUG("IoT client configured: name=%s, port=%d, has_auth=%d", + implementation->client->name, + implementation->client->port, + implementation->client->has_auth); + + /* Set up the thing name header */ + implementation->thing_name_header.key = "x-amzn-iot-thingname"; + implementation->thing_name_header.key_len = 20; + implementation->thing_name_header.val = implementation->thing_name; + implementation->thing_name_header.val_len = strlen(implementation->thing_name); + + AWS_CREDS_DEBUG("Setting IoT thing name header: %s = %s", + implementation->thing_name_header.key, + implementation->thing_name_header.val); + + /* Set the static headers for the client */ + implementation->client->static_headers = &implementation->thing_name_header; + implementation->client->static_headers_len = 1; + +cleanup: +#ifdef FLB_HAVE_LIBYAML + if (gg_parsed) { + free_gg_config(&gg_cfg); + } +#endif + flb_sds_destroy(protocol); + flb_sds_destroy(host); + flb_sds_destroy(port_sds); + flb_sds_destroy(endpoint_path); + return provider; + +error: + flb_aws_provider_destroy(provider); + provider = NULL; + goto cleanup; +} + +static int iot_credentials_request(struct flb_aws_provider_iot *implementation) +{ + struct flb_aws_credentials *creds = NULL; + struct flb_http_client *c = NULL; + time_t expiration; + flb_sds_t uri = NULL; + flb_sds_t tmp = NULL; + int ret; + + AWS_CREDS_DEBUG("Calling IoT credentials endpoint.."); + + /* Construct the URI for the IoT credentials request */ + uri = flb_sds_create_size(256); + if (!uri) { + flb_errno(); + return -1; + } + + tmp = flb_sds_printf(&uri, "/role-aliases/%s/credentials", + implementation->role_alias); + if (!tmp) { + flb_sds_destroy(uri); + return -1; + } + uri = tmp; + + /* Make the HTTP request */ + AWS_CREDS_DEBUG("Making IoT credentials request to: %s", uri); + AWS_CREDS_DEBUG("Client headers count: %d", implementation->client->static_headers_len); + if (implementation->client->static_headers_len > 0) { + AWS_CREDS_DEBUG("Client header: %s = %s", + implementation->client->static_headers[0].key, + implementation->client->static_headers[0].val); + } + + c = implementation->client->client_vtable->request(implementation->client, FLB_HTTP_GET, + uri, NULL, 0, NULL, 0); + + flb_sds_destroy(uri); + + if (!c) { + AWS_CREDS_ERROR("IoT credentials request failed - no response"); + return -1; + } + + AWS_CREDS_DEBUG("IoT credentials response status: %d", c->resp.status); + AWS_CREDS_DEBUG("IoT credentials response size: %zu", c->resp.payload_size); + + if (c->resp.status != 200) { + AWS_CREDS_ERROR("IoT credentials request failed with status: %d", c->resp.status); + if (c->resp.payload_size > 0) { + flb_aws_print_error_code(c->resp.payload, c->resp.payload_size, + "IoTCredentialsProvider"); + } + flb_http_client_destroy(c); + return -1; + } + + AWS_CREDS_DEBUG("IoT credentials response received (size: %zu)", + c->resp.payload_size); + + /* Parse the credentials response - IoT endpoint may have different format */ + creds = flb_parse_iot_credentials(c->resp.payload, c->resp.payload_size, &expiration); + if (!creds) { + AWS_CREDS_DEBUG("Failed to parse IoT credentials response"); + flb_http_client_destroy(c); + return -1; + } + + /* Destroy existing credentials */ + flb_aws_credentials_destroy(implementation->creds); + implementation->creds = NULL; + + implementation->creds = creds; + implementation->next_refresh = expiration - FLB_AWS_REFRESH_WINDOW; + flb_http_client_destroy(c); + + return 0; +} + +/* + * Parse IoT credentials response. + * AWS IoT credentials endpoint returns a JSON response with credentials. + * The format may be different from standard AWS credentials endpoints. + */ +static struct flb_aws_credentials *flb_parse_iot_credentials(char *response, + size_t response_len, + time_t *expiration) +{ + jsmntok_t *tokens = NULL; + const jsmntok_t *t = NULL; + char *current_token = NULL; + jsmn_parser parser; + int tokens_size = 50; + size_t size; + int ret; + struct flb_aws_credentials *creds = NULL; + int i = 0; + int len; + flb_sds_t tmp; + + /* + * Remove/reset existing value of expiration. + * Expiration should be in the response, but it is not + * strictly speaking needed. Fluent Bit logs a warning if it is missing. + */ + *expiration = -1; + + jsmn_init(&parser); + + size = sizeof(jsmntok_t) * tokens_size; + tokens = flb_calloc(1, size); + if (!tokens) { + goto error; + } + + ret = jsmn_parse(&parser, response, response_len, tokens, tokens_size); + + if (ret == JSMN_ERROR_INVAL || ret == JSMN_ERROR_PART) { + AWS_CREDS_ERROR("Could not parse IoT credentials response - invalid JSON."); + goto error; + } + + /* Shouldn't happen, but just in case, check for too many tokens error */ + if (ret == JSMN_ERROR_NOMEM) { + AWS_CREDS_ERROR("Could not parse IoT credentials response " + "- response contained more tokens than expected."); + goto error; + } + + /* return value is number of tokens parsed */ + tokens_size = ret; + + creds = flb_calloc(1, sizeof(struct flb_aws_credentials)); + if (!creds) { + flb_errno(); + goto error; + } + + /* + * jsmn will create an array of tokens like: + * key, value, key, value + * For IoT credentials, the structure is: + * {"credentials": {"accessKeyId": "...", "secretAccessKey": "...", ...}} + */ + while (i < (tokens_size - 1)) { + t = &tokens[i]; + + if (t->start == -1 || t->end == -1 || (t->start == 0 && t->end == 0)) { + break; + } + + if (t->type == JSMN_STRING) { + current_token = &response[t->start]; + len = t->end - t->start; + + /* Check for credentials wrapper object */ + if (len == sizeof("credentials") - 1 + && strncmp(current_token, "credentials", len) == 0) { + /* Skip the credentials object - we'll process its contents */ + i++; + continue; + } + + /* Check for AccessKeyId field (case insensitive) */ + if (len == sizeof("accessKeyId") - 1 + && (strncmp(current_token, "accessKeyId", len) == 0 || + strncmp(current_token, "AccessKeyId", len) == 0)) { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + if (creds->access_key_id != NULL) { + AWS_CREDS_ERROR("Trying to double allocate access_key_id"); + goto error; + } + creds->access_key_id = flb_sds_create_len(current_token, len); + if (!creds->access_key_id) { + flb_errno(); + goto error; + } + continue; + } + /* Check for SecretAccessKey field (case insensitive) */ + if (len == sizeof("secretAccessKey") - 1 + && (strncmp(current_token, "secretAccessKey", len) == 0 || + strncmp(current_token, "SecretAccessKey", len) == 0)) { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + if (creds->secret_access_key != NULL) { + AWS_CREDS_ERROR("Trying to double allocate secret_access_key"); + goto error; + } + creds->secret_access_key = flb_sds_create_len(current_token, len); + if (!creds->secret_access_key) { + flb_errno(); + goto error; + } + continue; + } + /* Check for Token field (session token) - case insensitive */ + if ((len == sizeof("sessionToken") - 1 + && strncmp(current_token, "sessionToken", len) == 0) || + (len == sizeof("Token") - 1 + && strncmp(current_token, "Token", len) == 0)) { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + if (creds->session_token != NULL) { + AWS_CREDS_ERROR("Trying to double allocate session_token"); + goto error; + } + creds->session_token = flb_sds_create_len(current_token, len); + if (!creds->session_token) { + flb_errno(); + goto error; + } + continue; + } + /* Check for Expiration field (case insensitive) */ + if (len == sizeof("expiration") - 1 + && (strncmp(current_token, "expiration", len) == 0 || + strncmp(current_token, "Expiration", len) == 0)) { + i++; + t = &tokens[i]; + current_token = &response[t->start]; + len = t->end - t->start; + tmp = flb_sds_create_len(current_token, len); + if (!tmp) { + flb_errno(); + goto error; + } + *expiration = flb_aws_cred_expiration(tmp); + if (*expiration < 0) { + AWS_CREDS_WARN("'%s' was invalid or could not be parsed. " + "Disabling auto-refresh of credentials.", tmp); + } + flb_sds_destroy(tmp); + } + } + + i++; + } + + if (creds->access_key_id == NULL) { + AWS_CREDS_ERROR("Missing AccessKeyId field in IoT credentials response"); + goto error; + } + + if (creds->secret_access_key == NULL) { + AWS_CREDS_ERROR("Missing SecretAccessKey field in IoT credentials response"); + goto error; + } + + AWS_CREDS_DEBUG("Successfully parsed IoT credentials " + "- AccessKeyId: %s, Expiration: %ld", + creds->access_key_id, *expiration); + + flb_free(tokens); + return creds; + +error: + flb_aws_credentials_destroy(creds); + flb_free(tokens); + return NULL; +} diff --git a/src/aws/flb_aws_credentials_log.h b/src/aws/flb_aws_credentials_log.h index 6e9f0806dee..056b2d5be21 100644 --- a/src/aws/flb_aws_credentials_log.h +++ b/src/aws/flb_aws_credentials_log.h @@ -2,7 +2,7 @@ /* Fluent Bit * ========== - * Copyright (C) 2021 The Fluent Bit Authors + * Copyright (C) 2021-2026 The Fluent Bit Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.