From dfbace67d830babab4172eaaf22349a53015de62 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 8 Sep 2026 09:46:30 +0200 Subject: [PATCH 1/3] sec(iac/aws): drop unconditioned account-wide KMS data-plane grant from deploy role The KMS statement in cudly-deploy-networking granted kms:CreateGrant, kms:Decrypt, kms:Encrypt, kms:GenerateDataKey and kms:DescribeKey on Resource "*" with no Condition. Customer managed keys delegate to IAM by default, so a leaked GitHub OIDC token assuming the deploy role could decrypt with, and create grants on, every CMK in the shared account. Nothing on the deploy path calls the four data-plane actions: the only CMK CUDly's modules create is the asymmetric SIGN_VERIFY OIDC signing key, which cannot serve them, and every other encrypted resource (RDS, Secrets Manager, ECR, the S3 state bucket, Lambda env vars) uses an AWS managed key whose key policy authorizes account principals through the service without an IAM grant. kms:DescribeKey stays available through KMSCreateAndRead in policy_compute.tf. Remove the statement and add a guard test that fails if any deploy-role identity policy grants a KMS data-plane action without a Condition, so the grant cannot come back bare. A tag-gated grant (the KMSMutateTaggedOnly convention) still passes the guard if a CUDly symmetric key ever needs one. The policy description is left mentioning KMS on purpose: description is ForceNew on aws_iam_policy and editing it would replace the attached policy instead of updating it in place. Closes #1969 Co-Authored-By: claude-flow Claude-Session: https://claude.ai/code/session_01Fu9uWjxtDFx5HDKeMRt1jC --- .../ci-cd-permissions/policy_guard_test.go | 66 +++++++++++++++++++ .../ci-cd-permissions/policy_networking.tf | 12 ---- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go index 37438e1a4..a6d51595f 100644 --- a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go +++ b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go @@ -1620,3 +1620,69 @@ func TestPolicyDocumentsUseLiteralActionAndResourceLists(t *testing.T) { t.Fatalf("scanned zero statements across %v; this guard would pass vacuously", files) } } + +// kmsDataPlaneActions are the KMS operations that read or write ciphertext, +// or (CreateGrant) delegate the right to. No resource in terraform/modules +// needs the deploy role to call any of them: the only CMK the modules create +// on the AWS path (modules/compute/aws/lambda/signing-key.tf) is an +// asymmetric SIGN_VERIFY key that cannot serve Encrypt, Decrypt or +// GenerateDataKey at all, and every other encrypted resource (RDS storage, +// Secrets Manager secrets, ECR, the S3 state bucket, Lambda environment +// variables) uses an AWS managed key whose own key policy authorizes account +// principals through the owning service, so no IAM grant is needed. An +// unconditioned Allow of any of these therefore protects nothing CUDly owns +// and, because customer managed keys carry a default key policy that +// delegates to IAM, reaches every CMK in the shared account (#1969). A grant +// gated on aws:ResourceTag/Project, the KMSMutateTaggedOnly convention in +// policy_compute.tf, or on kms:GrantIsForAWSResource for CreateGrant, still +// passes this guard if a CUDly symmetric key ever needs one. +var kmsDataPlaneActions = []string{ + "kms:CreateGrant", + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyPair", + "kms:GenerateDataKeyPairWithoutPlaintext", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", +} + +// TestKMSDataPlaneActionsAreNotUnconditionallyGranted is the KMS counterpart of +// TestBoundaryGatedActionsAreNotUnconditionallyGranted: every deploy-role +// identity policy in this directory may grant a kmsDataPlaneActions entry only +// under a Condition. policy_boundary.tf is skipped on purpose: its +// WorkloadServiceCeiling allows kms:* by design and, being a permissions +// boundary, grants nothing on its own. +func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { + scanned := 0 + for _, f := range append(guardedPolicyFiles(t), iamFile) { + if f == boundaryFile { + continue + } + t.Run(f, func(t *testing.T) { + stmts := extractStatementBlocks(readPolicySource(t, f)) + if len(stmts) == 0 { + t.Fatalf("%s: found zero Statement objects; the statement splitter in this test is broken and would otherwise pass vacuously", f) + } + for _, stmt := range stmts { + scanned++ + if !effectIsAllowPattern.MatchString(stmt) || statementConditionBody(stmt) != "" { + continue + } + granted := extractActionListActions(stmt) + if n := countActionListStrings(stmt); n != len(granted) { + t.Errorf("%s: statement %q has %d Action entries but this test could parse only %d of them; an entry it cannot read is still granted by IAM, so it may be a KMS data-plane grant this guard never sees", f, statementSid(stmt), n, len(granted)) + } + for _, action := range kmsDataPlaneActions { + if actionPermitted(granted, action) { + t.Errorf("%s: statement %q grants %s with no Condition. Customer managed keys delegate to IAM by default, so this reaches every CMK in the account, not just CUDly's (#1969). Nothing on the deploy path needs it; if a CUDly symmetric key ever does, gate the grant on aws:ResourceTag/Project like KMSMutateTaggedOnly in policy_compute.tf (and CreateGrant additionally on kms:GrantIsForAWSResource)", f, statementSid(stmt), action) + } + } + } + }) + } + if scanned == 0 { + t.Fatalf("scanned zero statements; this guard would pass vacuously") + } +} diff --git a/terraform/environments/aws/ci-cd-permissions/policy_networking.tf b/terraform/environments/aws/ci-cd-permissions/policy_networking.tf index 0ff3ec119..de56d2941 100644 --- a/terraform/environments/aws/ci-cd-permissions/policy_networking.tf +++ b/terraform/environments/aws/ci-cd-permissions/policy_networking.tf @@ -162,18 +162,6 @@ resource "aws_iam_policy" "networking" { ] Resource = "*" }, - { - Sid = "KMS" - Effect = "Allow" - Action = [ - "kms:CreateGrant", - "kms:Decrypt", - "kms:DescribeKey", - "kms:Encrypt", - "kms:GenerateDataKey", - ] - Resource = "*" - }, ] }) From 583a013a6f8d90615f966b251143953b81d3123e Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 8 Sep 2026 10:19:14 +0200 Subject: [PATCH 2/3] fix(iac/aws): fail closed on unreadable KMS Action and non-scoping Condition TestKMSDataPlaneActionsAreNotUnconditionallyGranted previously let a statement's Action expression be a non-literal reference (e.g. Action = local.x) pass silently: both extractActionListActions and countActionListStrings parsed it to zero entries, so 0 != 0 raised no error and the KMS action loop iterated over an empty set. It also treated any non-empty Condition as an exemption, including one that scopes nothing relevant, even though the escape hatch is meant to be a resource-scoping condition. Both were found by review. The guard now errors when an Allow statement's Action key is present but not parseable as a literal list or string, and only exempts a statement when its Condition actually references aws:ResourceTag/Project (and, for kms:CreateGrant, also kms:GrantIsForAWSResource). Co-Authored-By: claude-flow Claude-Session: https://claude.ai/code/session_01Fu9uWjxtDFx5HDKeMRt1jC --- .../ci-cd-permissions/policy_guard_test.go | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go index a6d51595f..ef4d36d17 100644 --- a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go +++ b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go @@ -1648,12 +1648,37 @@ var kmsDataPlaneActions = []string{ "kms:ReEncryptTo", } +// actionKeyPresentPattern matches an `Action = ...` or `actions = ...` +// assignment regardless of whether the value is a literal list/string this +// test can read. It exists so TestKMSDataPlaneActionsAreNotUnconditionallyGranted +// can tell "this statement grants nothing" (no Action key at all) apart from +// "this statement grants something this test cannot read" +// (`Action = local.something`): actionAssignmentPattern only matches the +// latter's value shape, so a bare reference produces the exact same zero +// match count as a statement with no Action key, and only the reference case +// is a hole worth failing on (#1969). +var actionKeyPresentPattern = regexp.MustCompile(`(?m)^[ \t]*(?:Action|actions)\s*=`) + +// kmsResourceScopingConditionKey is the condition key KMSMutateTaggedOnly +// (policy_compute.tf) and KMSReadTaggedOnly (policy_compute_b.tf) key their +// exemption on. A Condition that does not reference it (an unrelated key, or +// none at all) restricts nothing this guard cares about, so its presence, +// not merely a Condition's presence, is what earns the exemption below. +const kmsResourceScopingConditionKey = "aws:ResourceTag/Project" + +// kmsGrantIsForResourceConditionKey is required IN ADDITION to +// kmsResourceScopingConditionKey for kms:CreateGrant specifically: the tag +// condition scopes which key the grant is created on, not who the grant +// authorizes, so a CreateGrant gated on the tag alone still lets the deploy +// role hand the ability to use a CUDly-tagged key to any grantee it names. +const kmsGrantIsForResourceConditionKey = "kms:GrantIsForAWSResource" + // TestKMSDataPlaneActionsAreNotUnconditionallyGranted is the KMS counterpart of // TestBoundaryGatedActionsAreNotUnconditionallyGranted: every deploy-role // identity policy in this directory may grant a kmsDataPlaneActions entry only -// under a Condition. policy_boundary.tf is skipped on purpose: its -// WorkloadServiceCeiling allows kms:* by design and, being a permissions -// boundary, grants nothing on its own. +// under a Condition actually scoped to a CUDly-owned key. policy_boundary.tf +// is skipped on purpose: its WorkloadServiceCeiling allows kms:* by design +// and, being a permissions boundary, grants nothing on its own. func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { scanned := 0 for _, f := range append(guardedPolicyFiles(t), iamFile) { @@ -1667,17 +1692,32 @@ func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { } for _, stmt := range stmts { scanned++ - if !effectIsAllowPattern.MatchString(stmt) || statementConditionBody(stmt) != "" { + if !effectIsAllowPattern.MatchString(stmt) { continue } + + if !actionAssignmentPattern.MatchString(stmt) { + if actionKeyPresentPattern.MatchString(stmt) { + t.Errorf("%s: statement %q has an Action expression this test cannot read (not a literal list or quoted string, e.g. a local or var reference). IAM still grants whatever it resolves to, so a KMS data-plane action behind it is invisible to this guard (#1969). Gate the statement with a Condition scoped to %s, or write Action as literals", f, statementSid(stmt), kmsResourceScopingConditionKey) + } + continue + } + granted := extractActionListActions(stmt) if n := countActionListStrings(stmt); n != len(granted) { t.Errorf("%s: statement %q has %d Action entries but this test could parse only %d of them; an entry it cannot read is still granted by IAM, so it may be a KMS data-plane grant this guard never sees", f, statementSid(stmt), n, len(granted)) } + + condBody := statementConditionBody(stmt) + scopedToProject := strings.Contains(condBody, kmsResourceScopingConditionKey) for _, action := range kmsDataPlaneActions { - if actionPermitted(granted, action) { - t.Errorf("%s: statement %q grants %s with no Condition. Customer managed keys delegate to IAM by default, so this reaches every CMK in the account, not just CUDly's (#1969). Nothing on the deploy path needs it; if a CUDly symmetric key ever does, gate the grant on aws:ResourceTag/Project like KMSMutateTaggedOnly in policy_compute.tf (and CreateGrant additionally on kms:GrantIsForAWSResource)", f, statementSid(stmt), action) + if !actionPermitted(granted, action) { + continue + } + if scopedToProject && (action != "kms:CreateGrant" || strings.Contains(condBody, kmsGrantIsForResourceConditionKey)) { + continue } + t.Errorf("%s: statement %q grants %s without a Condition properly scoped to a CUDly-owned key (needs %s, and for kms:CreateGrant also %s). Customer managed keys delegate to IAM by default, so an unscoped or wrongly-scoped grant reaches every CMK in the account, not just CUDly's (#1969). Nothing on the deploy path needs it; if a CUDly symmetric key ever does, gate it like KMSMutateTaggedOnly in policy_compute.tf", f, statementSid(stmt), action, kmsResourceScopingConditionKey, kmsGrantIsForResourceConditionKey) } } }) From bacd4e15eabfc9f0ade9b14fad983059e73c7f72 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 8 Sep 2026 10:43:40 +0200 Subject: [PATCH 3/3] fix(iac/aws): close remaining KMS guard bypasses found in adversarial review The prior fix on this branch still let three Allow-statement shapes dodge both the unreadable-Action check and the NotAction check silently: a one-line statement, a quoted "Action" key, and NotAction itself, each of which can grant a KMS data-plane action while the guard stays green. A valid IAM Allow statement always carries Action or NotAction, so there is no legitimate statement left to protect by skipping; the guard now mirrors boundaryAllowedActions' fail-closed shape, refusing NotAction outright and refusing any statement whose Action this test cannot parse into literals. The Condition exemption also still passed on a substring check, which a wrong tag value, an inverted operator (StringNotEquals), or the tag key appearing only in a trailing comment could all satisfy while granting broadly. It now requires an operator literally named StringEquals or StringEqualsIgnoreCase whose body pins aws:ResourceTag/Project to CUDly in key position, and for kms:CreateGrant an additional Bool operator pinning kms:GrantIsForAWSResource to true, using the existing statementConditionOperators/anyBlockOpenPattern completeness check so an operator this guard cannot see is not silently trusted either. The kms:GrantIsForAWSResource comment also attributed the wrong mechanism: it does not scope who a grant authorizes (that is kms:GranteePrincipal), it restricts the call path to CreateGrant made by an AWS service on the deploy role's behalf rather than a direct caller. Comment corrected. All found by review. Co-Authored-By: claude-flow Claude-Session: https://claude.ai/code/session_01Fu9uWjxtDFx5HDKeMRt1jC --- .../ci-cd-permissions/policy_guard_test.go | 102 ++++++++++++------ 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go index ef4d36d17..38dc0a4b8 100644 --- a/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go +++ b/terraform/environments/aws/ci-cd-permissions/policy_guard_test.go @@ -1634,8 +1634,10 @@ func TestPolicyDocumentsUseLiteralActionAndResourceLists(t *testing.T) { // and, because customer managed keys carry a default key policy that // delegates to IAM, reaches every CMK in the shared account (#1969). A grant // gated on aws:ResourceTag/Project, the KMSMutateTaggedOnly convention in -// policy_compute.tf, or on kms:GrantIsForAWSResource for CreateGrant, still -// passes this guard if a CUDly symmetric key ever needs one. +// policy_compute.tf, still passes this guard if a CUDly symmetric key ever +// needs one; kms:CreateGrant additionally needs kms:GrantIsForAWSResource, +// since the tag alone scopes which key the grant targets, not who it +// authorizes. var kmsDataPlaneActions = []string{ "kms:CreateGrant", "kms:Decrypt", @@ -1648,37 +1650,56 @@ var kmsDataPlaneActions = []string{ "kms:ReEncryptTo", } -// actionKeyPresentPattern matches an `Action = ...` or `actions = ...` -// assignment regardless of whether the value is a literal list/string this -// test can read. It exists so TestKMSDataPlaneActionsAreNotUnconditionallyGranted -// can tell "this statement grants nothing" (no Action key at all) apart from -// "this statement grants something this test cannot read" -// (`Action = local.something`): actionAssignmentPattern only matches the -// latter's value shape, so a bare reference produces the exact same zero -// match count as a statement with no Action key, and only the reference case -// is a hole worth failing on (#1969). -var actionKeyPresentPattern = regexp.MustCompile(`(?m)^[ \t]*(?:Action|actions)\s*=`) - -// kmsResourceScopingConditionKey is the condition key KMSMutateTaggedOnly -// (policy_compute.tf) and KMSReadTaggedOnly (policy_compute_b.tf) key their -// exemption on. A Condition that does not reference it (an unrelated key, or -// none at all) restricts nothing this guard cares about, so its presence, -// not merely a Condition's presence, is what earns the exemption below. -const kmsResourceScopingConditionKey = "aws:ResourceTag/Project" - -// kmsGrantIsForResourceConditionKey is required IN ADDITION to -// kmsResourceScopingConditionKey for kms:CreateGrant specifically: the tag -// condition scopes which key the grant is created on, not who the grant -// authorizes, so a CreateGrant gated on the tag alone still lets the deploy -// role hand the ability to use a CUDly-tagged key to any grantee it names. +// kmsResourceScopingConditionKey and kmsResourceScopingTagValue are the +// condition key and value KMSMutateTaggedOnly (policy_compute.tf) and +// KMSReadTaggedOnly (policy_compute_b.tf) key their exemption on. Both are +// pinned, not merely the key's presence: a Condition that references the key +// with an unrelated value, or that inverts it (StringNotEquals), restricts +// nothing this guard cares about either. +const ( + kmsResourceScopingConditionKey = "aws:ResourceTag/Project" + kmsResourceScopingTagValue = "CUDly" +) + +// kmsGrantIsForResourceConditionKey closes the gap kmsResourceScopingConditionKey +// leaves open for kms:CreateGrant specifically: the tag condition scopes +// which key the grant is created ON, not who it authorizes, so CreateGrant +// additionally needs this Bool condition pinned true, which permits the call +// only when an AWS service integrated with KMS makes it on the deploy role's +// behalf, never a direct caller naming an arbitrary grantee. const kmsGrantIsForResourceConditionKey = "kms:GrantIsForAWSResource" +// kmsResourceScopingOperatorPattern matches kmsResourceScopingConditionKey in +// KEY position (quoted, followed by "="), pinned to kmsResourceScopingTagValue +// case-insensitively (matching the StringEqualsIgnoreCase these statements +// use, per the tag-casing note in policy_compute.tf). Key position, not a +// bare Contains, is what stops the key appearing only as a value, or inside a +// trailing comment (readPolicySource strips only whole-line comments), from +// earning the exemption; pinning the value is what stops a wrong tag value or +// an inverted operator from earning it once combined with the operator name +// check below. +var kmsResourceScopingOperatorPattern = regexp.MustCompile(`(?i)"` + regexp.QuoteMeta(kmsResourceScopingConditionKey) + `"\s*=\s*"` + regexp.QuoteMeta(kmsResourceScopingTagValue) + `"`) + +// kmsGrantIsForResourceTruePattern matches kmsGrantIsForResourceConditionKey +// in key position pinned to "true": "false" or any other value grants +// nothing extra and must not earn the exemption. +var kmsGrantIsForResourceTruePattern = regexp.MustCompile(`"` + regexp.QuoteMeta(kmsGrantIsForResourceConditionKey) + `"\s*=\s*"true"`) + // TestKMSDataPlaneActionsAreNotUnconditionallyGranted is the KMS counterpart of // TestBoundaryGatedActionsAreNotUnconditionallyGranted: every deploy-role // identity policy in this directory may grant a kmsDataPlaneActions entry only // under a Condition actually scoped to a CUDly-owned key. policy_boundary.tf // is skipped on purpose: its WorkloadServiceCeiling allows kms:* by design // and, being a permissions boundary, grants nothing on its own. +// +// Effect and Action are read the same fail-closed way boundaryAllowedActions +// reads policy_boundary.tf: a statement not positively identified as a Deny +// is treated as a grant, NotAction is refused outright because it cannot be +// expressed as a bounded Action set, and a statement whose Action this test +// cannot parse into literals is refused rather than silently skipped. A +// one-line statement and a quoted "Action" key both hide the assignment from +// actionAssignmentPattern's ^-anchored match the same way a local/var +// reference does, so all three are the same failure here. func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { scanned := 0 for _, f := range append(guardedPolicyFiles(t), iamFile) { @@ -1692,14 +1713,15 @@ func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { } for _, stmt := range stmts { scanned++ - if !effectIsAllowPattern.MatchString(stmt) { + if statementEffect(stmt) == "Deny" { + continue + } + if strings.Contains(stmt, "NotAction") { + t.Errorf("%s: statement %q uses NotAction, which grants every action except the ones it names. A KMS data-plane action granted by omission this way is invisible to a guard that can only check a bounded Action set (#1969)", f, statementSid(stmt)) continue } - if !actionAssignmentPattern.MatchString(stmt) { - if actionKeyPresentPattern.MatchString(stmt) { - t.Errorf("%s: statement %q has an Action expression this test cannot read (not a literal list or quoted string, e.g. a local or var reference). IAM still grants whatever it resolves to, so a KMS data-plane action behind it is invisible to this guard (#1969). Gate the statement with a Condition scoped to %s, or write Action as literals", f, statementSid(stmt), kmsResourceScopingConditionKey) - } + t.Errorf("%s: statement %q has no Action list this test can parse (not one attribute per line with a literal list or quoted string, e.g. a one-line statement, a quoted \"Action\" key, or a local/var reference). IAM still grants whatever it resolves to, so a KMS data-plane action behind it is invisible to this guard (#1969)", f, statementSid(stmt)) continue } @@ -1709,15 +1731,29 @@ func TestKMSDataPlaneActionsAreNotUnconditionallyGranted(t *testing.T) { } condBody := statementConditionBody(stmt) - scopedToProject := strings.Contains(condBody, kmsResourceScopingConditionKey) + ops := statementConditionOperators(stmt) + if opens := len(anyBlockOpenPattern.FindAllString(condBody, -1)); opens != len(ops) { + t.Errorf("%s: statement %q has a Condition block opening %d nested objects but this test parsed only %d of them as operators; an operator it cannot see is still ANDed in by IAM, so this guard cannot verify what actually scopes the grant", f, statementSid(stmt), opens, len(ops)) + } + + scopedToProject, grantIsForResource := false, false + for _, op := range ops { + switch op.name { + case "StringEquals", "StringEqualsIgnoreCase": + scopedToProject = scopedToProject || kmsResourceScopingOperatorPattern.MatchString(op.body) + case "Bool": + grantIsForResource = grantIsForResource || kmsGrantIsForResourceTruePattern.MatchString(op.body) + } + } + for _, action := range kmsDataPlaneActions { if !actionPermitted(granted, action) { continue } - if scopedToProject && (action != "kms:CreateGrant" || strings.Contains(condBody, kmsGrantIsForResourceConditionKey)) { + if scopedToProject && (action != "kms:CreateGrant" || grantIsForResource) { continue } - t.Errorf("%s: statement %q grants %s without a Condition properly scoped to a CUDly-owned key (needs %s, and for kms:CreateGrant also %s). Customer managed keys delegate to IAM by default, so an unscoped or wrongly-scoped grant reaches every CMK in the account, not just CUDly's (#1969). Nothing on the deploy path needs it; if a CUDly symmetric key ever does, gate it like KMSMutateTaggedOnly in policy_compute.tf", f, statementSid(stmt), action, kmsResourceScopingConditionKey, kmsGrantIsForResourceConditionKey) + t.Errorf("%s: statement %q grants %s without a Condition properly scoped to a CUDly-owned key (needs %s = %q via StringEquals/StringEqualsIgnoreCase, and for kms:CreateGrant also %s = \"true\" via Bool). Customer managed keys delegate to IAM by default, so an unscoped or wrongly-scoped grant reaches every CMK in the account, not just CUDly's (#1969). Nothing on the deploy path needs it; if a CUDly symmetric key ever does, gate it like KMSMutateTaggedOnly in policy_compute.tf", f, statementSid(stmt), action, kmsResourceScopingConditionKey, kmsResourceScopingTagValue, kmsGrantIsForResourceConditionKey) } } })