fix(terraform): reject wildcard accounts in bedrock_model_arns

The variable already documented that wildcards in the account segment are
not accepted, and the validation's own error_message said they are rejected,
but the regex used [^:]* there, so arn:aws:bedrock:*:*:imported-model/* passed
and was copied verbatim into the task role's policy. Require the account
segment to be empty -- the canonical form for AWS-owned foundation models,
arn:aws:bedrock:*::foundation-model/anthropic.* -- or a literal 12-digit
account id, which is what every inference-profile, imported-model and
provisioned-model ARN carries.

Two runs pin both sides: a wildcard account is rejected, and a concrete
account in a cross-region inference-profile ARN still passes, so the
tightening cannot quietly cost the form operators actually paste.
This commit is contained in:
Raul Cajias 2026-09-07 19:31:31 +02:00
parent cc31ff5792
commit de5b231682
4 changed files with 44 additions and 7 deletions

View file

@ -220,9 +220,13 @@ foundation model in whichever region has capacity, so it needs both ARNs above:
the profile itself, and the foundation model wildcarded across every region the
profile can route to
Every entry must be a Bedrock ARN. A bare `"*"` is rejected at plan time, since
it would let the proxy's task role invoke every Bedrock resource in the account,
including other teams' provisioned throughput and private imported models
Every entry must be a Bedrock ARN naming a resource type, with the account
segment either empty (foundation models, which AWS owns) or a literal 12-digit
account id. A bare `"*"`, and any wildcard in the account or resource-type
segment, is rejected at plan time, since it would let the proxy's task role
invoke every Bedrock resource in the account, including other teams'
provisioned throughput and private imported models. Wildcards inside the
resource segment are what scope a family of models, and stay allowed
Two adjacent flags cover the rest of the Bedrock surface. `enable_bedrock_mantle`
grants `bedrock-mantle:CreateInference`, which the `bedrock_mantle/...` models

View file

@ -139,8 +139,10 @@ env = "stage"
# reference them in proxy_config as `bedrock/<model id>`. A cross-region
# inference profile needs both the profile ARN and the foundation-model ARN.
# Every entry must name a Bedrock resource type ("foundation-model/...",
# "inference-profile/...", "imported-model/..."); "*", "arn:aws:bedrock:*",
# and "arn:aws:bedrock:*:*:*" are rejected at plan time.
# "inference-profile/...", "imported-model/...") and carry either an empty
# account segment or a literal 12-digit account id; "*", "arn:aws:bedrock:*",
# "arn:aws:bedrock:*:*:*" and "arn:aws:bedrock:*:*:imported-model/*" are
# rejected at plan time.
# bedrock_model_arns = [
# "arn:aws:bedrock:*:111122223333:inference-profile/us.anthropic.*",
# "arn:aws:bedrock:*::foundation-model/anthropic.*",

View file

@ -180,6 +180,37 @@ run "an_all_segments_wildcard_bedrock_arn_is_rejected" {
]
}
# A wildcard in the account segment reads like a scoped ARN but matches every
# account the identity can reach, which is why the variable documents the account
# as "empty or a concrete id". The resource-type slug clause above does not catch
# it, so it needs its own guard and its own case.
run "an_account_wildcard_bedrock_arn_is_rejected" {
command = plan
variables {
bedrock_model_arns = ["arn:aws:bedrock:us-east-1:*:imported-model/*"]
}
expect_failures = [
var.bedrock_model_arns,
]
}
# The mirror of the case above: tightening the account segment must not cost the
# ARN form operators actually paste for a cross-region inference profile.
run "a_concrete_account_inference_profile_arn_is_accepted" {
command = plan
variables {
bedrock_model_arns = ["arn:aws:bedrock:*:111122223333:inference-profile/us.anthropic.*"]
}
assert {
condition = length(aws_iam_policy.bedrock_invoke) == 1
error_message = "A 12-digit account in an inference-profile ARN must stay valid."
}
}
run "a_china_partition_bedrock_arn_is_accepted" {
command = plan

View file

@ -591,8 +591,8 @@ variable "bedrock_model_arns" {
default = []
validation {
condition = alltrue([for a in var.bedrock_model_arns : can(regex("^arn:aws[a-z-]*:bedrock:[^:]*:[^:]*:[a-z-]+/", a))])
error_message = "Every bedrock_model_arns entry must be a Bedrock ARN with a real resource type, e.g. arn:aws:bedrock:*::foundation-model/anthropic.*. Bare wildcards on the account or resource-type segment (\"*\", \"arn:aws:bedrock:*\", \"arn:aws:bedrock:*:*:*\") are rejected, because they would let the task role invoke every Bedrock resource in the account, including other teams' provisioned throughput and private imported models."
condition = alltrue([for a in var.bedrock_model_arns : can(regex("^arn:aws[a-z-]*:bedrock:[^:]*:([0-9]{12})?:[a-z-]+/", a))])
error_message = "Every bedrock_model_arns entry must be a Bedrock ARN with a real resource type, e.g. arn:aws:bedrock:*::foundation-model/anthropic.*. The account segment must be empty (foundation models) or a literal 12-digit account id; wildcards on the account or resource-type segment (\"*\", \"arn:aws:bedrock:*\", \"arn:aws:bedrock:*:*:*\", \"arn:aws:bedrock:*:*:imported-model/*\") are rejected, because they would let the task role invoke every Bedrock resource in the account, including other teams' provisioned throughput and private imported models."
}
}