shoc-backend/terraform/bootstrap/live_workspace_roles.tf
Adam Moussa 25c2e84e8f feat(terraform): adopt live deployment roles safely
Add import-only dev and staging state with least-privilege HCP authentication and plan safety guards.
2026-08-28 19:12:34 -04:00

215 lines
6.2 KiB
HCL

locals {
live_certificate_arn = "arn:aws:acm:${var.aws_region}:${local.account_id}:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00"
live_github_oidc_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
live_rds_arn = "arn:aws:rds:${var.aws_region}:${local.account_id}:db:shoc-sqlserver-shared"
live_environments = {
dev = {
workspace = "shoc-backend-dev"
deploy_role_name = "githubdeploy-shoc-backend-dev"
runtime_role_name = "shoc-backend-dev"
instance_profile_name = "shoc-backend-dev"
hosted_zone_id = "Z07671212N75U4YLPWZR8"
}
staging = {
workspace = "shoc-backend-staging"
deploy_role_name = "githubdeploy-shoc-backend-staging"
runtime_role_name = "shoc-backend-staging"
instance_profile_name = "shoc-backend-staging"
hosted_zone_id = "Z02602739VQWBWCAGXP4"
}
}
}
data "aws_iam_policy_document" "live_plan_assume" {
for_each = local.live_environments
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [local.tfc_oidc_arn]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:aud"
values = ["aws.workload.identity"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:sub"
values = ["organization:${var.hcp_organization}:project:${var.hcp_project}:workspace:${each.value.workspace}:run_phase:plan"]
}
}
}
data "aws_iam_policy_document" "live_apply_assume" {
for_each = local.live_environments
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [local.tfc_oidc_arn]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:aud"
values = ["aws.workload.identity"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:sub"
values = ["organization:${var.hcp_organization}:project:${var.hcp_project}:workspace:${each.value.workspace}:run_phase:apply"]
}
}
}
data "aws_iam_policy_document" "live_plan" {
for_each = local.live_environments
statement {
sid = "CallerIdentity"
effect = "Allow"
actions = ["sts:GetCallerIdentity"]
resources = ["*"]
}
statement {
sid = "ReadExactIamResources"
effect = "Allow"
actions = [
"iam:GetInstanceProfile",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfileTags",
"iam:ListInstanceProfilesForRole",
"iam:ListRolePolicies",
"iam:ListRoleTags",
]
resources = [
"arn:aws:iam::${local.account_id}:role/${each.value.deploy_role_name}",
"arn:aws:iam::${local.account_id}:role/${each.value.runtime_role_name}",
"arn:aws:iam::${local.account_id}:instance-profile/${each.value.instance_profile_name}",
local.eb_service_role_arn,
]
}
statement {
sid = "ReadGithubOidc"
effect = "Allow"
actions = ["iam:GetOpenIDConnectProvider"]
resources = [local.live_github_oidc_arn]
}
statement {
sid = "ReadSharedInventory"
effect = "Allow"
actions = [
"acm:ListCertificates",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs",
"iam:ListOpenIDConnectProviders",
"rds:DescribeDBInstances",
"route53:ListHostedZonesByName",
]
# These AWS read APIs do not support resource-level permissions.
resources = ["*"]
}
statement {
sid = "ReadPinnedCertificate"
effect = "Allow"
actions = ["acm:DescribeCertificate", "acm:ListTagsForCertificate"]
resources = [local.live_certificate_arn]
}
statement {
sid = "ReadSharedRdsTags"
effect = "Allow"
actions = ["rds:ListTagsForResource"]
resources = [local.live_rds_arn]
}
statement {
sid = "ReadPinnedHostedZone"
effect = "Allow"
actions = ["route53:GetHostedZone", "route53:ListResourceRecordSets", "route53:ListTagsForResource"]
resources = ["arn:aws:route53:::hostedzone/${each.value.hosted_zone_id}"]
}
}
data "aws_iam_policy_document" "live_apply" {
for_each = local.live_environments
source_policy_documents = [data.aws_iam_policy_document.live_plan[each.key].json]
statement {
sid = "UpdateImportedDeployRole"
effect = "Allow"
actions = [
"iam:PutRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:UpdateRole",
]
resources = ["arn:aws:iam::${local.account_id}:role/${each.value.deploy_role_name}"]
}
}
resource "aws_iam_role" "live_plan" {
for_each = local.live_environments
name = "hcptf-shoc-backend-${each.key}-plan"
description = "Import/read-only HCP Terraform plan role for shoc-backend ${each.key}."
assume_role_policy = data.aws_iam_policy_document.live_plan_assume[each.key].json
max_session_duration = 3600
permissions_boundary = var.execution_boundary_arn
depends_on = [terraform_data.account_guard]
lifecycle {
prevent_destroy = true
}
}
resource "aws_iam_role_policy" "live_plan" {
for_each = local.live_environments
name = "shoc-backend-${each.key}-import-plan"
role = aws_iam_role.live_plan[each.key].id
policy = data.aws_iam_policy_document.live_plan[each.key].json
}
resource "aws_iam_role" "live_apply" {
for_each = local.live_environments
name = "hcptf-shoc-backend-${each.key}"
description = "Import/update-only HCP Terraform apply role for shoc-backend ${each.key}."
assume_role_policy = data.aws_iam_policy_document.live_apply_assume[each.key].json
max_session_duration = 3600
permissions_boundary = var.execution_boundary_arn
depends_on = [terraform_data.account_guard]
lifecycle {
prevent_destroy = true
}
}
resource "aws_iam_role_policy" "live_apply" {
for_each = local.live_environments
name = "shoc-backend-${each.key}-import-apply"
role = aws_iam_role.live_apply[each.key].id
policy = data.aws_iam_policy_document.live_apply[each.key].json
}