From 84be0b4d73ac4f8e2b21bb50c7285c45a3a5eca9 Mon Sep 17 00:00:00 2001 From: Gabriel Yamin Date: Wed, 28 Jan 2026 15:53:01 +0200 Subject: [PATCH 1/5] out_s3: Add SSE support + validation tests Signed-off-by: Gabriel Yamin --- plugins/out_s3/s3.c | 66 ++++++++++++++++++++++++++++ plugins/out_s3/s3.h | 2 + tests/runtime/out_s3.c | 97 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/plugins/out_s3/s3.c b/plugins/out_s3/s3.c index 97c1b1d0fdb..714aec98f3e 100644 --- a/plugins/out_s3/s3.c +++ b/plugins/out_s3/s3.c @@ -138,6 +138,20 @@ static struct flb_aws_header storage_class_header = { .val_len = 0, }; +static struct flb_aws_header sse_header = { + .key = "x-amz-server-side-encryption", + .key_len = 28, + .val = "", + .val_len = 0, +}; + +static struct flb_aws_header sse_kms_key_id_header = { + .key = "x-amz-server-side-encryption-aws-kms-key-id", + .key_len = 43, + .val = "", + .val_len = 0, +}; + static char *mock_error_response(char *error_env_var) { char *err_val = NULL; @@ -194,6 +208,12 @@ int create_headers(struct flb_s3 *ctx, char *body_md5, if (ctx->storage_class != NULL) { headers_len++; } + if (ctx->sse != NULL) { + headers_len++; + } + if (ctx->sse_kms_key_id != NULL) { + headers_len++; + } if (headers_len == 0) { *num_headers = headers_len; *headers = s3_headers; @@ -239,6 +259,19 @@ int create_headers(struct flb_s3 *ctx, char *body_md5, s3_headers[n] = storage_class_header; s3_headers[n].val = ctx->storage_class; s3_headers[n].val_len = strlen(ctx->storage_class); + n++; + } + if (ctx->sse != NULL) { + s3_headers[n] = sse_header; + s3_headers[n].val = ctx->sse; + s3_headers[n].val_len = strlen(ctx->sse); + n++; + } + if (ctx->sse_kms_key_id != NULL) { + s3_headers[n] = sse_kms_key_id_header; + s3_headers[n].val = ctx->sse_kms_key_id; + s3_headers[n].val_len = strlen(ctx->sse_kms_key_id); + n++; } *num_headers = headers_len; @@ -860,6 +893,22 @@ static int cb_s3_init(struct flb_output_instance *ins, ctx->storage_class = (char *) tmp; } + tmp = flb_output_get_property("sse", ins); + if (tmp) { + if (strcasecmp(tmp, "AES256") != 0 && + strcasecmp(tmp, "aws:kms") != 0 && + strcasecmp(tmp, "aws:kms:dsse") != 0) { + flb_plg_error(ctx->ins, "Invalid 'sse' value '%s'. Must be 'AES256', 'aws:kms', or 'aws:kms:dsse'", tmp); + return -1; + } + ctx->sse = (char *) tmp; + } + + tmp = flb_output_get_property("sse_kms_key_id", ins); + if (tmp) { + ctx->sse_kms_key_id = (char *) tmp; + } + if (ctx->insecure == FLB_FALSE) { ctx->client_tls = flb_tls_create(FLB_TLS_CLIENT_MODE, ins->tls_verify, @@ -4154,6 +4203,23 @@ static struct flb_config_map config_map[] = { "will be stored with the default 'STANDARD' storage class." }, + { + FLB_CONFIG_MAP_STR, "sse", NULL, + 0, FLB_FALSE, 0, + "Server-side encryption for S3 objects. Set to 'AES256' for S3-managed keys " + "(SSE-S3), 'aws:kms' for AWS KMS-managed keys (SSE-KMS), or 'aws:kms:dsse' for " + "dual-layer server-side encryption with KMS (DSSE-KMS). When using 'aws:kms' or " + "'aws:kms:dsse', you can optionally specify the KMS key ID with the 'sse_kms_key_id' option." + }, + + { + FLB_CONFIG_MAP_STR, "sse_kms_key_id", NULL, + 0, FLB_FALSE, 0, + "AWS KMS key ID (or key ARN) for server-side encryption. Only applicable when " + "'sse' is set to 'aws:kms'. If not specified, the default AWS-managed KMS key " + "for S3 will be used." + }, + { FLB_CONFIG_MAP_STR, "profile", NULL, 0, FLB_TRUE, offsetof(struct flb_s3, profile), diff --git a/plugins/out_s3/s3.h b/plugins/out_s3/s3.h index 81122ca006c..9258fb45d41 100644 --- a/plugins/out_s3/s3.h +++ b/plugins/out_s3/s3.h @@ -114,6 +114,8 @@ struct flb_s3 { char *canned_acl; char *content_type; char *storage_class; + char *sse; + char *sse_kms_key_id; char *log_key; char *external_id; char *profile; diff --git a/tests/runtime/out_s3.c b/tests/runtime/out_s3.c index 5968ff12a50..081ba1ebdaf 100644 --- a/tests/runtime/out_s3.c +++ b/tests/runtime/out_s3.c @@ -228,6 +228,99 @@ void flb_test_s3_complete_upload_error(void) unsetenv("TEST_COMPLETE_MULTIPART_UPLOAD_ERROR"); } +void flb_test_s3_sse_invalid_value(void) +{ + int ret; + flb_ctx_t *ctx; + int in_ffd; + int out_ffd; + + /* mocks calls- signals that we are in test mode */ + setenv("FLB_S3_PLUGIN_UNDER_TEST", "true", 1); + + ctx = flb_create(); + + in_ffd = flb_input(ctx, (char *) "lib", NULL); + TEST_CHECK(in_ffd >= 0); + flb_input_set(ctx, in_ffd, "tag", "test", NULL); + + out_ffd = flb_output(ctx, (char *) "s3", NULL); + TEST_CHECK(out_ffd >= 0); + flb_output_set(ctx, out_ffd, "match", "*", NULL); + flb_output_set(ctx, out_ffd, "region", "us-west-2", NULL); + flb_output_set(ctx, out_ffd, "bucket", "fluent", NULL); + flb_output_set(ctx, out_ffd, "sse", "invalid_encryption", NULL); + + ret = flb_start(ctx); + TEST_CHECK(ret != 0); /* Expect failure due to invalid sse value */ + + flb_destroy(ctx); +} + +void flb_test_s3_sse_kms_valid(void) +{ + int ret; + flb_ctx_t *ctx; + int in_ffd; + int out_ffd; + + /* mocks calls- signals that we are in test mode */ + setenv("FLB_S3_PLUGIN_UNDER_TEST", "true", 1); + + ctx = flb_create(); + + in_ffd = flb_input(ctx, (char *) "lib", NULL); + TEST_CHECK(in_ffd >= 0); + flb_input_set(ctx, in_ffd, "tag", "test", NULL); + + out_ffd = flb_output(ctx, (char *) "s3", NULL); + TEST_CHECK(out_ffd >= 0); + flb_output_set(ctx, out_ffd, "match", "*", NULL); + flb_output_set(ctx, out_ffd, "region", "us-west-2", NULL); + flb_output_set(ctx, out_ffd, "bucket", "fluent", NULL); + flb_output_set(ctx, out_ffd, "sse", "aws:kms", NULL); + flb_output_set(ctx, out_ffd, "Retry_Limit", "1", NULL); + + ret = flb_start(ctx); + TEST_CHECK(ret == 0); /* Expect success with valid sse value */ + + sleep(1); + flb_stop(ctx); + flb_destroy(ctx); +} + +void flb_test_s3_sse_aes256_valid(void) +{ + int ret; + flb_ctx_t *ctx; + int in_ffd; + int out_ffd; + + /* mocks calls- signals that we are in test mode */ + setenv("FLB_S3_PLUGIN_UNDER_TEST", "true", 1); + + ctx = flb_create(); + + in_ffd = flb_input(ctx, (char *) "lib", NULL); + TEST_CHECK(in_ffd >= 0); + flb_input_set(ctx, in_ffd, "tag", "test", NULL); + + out_ffd = flb_output(ctx, (char *) "s3", NULL); + TEST_CHECK(out_ffd >= 0); + flb_output_set(ctx, out_ffd, "match", "*", NULL); + flb_output_set(ctx, out_ffd, "region", "us-west-2", NULL); + flb_output_set(ctx, out_ffd, "bucket", "fluent", NULL); + flb_output_set(ctx, out_ffd, "sse", "AES256", NULL); + flb_output_set(ctx, out_ffd, "Retry_Limit", "1", NULL); + + ret = flb_start(ctx); + TEST_CHECK(ret == 0); /* Expect success with valid sse value */ + + sleep(1); + flb_stop(ctx); + flb_destroy(ctx); +} + /* Test list */ TEST_LIST = { @@ -237,5 +330,9 @@ TEST_LIST = { {"create_upload_error", flb_test_s3_create_upload_error }, {"upload_part_error", flb_test_s3_upload_part_error }, {"complete_upload_error", flb_test_s3_complete_upload_error }, + {"sse_invalid_value", flb_test_s3_sse_invalid_value }, + {"sse_kms_valid", flb_test_s3_sse_kms_valid }, + {"sse_aes256_valid", flb_test_s3_sse_aes256_valid }, {NULL, NULL} }; + From 0bd330aea173d214d002959b8a4a85c1dffab5a4 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 28 Jan 2026 17:26:57 +0200 Subject: [PATCH 2/5] Validation issues fixes Signed-off-by: Gabriel --- plugins/out_s3/s3.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/out_s3/s3.c b/plugins/out_s3/s3.c index 714aec98f3e..3940aef3888 100644 --- a/plugins/out_s3/s3.c +++ b/plugins/out_s3/s3.c @@ -267,7 +267,7 @@ int create_headers(struct flb_s3 *ctx, char *body_md5, s3_headers[n].val_len = strlen(ctx->sse); n++; } - if (ctx->sse_kms_key_id != NULL) { + if (ctx->sse_kms_key_id != NULL && ctx->sse != NULL && (strcasecmp(ctx->sse, "aws:kms") == 0 || strcasecmp(ctx->sse, "aws:kms:dsse") == 0)) { s3_headers[n] = sse_kms_key_id_header; s3_headers[n].val = ctx->sse_kms_key_id; s3_headers[n].val_len = strlen(ctx->sse_kms_key_id); @@ -906,6 +906,10 @@ static int cb_s3_init(struct flb_output_instance *ins, tmp = flb_output_get_property("sse_kms_key_id", ins); if (tmp) { + if (ctx->sse == NULL || (strcasecmp(ctx->sse, "aws:kms") != 0 && strcasecmp(ctx->sse, "aws:kms:dsse") != 0)) { + flb_plg_error(ctx->ins, "Invalid 'sse_kms_key_id' value '%s'. 'sse_kms_key_id' is only applicable when 'sse' is set to 'aws:kms' or 'aws:kms:dsse'", tmp); + return -1; + } ctx->sse_kms_key_id = (char *) tmp; } @@ -4216,7 +4220,7 @@ static struct flb_config_map config_map[] = { FLB_CONFIG_MAP_STR, "sse_kms_key_id", NULL, 0, FLB_FALSE, 0, "AWS KMS key ID (or key ARN) for server-side encryption. Only applicable when " - "'sse' is set to 'aws:kms'. If not specified, the default AWS-managed KMS key " + "'sse' is set to 'aws:kms' or 'aws:kms:dsse'. If not specified, the default AWS-managed KMS key " "for S3 will be used." }, From 2c715c63b675d9408a78bc9d568c0891b9f63ace Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 28 Jan 2026 17:31:01 +0200 Subject: [PATCH 3/5] change strcasecmp to strcmp Signed-off-by: Gabriel --- plugins/out_s3/s3.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/out_s3/s3.c b/plugins/out_s3/s3.c index 3940aef3888..77ad8cc6f3f 100644 --- a/plugins/out_s3/s3.c +++ b/plugins/out_s3/s3.c @@ -267,7 +267,7 @@ int create_headers(struct flb_s3 *ctx, char *body_md5, s3_headers[n].val_len = strlen(ctx->sse); n++; } - if (ctx->sse_kms_key_id != NULL && ctx->sse != NULL && (strcasecmp(ctx->sse, "aws:kms") == 0 || strcasecmp(ctx->sse, "aws:kms:dsse") == 0)) { + if (ctx->sse_kms_key_id != NULL && ctx->sse != NULL && (strcmp(ctx->sse, "aws:kms") == 0 || strcmp(ctx->sse, "aws:kms:dsse") == 0)) { s3_headers[n] = sse_kms_key_id_header; s3_headers[n].val = ctx->sse_kms_key_id; s3_headers[n].val_len = strlen(ctx->sse_kms_key_id); @@ -895,9 +895,9 @@ static int cb_s3_init(struct flb_output_instance *ins, tmp = flb_output_get_property("sse", ins); if (tmp) { - if (strcasecmp(tmp, "AES256") != 0 && - strcasecmp(tmp, "aws:kms") != 0 && - strcasecmp(tmp, "aws:kms:dsse") != 0) { + if (strcmp(tmp, "AES256") != 0 && + strcmp(tmp, "aws:kms") != 0 && + strcmp(tmp, "aws:kms:dsse") != 0) { flb_plg_error(ctx->ins, "Invalid 'sse' value '%s'. Must be 'AES256', 'aws:kms', or 'aws:kms:dsse'", tmp); return -1; } @@ -906,7 +906,7 @@ static int cb_s3_init(struct flb_output_instance *ins, tmp = flb_output_get_property("sse_kms_key_id", ins); if (tmp) { - if (ctx->sse == NULL || (strcasecmp(ctx->sse, "aws:kms") != 0 && strcasecmp(ctx->sse, "aws:kms:dsse") != 0)) { + if (ctx->sse == NULL || (strcmp(ctx->sse, "aws:kms") != 0 && strcmp(ctx->sse, "aws:kms:dsse") != 0)) { flb_plg_error(ctx->ins, "Invalid 'sse_kms_key_id' value '%s'. 'sse_kms_key_id' is only applicable when 'sse' is set to 'aws:kms' or 'aws:kms:dsse'", tmp); return -1; } From 819ce041db986467fdb372b0c71afda9c20fcaf0 Mon Sep 17 00:00:00 2001 From: Gabriel Yamin <43830000+GabrielYamin@users.noreply.github.com> Date: Thu, 29 Jan 2026 08:47:53 +0200 Subject: [PATCH 4/5] Change strcmp to strncmp for safe string checks Signed-off-by: Gabriel Yamin <43830000+GabrielYamin@users.noreply.github.com> --- plugins/out_s3/s3.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/out_s3/s3.c b/plugins/out_s3/s3.c index 77ad8cc6f3f..a8df951b8da 100644 --- a/plugins/out_s3/s3.c +++ b/plugins/out_s3/s3.c @@ -267,7 +267,7 @@ int create_headers(struct flb_s3 *ctx, char *body_md5, s3_headers[n].val_len = strlen(ctx->sse); n++; } - if (ctx->sse_kms_key_id != NULL && ctx->sse != NULL && (strcmp(ctx->sse, "aws:kms") == 0 || strcmp(ctx->sse, "aws:kms:dsse") == 0)) { + if (ctx->sse_kms_key_id != NULL && ctx->sse != NULL && (strncmp(ctx->sse, "aws:kms", sizeof("aws:kms")) == 0 || strncmp(ctx->sse, "aws:kms:dsse", sizeof("aws:kms:dsse")) == 0)) { s3_headers[n] = sse_kms_key_id_header; s3_headers[n].val = ctx->sse_kms_key_id; s3_headers[n].val_len = strlen(ctx->sse_kms_key_id); @@ -895,9 +895,9 @@ static int cb_s3_init(struct flb_output_instance *ins, tmp = flb_output_get_property("sse", ins); if (tmp) { - if (strcmp(tmp, "AES256") != 0 && - strcmp(tmp, "aws:kms") != 0 && - strcmp(tmp, "aws:kms:dsse") != 0) { + if (strncmp(tmp, "AES256", sizeof("AES256")) != 0 && + strncmp(tmp, "aws:kms", sizeof("aws:kms")) != 0 && + strncmp(tmp, "aws:kms:dsse", sizeof("aws:kms:dsse")) != 0) { flb_plg_error(ctx->ins, "Invalid 'sse' value '%s'. Must be 'AES256', 'aws:kms', or 'aws:kms:dsse'", tmp); return -1; } @@ -906,7 +906,7 @@ static int cb_s3_init(struct flb_output_instance *ins, tmp = flb_output_get_property("sse_kms_key_id", ins); if (tmp) { - if (ctx->sse == NULL || (strcmp(ctx->sse, "aws:kms") != 0 && strcmp(ctx->sse, "aws:kms:dsse") != 0)) { + if (ctx->sse == NULL || (strncmp(ctx->sse, "aws:kms", sizeof("aws:kms")) != 0 && strncmp(ctx->sse, "aws:kms:dsse", sizeof("aws:kms:dsse")) != 0)) { flb_plg_error(ctx->ins, "Invalid 'sse_kms_key_id' value '%s'. 'sse_kms_key_id' is only applicable when 'sse' is set to 'aws:kms' or 'aws:kms:dsse'", tmp); return -1; } From 85d4edac22c330cf24ba3ac4860664e75abfb4c6 Mon Sep 17 00:00:00 2001 From: Gabriel Yamin <43830000+GabrielYamin@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:04:20 +0200 Subject: [PATCH 5/5] Fix config map Signed-off-by: Gabriel Yamin <43830000+GabrielYamin@users.noreply.github.com> --- plugins/out_s3/s3.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/out_s3/s3.c b/plugins/out_s3/s3.c index a8df951b8da..8095f046083 100644 --- a/plugins/out_s3/s3.c +++ b/plugins/out_s3/s3.c @@ -4213,13 +4213,13 @@ static struct flb_config_map config_map[] = { "Server-side encryption for S3 objects. Set to 'AES256' for S3-managed keys " "(SSE-S3), 'aws:kms' for AWS KMS-managed keys (SSE-KMS), or 'aws:kms:dsse' for " "dual-layer server-side encryption with KMS (DSSE-KMS). When using 'aws:kms' or " - "'aws:kms:dsse', you can optionally specify the KMS key ID with the 'sse_kms_key_id' option." + "'aws:kms:dsse'" }, { FLB_CONFIG_MAP_STR, "sse_kms_key_id", NULL, 0, FLB_FALSE, 0, - "AWS KMS key ID (or key ARN) for server-side encryption. Only applicable when " + "AWS key ARN for server-side encryption. Only applicable when " "'sse' is set to 'aws:kms' or 'aws:kms:dsse'. If not specified, the default AWS-managed KMS key " "for S3 will be used." },