From 0a903a4ed8df97f8f8cf36142bbeacf2b787728a Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Mon, 24 Aug 2026 12:32:56 -0300 Subject: [PATCH 1/5] fix(rds-postgres-server): grant tag-scoped KMS permissions for storage encryption rds-postgres-server/deployment always creates its own customer-managed KMS key for RDS storage encryption (required for AVD-AWS-0079 compliance, not optional), but specs/requirements/aws granted no kms:* actions at all, causing CreateKey/TagResource to fail with AccessDenied at apply time. Scope the new policy by the "managed-by=nullplatform" tag the deployment module already applies to the key, rather than "Resource": "*", so the role can only create/manage CMKs it tagged itself and can never touch any other KMS key in the account. --- .../specs/requirements/aws/main.tf | 70 +++++++++++++++++++ .../specs/requirements/aws/output.tf | 5 ++ 2 files changed, 75 insertions(+) diff --git a/rds-postgres-server/specs/requirements/aws/main.tf b/rds-postgres-server/specs/requirements/aws/main.tf index b068cf2..07a9a96 100644 --- a/rds-postgres-server/specs/requirements/aws/main.tf +++ b/rds-postgres-server/specs/requirements/aws/main.tf @@ -48,6 +48,12 @@ resource "aws_iam_role_policy_attachment" "rds_s3" { policy_arn = aws_iam_policy.nullplatform_rds_s3_policy[0].arn } +resource "aws_iam_role_policy_attachment" "rds_kms" { + count = local.iam_create ? 1 : 0 + role = aws_iam_role.nullplatform_rds_postgres_server[0].name + policy_arn = aws_iam_policy.nullplatform_rds_kms_policy[0].arn +} + ################################################################################ # RDS IAM policy ################################################################################ @@ -196,3 +202,67 @@ resource "aws_iam_policy" "nullplatform_rds_secretsmanager_policy" { ] }) } + +################################################################################ +# KMS IAM policy +# +# rds-postgres-server/deployment always creates its own customer-managed KMS +# key for RDS storage encryption (not optional — required for AVD-AWS-0079 +# compliance) and tags it "managed-by" = "nullplatform". Scoped by that tag +# (not "Resource" : "*") so this role can create and manage only the CMKs it +# tags itself — it can never touch, disable, or schedule deletion of any +# other KMS key in the account. kms:CreateKey can't be scoped to a specific +# key (it doesn't exist yet), so it's gated on the tag being requested at +# creation time instead (aws:RequestTag); every other action is gated on the +# tag already present on the key (aws:ResourceTag). +################################################################################ + +# Grant permissions to create and manage the customer-managed KMS key used +# for RDS storage encryption, scoped to keys this role itself tags +resource "aws_iam_policy" "nullplatform_rds_kms_policy" { + count = local.iam_create ? 1 : 0 + + name = "${local.policies_name_prefix}-rds-kms-policy" + description = "Policy for managing the customer-managed KMS key used for RDS storage encryption, scoped to keys tagged managed-by=nullplatform" + + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Sid" : "CreateOwnCMK", + "Effect" : "Allow", + "Action" : "kms:CreateKey", + "Resource" : "*", + "Condition" : { + "StringEquals" : { "aws:RequestTag/managed-by" : "nullplatform" } + } + }, + { + "Sid" : "ManageOwnCMK", + "Effect" : "Allow", + "Action" : [ + "kms:TagResource", + "kms:DescribeKey", + "kms:EnableKeyRotation", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ], + "Resource" : "*", + "Condition" : { + "StringEquals" : { "aws:ResourceTag/managed-by" : "nullplatform" } + } + }, + { + "Sid" : "ManageOwnAlias", + "Effect" : "Allow", + "Action" : ["kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias"], + "Resource" : "arn:aws:kms:*:${data.aws_caller_identity.current.account_id}:alias/nullplatform-*" + } + ] + }) +} diff --git a/rds-postgres-server/specs/requirements/aws/output.tf b/rds-postgres-server/specs/requirements/aws/output.tf index 041efb6..76570eb 100644 --- a/rds-postgres-server/specs/requirements/aws/output.tf +++ b/rds-postgres-server/specs/requirements/aws/output.tf @@ -13,6 +13,11 @@ output "rds_secretsmanager_policy_arn" { value = local.iam_create ? aws_iam_policy.nullplatform_rds_secretsmanager_policy[0].arn : "" } +output "rds_kms_policy_arn" { + description = "ARN of the KMS policy for the customer-managed RDS storage encryption key" + value = local.iam_create ? aws_iam_policy.nullplatform_rds_kms_policy[0].arn : "" +} + output "permissions_role_arn" { description = "ARN of the rds-postgres-server permissions role assumed by the nullplatform agent role. Pass to the agent (assume_role_arns)." value = local.iam_create ? aws_iam_role.nullplatform_rds_postgres_server[0].arn : "" From c3610c6f738f8a96db3b9dffb27253c2c812f311 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Mon, 24 Aug 2026 13:14:22 -0300 Subject: [PATCH 2/5] fix(rds-postgres-server): grant kms:GetKeyPolicy for CMK read-back Live test surfaced a second missing permission after the previous kms:CreateKey/TagResource fix: the AWS provider reads the key policy back right after creating aws_kms_key.rds, which needs kms:GetKeyPolicy. Add it to the same tag-scoped ManageOwnCMK statement. --- rds-postgres-server/specs/requirements/aws/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/rds-postgres-server/specs/requirements/aws/main.tf b/rds-postgres-server/specs/requirements/aws/main.tf index 07a9a96..6f5a86a 100644 --- a/rds-postgres-server/specs/requirements/aws/main.tf +++ b/rds-postgres-server/specs/requirements/aws/main.tf @@ -243,6 +243,7 @@ resource "aws_iam_policy" "nullplatform_rds_kms_policy" { "Action" : [ "kms:TagResource", "kms:DescribeKey", + "kms:GetKeyPolicy", "kms:EnableKeyRotation", "kms:GetKeyRotationStatus", "kms:ListResourceTags", From f4c21257c7a00d1ac21652c43834f8feb4177589 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Mon, 24 Aug 2026 13:32:13 -0300 Subject: [PATCH 3/5] fix(rds-postgres-server): grant kms:*Alias on the key resource too Live test surfaced a third missing permission: kms:CreateAlias authorizes against BOTH the alias ARN and the target key ARN as separate resource checks. The previous fix only covered the alias-ARN side (ManageOwnAlias); add the same three actions to the tag-scoped ManageOwnCMK statement so the key-side check passes too. --- rds-postgres-server/specs/requirements/aws/main.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rds-postgres-server/specs/requirements/aws/main.tf b/rds-postgres-server/specs/requirements/aws/main.tf index 6f5a86a..e1d4214 100644 --- a/rds-postgres-server/specs/requirements/aws/main.tf +++ b/rds-postgres-server/specs/requirements/aws/main.tf @@ -251,7 +251,10 @@ resource "aws_iam_policy" "nullplatform_rds_kms_policy" { "kms:CancelKeyDeletion", "kms:CreateGrant", "kms:ListGrants", - "kms:RevokeGrant" + "kms:RevokeGrant", + "kms:CreateAlias", + "kms:DeleteAlias", + "kms:UpdateAlias" ], "Resource" : "*", "Condition" : { @@ -259,6 +262,9 @@ resource "aws_iam_policy" "nullplatform_rds_kms_policy" { } }, { + # kms:*Alias actions authorize against BOTH the alias ARN and the + # target key ARN (two separate resource checks) — ManageOwnCMK above + # covers the key side (tag-scoped); this covers the alias side. "Sid" : "ManageOwnAlias", "Effect" : "Allow", "Action" : ["kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias"], From 1019ad8f0561d178a8e4de6c22ebadaab71bd876 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Mon, 24 Aug 2026 13:55:39 -0300 Subject: [PATCH 4/5] fix(rds-postgres-server): grant kms:ListAliases for alias read-back Live test surfaced a fourth missing permission: KMS has no DescribeAlias API, so the AWS provider reads aws_kms_alias back via ListAliases, which only supports "Resource": "*" (it enumerates every alias in the account/region, with no per-item ARN or tag condition available). Read-only: exposes alias names, not key material or policies. --- rds-postgres-server/specs/requirements/aws/main.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rds-postgres-server/specs/requirements/aws/main.tf b/rds-postgres-server/specs/requirements/aws/main.tf index e1d4214..19e861a 100644 --- a/rds-postgres-server/specs/requirements/aws/main.tf +++ b/rds-postgres-server/specs/requirements/aws/main.tf @@ -269,6 +269,17 @@ resource "aws_iam_policy" "nullplatform_rds_kms_policy" { "Effect" : "Allow", "Action" : ["kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias"], "Resource" : "arn:aws:kms:*:${data.aws_caller_identity.current.account_id}:alias/nullplatform-*" + }, + { + # KMS has no DescribeAlias API — the AWS provider reads an alias back + # via ListAliases, which only supports "Resource": "*" (it enumerates + # every alias in the account/region; there's no per-item ARN or tag + # condition to scope it to just nullplatform-*). Read-only: exposes + # alias names, not key material or policies. + "Sid" : "ListAliases", + "Effect" : "Allow", + "Action" : "kms:ListAliases", + "Resource" : "*" } ] }) From 4977c72519d68126ecde96a374f90ef9a50f3c1c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Mon, 24 Aug 2026 16:37:19 -0300 Subject: [PATCH 5/5] style(rds-postgres-server): trim explanatory comments from the KMS policy Keep the section header/one-liner pattern the rest of the file already uses; the detailed rationale lives in the PR description instead. --- .../specs/requirements/aws/main.tf | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/rds-postgres-server/specs/requirements/aws/main.tf b/rds-postgres-server/specs/requirements/aws/main.tf index 19e861a..1dc1136 100644 --- a/rds-postgres-server/specs/requirements/aws/main.tf +++ b/rds-postgres-server/specs/requirements/aws/main.tf @@ -205,16 +205,6 @@ resource "aws_iam_policy" "nullplatform_rds_secretsmanager_policy" { ################################################################################ # KMS IAM policy -# -# rds-postgres-server/deployment always creates its own customer-managed KMS -# key for RDS storage encryption (not optional — required for AVD-AWS-0079 -# compliance) and tags it "managed-by" = "nullplatform". Scoped by that tag -# (not "Resource" : "*") so this role can create and manage only the CMKs it -# tags itself — it can never touch, disable, or schedule deletion of any -# other KMS key in the account. kms:CreateKey can't be scoped to a specific -# key (it doesn't exist yet), so it's gated on the tag being requested at -# creation time instead (aws:RequestTag); every other action is gated on the -# tag already present on the key (aws:ResourceTag). ################################################################################ # Grant permissions to create and manage the customer-managed KMS key used @@ -262,20 +252,12 @@ resource "aws_iam_policy" "nullplatform_rds_kms_policy" { } }, { - # kms:*Alias actions authorize against BOTH the alias ARN and the - # target key ARN (two separate resource checks) — ManageOwnCMK above - # covers the key side (tag-scoped); this covers the alias side. "Sid" : "ManageOwnAlias", "Effect" : "Allow", "Action" : ["kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias"], "Resource" : "arn:aws:kms:*:${data.aws_caller_identity.current.account_id}:alias/nullplatform-*" }, { - # KMS has no DescribeAlias API — the AWS provider reads an alias back - # via ListAliases, which only supports "Resource": "*" (it enumerates - # every alias in the account/region; there's no per-item ARN or tag - # condition to scope it to just nullplatform-*). Read-only: exposes - # alias names, not key material or policies. "Sid" : "ListAliases", "Effect" : "Allow", "Action" : "kms:ListAliases",