Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion terraform/modules/compute/aws/lambda/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,14 @@ resource "aws_iam_role_policy" "secrets_access" {
Action = [
"secretsmanager:GetSecretValue"
]
# Every "${arn}*" entry is guarded: an empty ARN would otherwise render
# the bare "*", which compact() keeps, granting GetSecretValue on every
# secret in the account (#291). var.database_password_secret_arn is
# also validated non-empty; the guard keeps this fail-closed if that
# input is ever made optional.
Resource = compact([
var.database_password_secret_arn,
"${var.database_password_secret_arn}*",
var.database_password_secret_arn != "" ? "${var.database_password_secret_arn}*" : "",
var.admin_password_secret_arn,
var.admin_password_secret_arn != "" ? "${var.admin_password_secret_arn}*" : "",
var.credential_encryption_key_secret_arn,
Expand Down
124 changes: 124 additions & 0 deletions terraform/modules/compute/aws/lambda/tests/secrets_policy.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Lambda secrets policy must fail closed on the database secret ARN (#291).
# The GetSecretValue resource list adds "${arn}*" for each secret; the
# database entry was unguarded, so an empty ARN rendered the bare "*" (which
# compact() keeps) and granted read on every secret in the account. The input
# is now validated, and the entry is guarded like its optional siblings.
mock_provider "aws" {}

variables {
stack_name = "cudly-test"
permissions_boundary_arn = "arn:aws:iam::123456789012:policy/cudly-boundary"
environment = "dev"
region = "us-east-1"
image_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/cudly:test"
database_host = "db.example.com"
database_name = "cudly"
database_username = "cudly"
database_password_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-db-AbCdEf"
admin_email = "admin@example.com"
allowed_origins = ["https://app.example.com"]
}

run "db_secret_only_grants_that_secret" {
command = plan

assert {
condition = jsondecode(aws_iam_role_policy.secrets_access.policy).Statement[0].Resource == [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-db-AbCdEf",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-db-AbCdEf*",
]
error_message = "With only the database secret set, GetSecretValue must cover exactly that ARN and its suffix pattern."
}

assert {
condition = alltrue([
for s in jsondecode(aws_iam_role_policy.secrets_access.policy).Statement :
!contains(flatten([s.Resource]), "*")
])
error_message = "No secrets policy statement may grant Resource \"*\"."
}
}

run "optional_secrets_add_their_own_patterns_only" {
command = plan

variables {
admin_password_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-admin-AbCdEf"
credential_encryption_key_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-credkey-AbCdEf"
}

assert {
condition = jsondecode(aws_iam_role_policy.secrets_access.policy).Statement[0].Resource == [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-db-AbCdEf",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-db-AbCdEf*",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-admin-AbCdEf",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-admin-AbCdEf*",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-credkey-AbCdEf",
"arn:aws:secretsmanager:us-east-1:123456789012:secret:cudly-dev-credkey-AbCdEf*",
]
error_message = "Each configured secret must contribute exactly its ARN and suffix pattern."
}
}

run "accepts_govcloud_partition" {
command = plan

variables {
database_password_secret_arn = "arn:aws-us-gov:secretsmanager:us-gov-west-1:123456789012:secret:cudly/db_pass-AbCdEf"
}

assert {
condition = contains(jsondecode(aws_iam_role_policy.secrets_access.policy).Statement[0].Resource, "arn:aws-us-gov:secretsmanager:us-gov-west-1:123456789012:secret:cudly/db_pass-AbCdEf*")
error_message = "A GovCloud secret ARN must be accepted and granted."
}
}

run "rejects_empty_db_secret_arn" {
command = plan

variables {
database_password_secret_arn = ""
}

expect_failures = [var.database_password_secret_arn]
}

run "rejects_bare_wildcard" {
command = plan

variables {
database_password_secret_arn = "*"
}

expect_failures = [var.database_password_secret_arn]
}

run "rejects_wildcard_secret_name" {
command = plan

variables {
database_password_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:*"
}

expect_failures = [var.database_password_secret_arn]
}

run "rejects_wildcard_account" {
command = plan

variables {
database_password_secret_arn = "arn:aws:secretsmanager:us-east-1:*:secret:cudly-dev-db"
}

expect_failures = [var.database_password_secret_arn]
}

run "rejects_non_secretsmanager_arn" {
command = plan

variables {
database_password_secret_arn = "arn:aws:ssm:us-east-1:123456789012:parameter/cudly-dev-db"
}

expect_failures = [var.database_password_secret_arn]
}
9 changes: 9 additions & 0 deletions terraform/modules/compute/aws/lambda/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ variable "database_username" {
variable "database_password_secret_arn" {
description = "ARN of Secrets Manager secret containing database password"
type = string

# The secrets policy grants GetSecretValue on this ARN and "${arn}*". An
# empty value would make that the bare "*" (every secret in the account),
# and a wildcard anywhere in it widens the grant the same way, so require a
# concrete Secrets Manager secret ARN (#291).
validation {
condition = can(regex("^arn:aws(-[a-z]+)*:secretsmanager:[a-z0-9-]+:[0-9]{12}:secret:[A-Za-z0-9/_+=.@-]+$", var.database_password_secret_arn))
error_message = "database_password_secret_arn must be a concrete Secrets Manager secret ARN (arn:<partition>:secretsmanager:<region>:<account-id>:secret:<name>) with no wildcards. An empty value would render the secrets policy resource as \"*\" and grant GetSecretValue on every secret in the account."
}
}

variable "admin_email" {
Expand Down
Loading