From 6b8cccb49f9a3460eb5258d9cc0f98bea0b899e2 Mon Sep 17 00:00:00 2001 From: Eric Conklin Date: Thu, 4 Jun 2026 00:40:43 -0500 Subject: [PATCH] Add prod-like Terraform IAM relationships --- .../aws/prod_like_accuracy_sandbox/README.md | 13 + .../terraform/main.tf | 303 ++++++++++++++++-- .../terraform/outputs.tf | 22 ++ .../test_prod_like_terraform_sandbox_files.py | 71 ++++ 4 files changed, 390 insertions(+), 19 deletions(-) diff --git a/tests/live/aws/prod_like_accuracy_sandbox/README.md b/tests/live/aws/prod_like_accuracy_sandbox/README.md index beae260..2373546 100644 --- a/tests/live/aws/prod_like_accuracy_sandbox/README.md +++ b/tests/live/aws/prod_like_accuracy_sandbox/README.md @@ -31,6 +31,19 @@ unless: - `live_ack` matches the required acknowledgement; - `resource_prefix` begins with `iamscope-prodlike-v1-`. +## IAM Relationship Model + +The source models frozen oracle rows with IAM-only resources: + +- source-principal inline policies for selected allow/precondition/inconclusive shapes; +- target-role trust policies for selected AWS service trust and direct assume-role shapes; +- permission boundaries attached to selected test users; +- an explicit deny policy attached to the selected deny probe user; +- IAM-only guardrail simulation policy attachments for selected blocked/unknown rows; +- one role inline policy for the bounded two-hop assume-role shape. + +Unsupported rows remain static-only and have no live resource mapping. + ## Future Commands, Do Not Run Until Phase 4 Approval ```bash diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf b/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf index d506174..1878154 100644 --- a/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/main.tf @@ -66,6 +66,182 @@ locals { scope_guardrail = "guardrail-scope" } + target_role_trust_services = { + lambda_exec_scoped = ["lambda.amazonaws.com"] + ecs_task_scoped = ["ecs-tasks.amazonaws.com"] + lambda_exec_boundary = ["lambda.amazonaws.com"] + service_mediated_target = ["lambda.amazonaws.com"] + } + + source_permission_boundary_keys = { + boundary_probe = "passrole_lambda" + uncertainty_probe = "session_context" + } + + source_inline_policy_specs = { + ci_deployer = { + statements = [ + { + sid = "OracleV001LambdaCreateAndOracleP001ServiceAction" + effect = "Allow" + actions = ["lambda:CreateFunction"] + resources = ["*"] + }, + { + sid = "OracleV001PassRoleToScopedLambdaRole" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [aws_iam_role.target["lambda_exec_scoped"].arn] + }, + { + sid = "OracleP002PassRoleToRoleWithoutServiceTrust" + effect = "Allow" + actions = ["iam:PassRole", "lambda:CreateFunction"] + resources = [aws_iam_role.target["readonly_ops"].arn] + }, + { + sid = "OracleV006ServiceMediatedPolicyShape" + effect = "Allow" + actions = ["lambda:CreateFunction", "iam:PassRole"] + resources = [aws_iam_role.target["service_mediated_target"].arn] + }, + ] + } + ecs_deployer = { + statements = [ + { + sid = "OracleV002EcsTaskRunShape" + effect = "Allow" + actions = ["ecs:RegisterTaskDefinition", "ecs:RunTask", "iam:PassRole"] + resources = [aws_iam_role.target["ecs_task_scoped"].arn] + }, + ] + } + helpdesk = { + statements = [ + { + sid = "OracleV003DirectAssumeRole" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["readonly_ops"].arn] + }, + { + sid = "OracleP003PassRoleWithoutServiceAction" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [aws_iam_role.target["lambda_exec_scoped"].arn] + }, + ] + } + build = { + statements = [ + { + sid = "OracleV004FirstHopAssumeRole" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["prod_observer"].arn] + }, + ] + } + audit = { + statements = [ + { + sid = "OracleV005CrossAccountShapedConditionSatisfied" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["audit_b"].arn] + condition = { + test = "StringEquals" + variable = "aws:PrincipalTag/IAMScopeSyntheticAccount" + values = ["synthetic-account-a"] + } + }, + { + sid = "OracleI005CrossAccountTrustConditionUnknown" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["audit_b"].arn] + condition = { + test = "StringEquals" + variable = "aws:RequestTag/IAMScopeUnknownContext" + values = ["synthetic-account-b"] + } + }, + ] + } + boundary_probe = { + statements = [ + { + sid = "OracleB001BoundaryBlockedPassRoleLambdaShape" + effect = "Allow" + actions = ["lambda:CreateFunction", "iam:PassRole"] + resources = [aws_iam_role.target["lambda_exec_boundary"].arn] + }, + { + sid = "OracleB002BoundaryBlockedChainContinuationShape" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["chain_target"].arn] + }, + ] + } + deny_probe = { + statements = [ + { + sid = "OracleB003ScpLikeGuardrailPassRoleShape" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [aws_iam_role.target["scp_passrole_target"].arn] + }, + { + sid = "OracleB004IdentityDenyAssumeRoleShape" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["denied_assume"].arn] + }, + { + sid = "OracleB005ExplicitDenyServiceMediatedShape" + effect = "Allow" + actions = ["lambda:CreateFunction", "iam:PassRole"] + resources = [aws_iam_role.target["service_mediated_target"].arn] + }, + { + sid = "OracleI004ScpLikeScopeUnknownShape" + effect = "Allow" + actions = ["iam:PassRole"] + resources = [aws_iam_role.target["scp_passrole_target"].arn] + }, + ] + } + uncertainty_probe = { + statements = [ + { + sid = "OracleI001WildcardResourceScopeUnknown" + effect = "Allow" + actions = ["iam:PassRole", "lambda:CreateFunction"] + resources = ["*"] + }, + { + sid = "OracleI002UnresolvedConditionKey" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["readonly_ops"].arn] + condition = { + test = "StringEquals" + variable = "aws:RequestTag/IAMScopeUnknownContext" + values = ["required-but-not-collected"] + } + }, + { + sid = "OracleI003SessionBoundaryContextMissing" + effect = "Allow" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["chain_target"].arn] + }, + ] + } + } + unsupported_static_only_rows = [ "oracle-u-001", "oracle-u-002", @@ -293,26 +469,11 @@ resource "terraform_data" "safety_guards" { } } -data "aws_iam_policy_document" "target_role_trust" { - for_each = local.target_roles - - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - - principals { - type = "AWS" - identifiers = [ - "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", - ] - } - } -} - resource "aws_iam_user" "source" { for_each = local.source_principals - name = "${var.resource_prefix}${each.value}" + name = "${var.resource_prefix}${each.value}" + permissions_boundary = contains(keys(local.source_permission_boundary_keys), each.key) ? aws_iam_policy.permission_boundary[local.source_permission_boundary_keys[each.key]].arn : null tags = merge(local.common_tags, { OracleFixture = "prod_like_aws_accuracy_oracle_v1" @@ -323,8 +484,30 @@ resource "aws_iam_user" "source" { resource "aws_iam_role" "target" { for_each = local.target_roles - name = "${var.resource_prefix}${each.value}" - assume_role_policy = data.aws_iam_policy_document.target_role_trust[each.key].json + name = "${var.resource_prefix}${each.value}" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = concat( + [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + } + ], + [ + for service in lookup(local.target_role_trust_services, each.key, []) : { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + Service = service + } + } + ] + ) + }) tags = merge(local.common_tags, { OracleFixture = "prod_like_aws_accuracy_oracle_v1" @@ -378,3 +561,85 @@ resource "aws_iam_policy" "guardrail_simulation" { SandboxRole = "guardrail-simulation" }) } + +resource "aws_iam_user_policy" "source_relationships" { + for_each = local.source_inline_policy_specs + + name = "${var.resource_prefix}${each.key}-oracle-relationships" + user = aws_iam_user.source[each.key].name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + for statement in each.value.statements : merge( + { + Sid = statement.sid + Effect = statement.effect + Action = statement.actions + Resource = statement.resources + }, + try({ + Condition = { + (statement.condition.test) = { + (statement.condition.variable) = statement.condition.values + } + } + }, {}) + ) + ] + }) +} + +resource "aws_iam_role_policy" "assume_chain_continuation" { + name = "${var.resource_prefix}prod-observer-chain-continuation" + role = aws_iam_role.target["prod_observer"].id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "OracleV004ContinuationAssumeRoleShape" + Effect = "Allow" + Action = ["sts:AssumeRole"] + Resource = [aws_iam_role.target["chain_target"].arn] + } + ] + }) +} + +data "aws_iam_policy_document" "identity_deny" { + statement { + sid = "OracleB004DenyAssumeRole" + effect = "Deny" + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.target["denied_assume"].arn] + } + + statement { + sid = "OracleB005DenyServiceMediatedPermission" + effect = "Deny" + actions = ["lambda:CreateFunction", "iam:PassRole"] + resources = [aws_iam_role.target["service_mediated_target"].arn] + } +} + +resource "aws_iam_policy" "identity_deny" { + name = "${var.resource_prefix}identity-deny-selected-oracle-rows" + description = "IAMScope prod-like v1 explicit deny policy for selected blocked oracle rows." + policy = data.aws_iam_policy_document.identity_deny.json + + tags = merge(local.common_tags, { + OracleFixture = "prod_like_aws_accuracy_oracle_v1" + SandboxRole = "identity-deny" + }) +} + +resource "aws_iam_user_policy_attachment" "identity_deny" { + user = aws_iam_user.source["deny_probe"].name + policy_arn = aws_iam_policy.identity_deny.arn +} + +resource "aws_iam_user_policy_attachment" "guardrail_simulation" { + for_each = aws_iam_policy.guardrail_simulation + + user = aws_iam_user.source["deny_probe"].name + policy_arn = each.value.arn +} diff --git a/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf b/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf index 05425eb..36c6587 100644 --- a/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf +++ b/tests/live/aws/prod_like_accuracy_sandbox/terraform/outputs.tf @@ -18,6 +18,28 @@ output "target_role_arns" { value = { for key, role in aws_iam_role.target : key => role.arn } } +output "source_inline_policy_names" { + description = "IAM source-principal inline policy names for future controlled collection." + value = { for key, policy in aws_iam_user_policy.source_relationships : key => policy.name } +} + +output "role_inline_policy_names" { + description = "IAM role inline policy names for future controlled collection." + value = { + prod_observer = aws_iam_role_policy.assume_chain_continuation.name + } +} + +output "identity_deny_policy_arn" { + description = "Explicit deny policy ARN for future controlled collection. This must be sanitized before commit." + value = aws_iam_policy.identity_deny.arn +} + +output "guardrail_simulation_policy_arns" { + description = "IAM-only guardrail simulation policy ARNs for future controlled collection. These must be sanitized before commit." + value = { for key, policy in aws_iam_policy.guardrail_simulation : key => policy.arn } +} + output "oracle_row_mapping" { description = "Static mapping from frozen oracle rows to planned Terraform resource groups." value = local.oracle_row_mapping diff --git a/tests/test_prod_like_terraform_sandbox_files.py b/tests/test_prod_like_terraform_sandbox_files.py index cf88992..a325f84 100644 --- a/tests/test_prod_like_terraform_sandbox_files.py +++ b/tests/test_prod_like_terraform_sandbox_files.py @@ -97,6 +97,55 @@ def test_provider_and_tags_are_bounded_to_aws_provider() -> None: assert 'ManagedBy = "Terraform"' in text +def test_terraform_contains_meaningful_iam_policy_relationships() -> None: + text = _terraform_text() + + assert 'resource "aws_iam_user_policy" "source_relationships"' in text + assert 'resource "aws_iam_role_policy" "assume_chain_continuation"' in text + assert 'resource "aws_iam_user_policy_attachment" "identity_deny"' in text + assert 'resource "aws_iam_user_policy_attachment" "guardrail_simulation"' in text + assert "permissions_boundary =" in text + assert '"lambda.amazonaws.com"' in text + assert '"ecs-tasks.amazonaws.com"' in text + + +def test_terraform_maps_oracle_rows_to_iam_policy_themes() -> None: + text = _terraform_text() + + required_terms = { + "OracleV001PassRoleToScopedLambdaRole", + "OracleV002EcsTaskRunShape", + "OracleV003DirectAssumeRole", + "OracleV004ContinuationAssumeRoleShape", + "OracleV005CrossAccountShapedConditionSatisfied", + "OracleV006ServiceMediatedPolicyShape", + "OracleB001BoundaryBlockedPassRoleLambdaShape", + "OracleB002BoundaryBlockedChainContinuationShape", + "OracleB003ScpLikeGuardrailPassRoleShape", + "OracleB004DenyAssumeRole", + "OracleB005DenyServiceMediatedPermission", + "OracleP001ServiceAction", + "OracleP002PassRoleToRoleWithoutServiceTrust", + "OracleP003PassRoleWithoutServiceAction", + "oracle-p-004", + "OracleI001WildcardResourceScopeUnknown", + "OracleI002UnresolvedConditionKey", + "OracleI003SessionBoundaryContextMissing", + "OracleI004ScpLikeScopeUnknownShape", + "OracleI005CrossAccountTrustConditionUnknown", + } + for term in required_terms: + assert term in text + + assert "lambda:CreateFunction" in text + assert "iam:PassRole" in text + assert "ecs:RegisterTaskDefinition" in text + assert "ecs:RunTask" in text + assert "sts:AssumeRole" in text + assert "Condition" in text + assert 'effect = "Deny"' in text + + def test_no_raw_non_synthetic_account_ids_or_iam_arns() -> None: text = _all_sandbox_text() account_ids = set(re.findall(r"\b[0-9]{12}\b", text)) @@ -145,7 +194,25 @@ def test_no_lambda_invocation_compute_networking_or_organizations_resources() -> assert "aws_subnet" not in lowered assert "aws_internet_gateway" not in lowered assert "aws_route_table" not in lowered + assert "aws_security_group" not in lowered + assert "aws_s3_bucket" not in lowered + assert "aws_instance" not in lowered + assert "aws_db_instance" not in lowered + assert "aws_rds" not in lowered + assert "aws_dynamodb_table" not in lowered assert "aws_organizations" not in lowered + assert "aws_iam_service_linked_role" not in lowered + + +def test_no_access_key_secret_or_raw_credential_resources() -> None: + text = _terraform_text().lower() + + assert "aws_iam_access_key" not in text + assert "aws_secretsmanager_secret" not in text + assert "aws_ssm_parameter" not in text + assert "secret_access_key" not in text + assert "session_token" not in text + assert "raw_credentials" not in text def test_readme_warns_not_to_run_until_phase4_and_lists_non_claims() -> None: @@ -158,6 +225,10 @@ def test_readme_warns_not_to_run_until_phase4_and_lists_non_claims() -> None: assert "Do not run until Phase 4 approval" in readme assert "expected_account_id" in readme assert "I_UNDERSTAND_THIS_IS_A_DEDICATED_IAMSCOPE_SANDBOX" in readme + assert "source-principal inline policies" in readme + assert "permission boundaries attached to selected test users" in readme + assert "explicit deny policy attached to the selected deny probe user" in readme + assert "Unsupported rows remain static-only" in readme for claim in NON_CLAIMS: assert claim in readme