From 0345b4a445165c4e43345f662e44870b58cdb50f Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 10 Jun 2026 17:44:21 -0700 Subject: [PATCH] fix(terraform/aws): grant OpenSearch RI actions instead of legacy es names The lambda and fargate runtime modules granted only the legacy Elasticsearch-era actions (es:DescribeReservedElasticsearchInstances, es:DescribeReservedElasticsearchInstanceOfferings, es:PurchaseReservedElasticsearchInstanceOffering), while the application uses the OpenSearch SDK whose DescribeReservedInstances, DescribeReservedInstanceOfferings and PurchaseReservedInstanceOffering operations authorize against the distinct new-style es:* action names. As a result, every host-account OpenSearch RI describe/offering/purchase call returned AccessDenied on Terraform deployments, while the parallel CloudFormation stack already granted the correct actions. Replace the three legacy actions with the new-style ones in both runtime modules, matching the CloudFormation stack, and add a regression test that fails if a runtime module grants legacy Elasticsearch-era actions or drops any of the OpenSearch actions the client code requires. Closes #1149 --- terraform/modules/compute/aws/fargate/main.tf | 6 +- .../modules/compute/aws/iam_actions_test.go | 62 +++++++++++++++++++ terraform/modules/compute/aws/lambda/main.tf | 6 +- 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 terraform/modules/compute/aws/iam_actions_test.go diff --git a/terraform/modules/compute/aws/fargate/main.tf b/terraform/modules/compute/aws/fargate/main.tf index e7d347052..6d721e66b 100644 --- a/terraform/modules/compute/aws/fargate/main.tf +++ b/terraform/modules/compute/aws/fargate/main.tf @@ -313,9 +313,9 @@ resource "aws_iam_role_policy" "ri_exchange" { "elasticache:DescribeReservedCacheNodesOfferings", "elasticache:PurchaseReservedCacheNodesOffering", # OpenSearch reserved instances - "es:DescribeReservedElasticsearchInstances", - "es:DescribeReservedElasticsearchInstanceOfferings", - "es:PurchaseReservedElasticsearchInstanceOffering", + "es:DescribeReservedInstances", + "es:DescribeReservedInstanceOfferings", + "es:PurchaseReservedInstanceOffering", # Redshift reserved nodes "redshift:DescribeReservedNodes", "redshift:DescribeReservedNodeOfferings", diff --git a/terraform/modules/compute/aws/iam_actions_test.go b/terraform/modules/compute/aws/iam_actions_test.go new file mode 100644 index 000000000..43501e130 --- /dev/null +++ b/terraform/modules/compute/aws/iam_actions_test.go @@ -0,0 +1,62 @@ +// Package aws_test guards the AWS runtime IAM action lists in the lambda and +// fargate Terraform modules against drift from the actions the application +// code actually calls. +// +// Regression test for issue #1149 (INF-01): both runtime modules granted the +// legacy Elasticsearch-era reserved-instance actions +// (es:DescribeReservedElasticsearchInstances, ...) while the application uses +// the OpenSearch SDK (opensearch.NewFromConfig), whose operations +// DescribeReservedInstances / DescribeReservedInstanceOfferings / +// PurchaseReservedInstanceOffering authorize against the distinct new-style +// es:* action names. The mismatch made every host-account OpenSearch RI call +// return AccessDenied on Terraform deployments. +package aws_test + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// runtimeModuleFiles are the Terraform runtime modules that define the IAM +// policy for the deployed application identity. +var runtimeModuleFiles = []string{ + filepath.Join("lambda", "main.tf"), + filepath.Join("fargate", "main.tf"), +} + +// requiredOpenSearchActions are the IAM actions required by the OpenSearch +// API operations invoked in providers/aws/services/opensearch/client.go. +var requiredOpenSearchActions = []string{ + "es:DescribeReservedInstances", + "es:DescribeReservedInstanceOfferings", + "es:PurchaseReservedInstanceOffering", +} + +// legacyElasticsearchActionPattern matches the pre-OpenSearch action names +// (e.g. es:DescribeReservedElasticsearchInstances) that no code path uses. +var legacyElasticsearchActionPattern = regexp.MustCompile(`es:\w*ReservedElasticsearch\w*`) + +func TestRuntimeModulesGrantOpenSearchActions(t *testing.T) { + for _, rel := range runtimeModuleFiles { + t.Run(rel, func(t *testing.T) { + data, err := os.ReadFile(rel) + if err != nil { + t.Fatalf("reading %s: %v", rel, err) + } + content := string(data) + + if legacy := legacyElasticsearchActionPattern.FindAllString(content, -1); len(legacy) > 0 { + t.Errorf("%s grants legacy Elasticsearch-era actions %v; the code calls the OpenSearch API, which authorizes against the new-style es:* action names", rel, legacy) + } + + for _, action := range requiredOpenSearchActions { + if !strings.Contains(content, `"`+action+`"`) { + t.Errorf("%s is missing required OpenSearch action %q (called by providers/aws/services/opensearch/client.go)", rel, action) + } + } + }) + } +} diff --git a/terraform/modules/compute/aws/lambda/main.tf b/terraform/modules/compute/aws/lambda/main.tf index 418fdceca..fbeba5c2d 100644 --- a/terraform/modules/compute/aws/lambda/main.tf +++ b/terraform/modules/compute/aws/lambda/main.tf @@ -363,9 +363,9 @@ resource "aws_iam_role_policy" "ri_exchange" { "elasticache:DescribeReservedCacheNodesOfferings", "elasticache:PurchaseReservedCacheNodesOffering", # OpenSearch reserved instances - "es:DescribeReservedElasticsearchInstances", - "es:DescribeReservedElasticsearchInstanceOfferings", - "es:PurchaseReservedElasticsearchInstanceOffering", + "es:DescribeReservedInstances", + "es:DescribeReservedInstanceOfferings", + "es:PurchaseReservedInstanceOffering", # Redshift reserved nodes "redshift:DescribeReservedNodes", "redshift:DescribeReservedNodeOfferings",