From a84d3199c8548a1ce13ad508bf862d0cb75d4a86 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Wed, 2 Sep 2026 14:49:06 -0400 Subject: [PATCH] feat(iam): import hcptf roles into app Terraform (PLAT-146) Move the existing hcptf pair into this repo so app Terraform owns prod IAM after the substrate handoff. --- terraform/hcp_iam.tf | 459 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 terraform/hcp_iam.tf diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf new file mode 100644 index 0000000..91b3867 --- /dev/null +++ b/terraform/hcp_iam.tf @@ -0,0 +1,459 @@ +# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146). +# Import, do not recreate. Role names stay hcptf-sh-openswe-traces / hcptf-sh-openswe-traces-plan. +# +# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy +# and PutRolePolicy on hcptf-* (including this role). Import apply sequence: +# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh +# --account prod --allow-workspace sh-openswe-traces-prod +# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / +# hcptf-bootstrap-plan (workspace vars, never a project set). +# 3. One Manual apply (import + detach seahaven-hcptf-iam-management + +# put scoped inline). +# 4. Point TFC_AWS_* back at hcptf-sh-openswe-traces / hcptf-sh-openswe-traces-plan. +# 5. Re-run the script without --allow-workspace to pin trust back to +# iam-bootstrap-prod only. +# This stack has no Lambda execution-role boundary pin. + +import { + to = aws_iam_role.hcptf_apply + id = "hcptf-sh-openswe-traces" +} + +import { + to = aws_iam_role.hcptf_plan + id = "hcptf-sh-openswe-traces-plan" +} + +import { + to = aws_iam_role_policy.hcptf_apply_services + id = "hcptf-sh-openswe-traces:sh-openswe-traces-services" +} + +import { + to = aws_iam_role_policy.hcptf_plan_refresh + id = "hcptf-sh-openswe-traces-plan:sh-openswe-traces-plan-refresh" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_apply + id = "hcptf-sh-openswe-traces" +} + +import { + to = aws_iam_role_policy_attachment.hcptf_plan_viewonly + id = "hcptf-sh-openswe-traces-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +import { + to = aws_iam_role_policy_attachments_exclusive.hcptf_plan + id = "hcptf-sh-openswe-traces-plan" +} + +data "aws_iam_policy_document" "hcptf_apply_trust" { + statement { + sid = "HcpApply" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:sh-openswe-traces-prod:run_phase:apply", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_plan_trust" { + statement { + sid = "HcpPlan" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + + condition { + test = "StringEquals" + variable = "app.terraform.io:sub" + values = [ + "organization:seahaven:project:seahaven-prod:workspace:sh-openswe-traces-prod:run_phase:plan", + ] + } + } +} + +data "aws_iam_policy_document" "hcptf_scoped_iam" { + statement { + sid = "DenyCreatePolicy" + effect = "Deny" + actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"] + resources = ["*"] + } + + statement { + sid = "IamReadOnly" + effect = "Allow" + actions = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoles", + ] + resources = ["*"] + } + + statement { + sid = "DenySelfMutation" + effect = "Deny" + actions = [ + "iam:AttachRolePolicy", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DeleteRolePermissionsBoundary", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + "iam:UpdateRoleDescription", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/hcptf-*", + "arn:aws:iam::${local.account_id}:role/github-cfn-execution-role", + "arn:aws:iam::${local.account_id}:role/githubdeploy-*", + "arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*", + "arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole", + "arn:aws:iam::${local.account_id}:role/seahaven-*", + ] + } + + statement { + sid = "DenyBoundaryTampering" + effect = "Deny" + actions = [ + "iam:DeleteRolePermissionsBoundary", + "iam:DeleteUserPermissionsBoundary", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/*", + "arn:aws:iam::${local.account_id}:user/*", + ] + } + + statement { + sid = "DenyBoundaryPolicyEdit" + effect = "Deny" + actions = [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion", + ] + resources = ["arn:aws:iam::${local.account_id}:policy/seahaven-*"] + } +} + +resource "aws_iam_role_policy" "hcptf_apply_services" { + name = "sh-openswe-traces-services" + role = aws_iam_role.hcptf_apply.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "s3:*", + ] + Resource = [ + "arn:aws:s3:::sh-openswe-traces-${local.account_id}", + "arn:aws:s3:::sh-openswe-traces-${local.account_id}/*", + "arn:aws:s3:::sh-openswe-traces-logs-${local.account_id}", + "arn:aws:s3:::sh-openswe-traces-logs-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "TracesBuckets" + }, + { + Condition = { + StringEquals = { + "aws:RequestTag/Project" = "sh-openswe-traces" + } + } + Action = [ + "kms:CreateKey", + ] + Resource = "*" + Effect = "Allow" + Sid = "TracesKmsCreate" + }, + { + Action = [ + "kms:ListAliases", + ] + Resource = "*" + Effect = "Allow" + Sid = "TracesKmsList" + }, + { + Action = [ + "kms:CreateAlias", + "kms:UpdateAlias", + "kms:DeleteAlias", + ] + Resource = [ + "arn:aws:kms:us-east-1:${local.account_id}:alias/sh-openswe-traces", + ] + Effect = "Allow" + Sid = "TracesKmsAlias" + }, + { + Condition = { + StringEquals = { + "aws:ResourceTag/Project" = "sh-openswe-traces" + } + } + Action = [ + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:EnableKeyRotation", + "kms:DisableKeyRotation", + "kms:PutKeyPolicy", + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:EnableKey", + "kms:DisableKey", + "kms:CreateAlias", + "kms:UpdateAlias", + "kms:DeleteAlias", + ] + Resource = "*" + Effect = "Allow" + Sid = "TracesKmsKey" + }, + { + Action = [ + "iam:CreateUser", + "iam:DeleteUser", + "iam:GetUser", + "iam:TagUser", + "iam:UntagUser", + "iam:UpdateUser", + "iam:PutUserPolicy", + "iam:DeleteUserPolicy", + "iam:GetUserPolicy", + "iam:ListUserPolicies", + "iam:ListAttachedUserPolicies", + "iam:ListUserTags", + "iam:ListAccessKeys", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:user/sh-openswe-langsmith-export", + ] + Effect = "Allow" + Sid = "ExportIamUser" + }, + { + Action = [ + "iam:ListUsers", + "iam:GetAccountSummary", + ] + Resource = "*" + Effect = "Allow" + Sid = "ExportIamUserList" + }, + { + Action = [ + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:PutResourcePolicy", + "secretsmanager:DeleteResourcePolicy", + "secretsmanager:TagResource", + "secretsmanager:UntagResource", + ] + Resource = [ + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:sh-openswe/langsmith-export-s3-*", + ] + Effect = "Allow" + Sid = "ExportSecretShell" + }, + { + Condition = { + StringEquals = { + "secretsmanager:Name" = "sh-openswe/langsmith-export-s3" + } + } + Action = [ + "secretsmanager:CreateSecret", + ] + Resource = "*" + Effect = "Allow" + Sid = "ExportSecretCreate" + }, + ] + }) +} + +resource "aws_iam_role_policy" "hcptf_plan_refresh" { + name = "sh-openswe-traces-plan-refresh" + role = aws_iam_role.hcptf_plan.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "iam:GetUser", + "iam:GetUserPolicy", + "iam:ListUserPolicies", + "iam:ListAttachedUserPolicies", + "iam:ListUserTags", + "iam:GetAccessKeyLastUsed", + "iam:ListAccessKeys", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:user/sh-openswe-langsmith-export", + ] + Effect = "Allow" + Sid = "RefreshIamUser" + }, + { + Action = [ + "iam:GetPolicy", + "iam:GetPolicyVersion", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshManagedPolicies" + }, + { + Action = [ + "s3:Get*", + "s3:ListBucket", + ] + Resource = [ + "arn:aws:s3:::sh-openswe-traces-${local.account_id}", + "arn:aws:s3:::sh-openswe-traces-${local.account_id}/*", + "arn:aws:s3:::sh-openswe-traces-logs-${local.account_id}", + "arn:aws:s3:::sh-openswe-traces-logs-${local.account_id}/*", + ] + Effect = "Allow" + Sid = "RefreshBuckets" + }, + { + Action = [ + "kms:Describe*", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:ListAliases", + ] + Resource = "*" + Effect = "Allow" + Sid = "RefreshKms" + }, + { + Action = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds", + ] + Resource = [ + "arn:aws:secretsmanager:us-east-1:${local.account_id}:secret:sh-openswe/langsmith-export-s3-*", + ] + Effect = "Allow" + Sid = "RefreshSecret" + }, + { + Sid = "RefreshHcptfRoles" + Effect = "Allow" + Action = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + ] + Resource = [ + "arn:aws:iam::${local.account_id}:role/hcptf-sh-openswe-traces", + "arn:aws:iam::${local.account_id}:role/hcptf-sh-openswe-traces-plan", + ] + }, + ] + }) +} + +resource "aws_iam_role" "hcptf_apply" { + name = "hcptf-sh-openswe-traces" + assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json + max_session_duration = 3600 + + tags = { + Project = "sh-openswe-traces" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +# Empty exclusive set keeps seahaven-hcptf-iam-management detached. +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" { + role_name = aws_iam_role.hcptf_apply.name + policy_arns = [] +} + +resource "aws_iam_role" "hcptf_plan" { + name = "hcptf-sh-openswe-traces-plan" + assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json + max_session_duration = 3600 + + tags = { + Project = "sh-openswe-traces" + Owner = "adam@seahavenind.com" + ManagedBy = "terraform" + } +} + +resource "aws_iam_role_policy_attachment" "hcptf_plan_viewonly" { + role = aws_iam_role.hcptf_plan.name + policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" { + role_name = aws_iam_role.hcptf_plan.name + policy_arns = [ + aws_iam_role_policy_attachment.hcptf_plan_viewonly.policy_arn, + ] +} + +resource "aws_iam_role_policy" "hcptf_scoped_iam" { + name = "scoped-iam-management" + role = aws_iam_role.hcptf_apply.id + policy = data.aws_iam_policy_document.hcptf_scoped_iam.json +}