seahaven-org-baseline/examples/hcptf-workspace-iam/hcp_iam.tf.example

265 lines
8.6 KiB
Text
Raw Normal View History

# Example: import an existing prod/dev hcptf-<stack> pair into app Terraform
# (PLAT-146). Copy into the consumer repo's terraform/ directory. Replace
# locals, then `terraform import` (or keep the import blocks) on a Manual
# apply. Do not recreate the role. Role names stay `hcptf-STACK` /
# `hcptf-STACK-plan`.
#
# Live `seahaven-hcptf-iam-management` DenySelfMutation blocks DetachRolePolicy
# and PutRolePolicy on hcptf-* (including this role). The stack workspace
# cannot apply this file while TFC_AWS_* still points at hcptf-STACK, and
# hcptf-bootstrap trust is exact StringEquals for workspace
# iam-bootstrap-<env> only (HCP names are org-unique). Import apply sequence:
# 1. scripts/create-hcptf-bootstrap-roles.sh --account prod|dev \
# --allow-workspace STACK-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-STACK / hcptf-STACK-plan.
# 5. Re-run the script without --allow-workspace to pin trust back to
# iam-bootstrap-<env> only.
# Later apply-role IAM edits use the same window. Do not add StringLike
# on bootstrap trust.
#
# SAM-only repos and SHOC/external-dev do not use this file.
locals {
account_id = "011934824531" # seahaven-prod; use 710827005802 for seahaven-dev
hcp_project = "seahaven-prod"
hcp_workspace = "STACK-prod"
apply_role = "hcptf-STACK"
plan_role = "hcptf-STACK-plan"
stack_name = "STACK"
stack_prefix = "STACK-"
}
data "aws_iam_policy_document" "hcptf_apply_trust" {
statement {
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:${local.hcp_project}:workspace:${local.hcp_workspace}:run_phase:apply",
]
}
}
}
data "aws_iam_policy_document" "hcptf_plan_trust" {
statement {
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:${local.hcp_project}:workspace:${local.hcp_workspace}:run_phase:plan",
]
}
}
}
# Rendered from lib/hcptf-bootstrap/scoped-apply-iam.json.tmpl. CreatePolicy
# stays on hcptf-bootstrap only. Exec-role writes are prefix-scoped. Boundary
# ARNs are StringLike-pinned (not Null); AdministratorAccess is not accepted.
# The role cannot PutRolePolicy on hcptf-* (including itself).
data "aws_iam_policy_document" "hcptf_scoped_iam" {
statement {
sid = "DenyCreatePolicy"
effect = "Deny"
actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"]
resources = ["*"]
}
statement {
sid = "CreateExecRoleWithBoundary"
effect = "Allow"
actions = ["iam:CreateRole"]
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
condition {
test = "StringLike"
variable = "iam:PermissionsBoundary"
values = [
"arn:aws:iam::${local.account_id}:policy/tf-managed/${local.stack_prefix}*",
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary",
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-${local.stack_name}",
]
}
}
statement {
sid = "MutateExecRoleWithBoundary"
effect = "Allow"
actions = [
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"iam:PutRolePermissionsBoundary",
]
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
condition {
test = "StringLike"
variable = "iam:PermissionsBoundary"
values = [
"arn:aws:iam::${local.account_id}:policy/tf-managed/${local.stack_prefix}*",
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary",
"arn:aws:iam::${local.account_id}:policy/seahaven-lambda-execution-boundary-${local.stack_name}",
]
}
}
statement {
sid = "WriteExecRoles"
effect = "Allow"
actions = [
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
]
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
}
statement {
sid = "PassExecRolesToCompute"
effect = "Allow"
actions = ["iam:PassRole"]
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*"]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["lambda.amazonaws.com", "ecs-tasks.amazonaws.com", "scheduler.amazonaws.com"]
}
}
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" "hcptf_apply" {
name = local.apply_role
assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json
max_session_duration = 3600
# Empty list detaches seahaven-hcptf-iam-management after import.
managed_policy_arns = []
tags = {
Owner = "adam@seahavenind.com"
ManagedBy = "terraform"
}
}
resource "aws_iam_role" "hcptf_plan" {
name = local.plan_role
assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json
max_session_duration = 3600
managed_policy_arns = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"]
tags = {
Owner = "adam@seahavenind.com"
ManagedBy = "terraform"
}
}
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
}
# Also import the existing service inline policy (name matches CFN PolicyName)
# and the plan-refresh sidecar. Copy those documents from
# lib/terraform-substrate/terraform-substrate.template.yaml. Do not invent a
# new Get* allow-list.
#
# import {
# to = aws_iam_role.hcptf_apply
# id = "hcptf-STACK"
# }
# import {
# to = aws_iam_role.hcptf_plan
# id = "hcptf-STACK-plan"
# }
# import {
# to = aws_iam_role_policy.services
# id = "hcptf-STACK:STACK-services"
# }
# import {
# to = aws_iam_role_policy.plan_refresh
# id = "hcptf-STACK-plan:STACK-plan-refresh"
# }