forgejo/terraform/hcp_iam.tf

805 lines
21 KiB
Terraform
Raw Normal View History

# HCP plan/apply roles for forgejo-prod (PLAT-80).
# Copy of seahaven-org-baseline/examples/hcptf-workspace-iam/hcp_iam.tf.example
# with the Forgejo EC2, ALB, S3 CRR, DLM, and Lambda service set. Create, do not import.
#
# First-apply sequence:
# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh
# --account prod --allow-workspace forgejo-prod
# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / hcptf-bootstrap-plan
# (workspace vars, never a project set).
# 3. One Manual apply. Bootstrap can write hcptf-* and policy/tf-managed/*.
# It cannot PassRole to ec2 or create the buckets, so non-IAM resources
# error and stay out of state.
# 4. Point TFC_AWS_* at hcptf-forgejo / hcptf-forgejo-plan.
# 5. Re-run the script without --allow-workspace.
# 6. Second Manual apply creates the instance, buckets, ALB, and Lambda.
# Later edits to these hcptf-* inline policies need the same window.
# DenySelfMutation blocks PutRolePolicy on hcptf-* from the scoped role.
# Do not add StringLike on bootstrap trust. CreatePolicy stays on hcptf-bootstrap.
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:${local.hcp_project}:workspace:${local.hcp_workspace}: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:${local.hcp_project}:workspace:${local.hcp_workspace}:run_phase:plan",
]
}
}
}
data "aws_iam_policy_document" "hcptf_scoped_iam" {
statement {
sid = "DenyCreatePolicy"
effect = "Deny"
actions = [
"iam:CreatePolicy",
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
]
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}*",
]
}
}
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}*",
]
}
}
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 = "PassInstanceRoleToEc2"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
"arn:aws:iam::${local.account_id}:role/tf-managed/${local.instance_role_name}",
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["ec2.amazonaws.com"]
}
}
statement {
sid = "PassDlmRole"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
"arn:aws:iam::${local.account_id}:role/tf-managed/${local.dlm_role_name}",
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["dlm.amazonaws.com"]
}
}
statement {
sid = "PassReplicationRoleToS3"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
"arn:aws:iam::${local.account_id}:role/tf-managed/${local.replication_role_name}",
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["s3.amazonaws.com"]
}
}
statement {
sid = "PassLambdaRole"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
"arn:aws:iam::${local.account_id}:role/tf-managed/${local.lambda_role_name}",
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["lambda.amazonaws.com"]
}
}
statement {
sid = "InstanceProfiles"
effect = "Allow"
actions = [
"iam:AddRoleToInstanceProfile",
"iam:CreateInstanceProfile",
"iam:DeleteInstanceProfile",
"iam:GetInstanceProfile",
"iam:ListInstanceProfileTags",
"iam:RemoveRoleFromInstanceProfile",
"iam:TagInstanceProfile",
"iam:UntagInstanceProfile",
]
resources = [
"arn:aws:iam::${local.account_id}:instance-profile/tf-managed/${local.instance_profile_name}",
]
}
statement {
sid = "GcsTransferUser"
effect = "Allow"
actions = [
"iam:CreateUser",
"iam:DeleteUser",
"iam:GetUser",
"iam:GetUserPolicy",
"iam:ListUserPolicies",
"iam:ListUserTags",
"iam:PutUserPermissionsBoundary",
"iam:PutUserPolicy",
"iam:DeleteUserPolicy",
"iam:TagUser",
"iam:UntagUser",
]
resources = [
"arn:aws:iam::${local.account_id}:user/tf-managed/${local.gcs_user_name}",
]
}
statement {
sid = "IamReadOnly"
effect = "Allow"
actions = [
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"iam:ListPolicies",
"iam:ListPolicyVersions",
"iam:ListRolePolicies",
"iam:ListRoleTags",
"iam:ListRoles",
]
resources = ["*"]
}
statement {
sid = "TagExecBoundary"
effect = "Allow"
actions = [
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListPolicyTags",
"iam:ListPolicyVersions",
"iam:TagPolicy",
"iam:UntagPolicy",
]
resources = [
"arn:aws:iam::${local.account_id}:policy/tf-managed/${local.stack_prefix}*",
]
}
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-*",
"arn:aws:iam::${local.account_id}:policy/tf-managed/*",
]
}
}
data "aws_iam_policy_document" "hcptf_apply_services" {
statement {
sid = "BackupAndArtifactBuckets"
effect = "Allow"
actions = ["s3:*"]
resources = [
"arn:aws:s3:::${local.backup_bucket_name}",
"arn:aws:s3:::${local.backup_bucket_name}/*",
"arn:aws:s3:::${local.replica_bucket_name}",
"arn:aws:s3:::${local.replica_bucket_name}/*",
"arn:aws:s3:::${local.artifacts_bucket_name}",
"arn:aws:s3:::${local.artifacts_bucket_name}/*",
]
}
# RunInstances and CreateVolume do not support a useful resource ARN.
# This document is a managed policy so it is not counted against the
# apply role's 10,240-character inline quota. The plan role does not get it.
statement {
sid = "Ec2Host"
effect = "Allow"
actions = [
"ec2:AssociateIamInstanceProfile",
"ec2:AttachVolume",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:CreateVolume",
"ec2:DeleteSecurityGroup",
"ec2:DeleteTags",
"ec2:DeleteVolume",
"ec2:DescribeAccountAttributes",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeImages",
"ec2:DescribeInstanceAttribute",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstances",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroupRules",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeTags",
"ec2:DescribeVolumeAttribute",
"ec2:DescribeVolumeStatus",
"ec2:DescribeVolumes",
"ec2:DescribeVpcAttribute",
"ec2:DescribeVpcs",
"ec2:DetachVolume",
"ec2:DisassociateIamInstanceProfile",
"ec2:ModifyInstanceAttribute",
"ec2:ModifySecurityGroupRules",
"ec2:ModifyVolume",
"ec2:ReplaceIamInstanceProfileAssociation",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RunInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:TerminateInstances",
]
resources = ["*"]
}
# Listener and rule ARNs are allocated at create time.
statement {
sid = "LoadBalancer"
effect = "Allow"
actions = ["elasticloadbalancing:*"]
resources = ["*"]
}
statement {
sid = "Certificate"
effect = "Allow"
actions = ["acm:*"]
resources = ["*"]
}
statement {
sid = "Snapshots"
effect = "Allow"
actions = ["dlm:*"]
resources = ["*"]
}
statement {
sid = "VerificationFunction"
effect = "Allow"
actions = ["lambda:*"]
resources = [
"arn:aws:lambda:${var.aws_region}:${local.account_id}:function:forgejo-backup-verification",
]
}
statement {
sid = "VerificationSchedules"
effect = "Allow"
actions = ["events:*"]
resources = [
"arn:aws:events:${var.aws_region}:${local.account_id}:rule/forgejo-backup-*",
]
}
# CreateLogGroup is not reliable on the log-group ARN before the group exists.
statement {
sid = "CreateVerificationLogGroup"
effect = "Allow"
actions = ["logs:CreateLogGroup"]
resources = ["*"]
}
statement {
sid = "VerificationLogs"
effect = "Allow"
actions = [
"logs:DeleteLogGroup",
"logs:DeleteRetentionPolicy",
"logs:DescribeLogGroups",
"logs:ListTagsForResource",
"logs:PutRetentionPolicy",
"logs:TagResource",
"logs:UntagResource",
]
resources = [
"arn:aws:logs:${var.aws_region}:${local.account_id}:log-group:${local.verification_log_group}",
"arn:aws:logs:${var.aws_region}:${local.account_id}:log-group:${local.verification_log_group}:*",
]
}
statement {
sid = "VerificationAlarms"
effect = "Allow"
actions = [
"cloudwatch:DeleteAlarms",
"cloudwatch:DescribeAlarms",
"cloudwatch:ListTagsForResource",
"cloudwatch:PutMetricAlarm",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
]
resources = [
"arn:aws:cloudwatch:${var.aws_region}:${local.account_id}:alarm:forgejo-backup-*",
]
}
statement {
sid = "DescribeAlarms"
effect = "Allow"
actions = ["cloudwatch:DescribeAlarms"]
resources = ["*"]
}
statement {
sid = "BackupPrefixParameter"
effect = "Allow"
actions = [
"ssm:AddTagsToResource",
"ssm:DeleteParameter",
"ssm:GetParameter",
"ssm:ListTagsForResource",
"ssm:PutParameter",
"ssm:RemoveTagsFromResource",
]
resources = [
"arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter/forgejo/*",
]
}
statement {
sid = "AlertTopicRead"
effect = "Allow"
actions = [
"sns:GetTopicAttributes",
"sns:ListTagsForResource",
]
resources = [local.site_alerts_arn]
}
statement {
sid = "EbsEncryption"
effect = "Allow"
actions = [
"kms:CreateGrant",
"kms:Decrypt",
"kms:DescribeKey",
"kms:GenerateDataKeyWithoutPlaintext",
"kms:ReEncryptFrom",
"kms:ReEncryptTo",
]
resources = ["*"]
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["ec2.${var.aws_region}.amazonaws.com"]
}
}
}
data "aws_iam_policy_document" "hcptf_plan_refresh" {
statement {
sid = "RefreshIamRoles"
effect = "Allow"
actions = [
"iam:GetInstanceProfile",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:GetUser",
"iam:GetUserPolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"iam:ListRolePolicies",
"iam:ListRoleTags",
"iam:ListUserPolicies",
"iam:ListUserTags",
]
resources = [
"arn:aws:iam::${local.account_id}:role/tf-managed/${local.stack_prefix}*",
"arn:aws:iam::${local.account_id}:instance-profile/tf-managed/${local.stack_prefix}*",
"arn:aws:iam::${local.account_id}:user/tf-managed/${local.gcs_user_name}",
"arn:aws:iam::${local.account_id}:role/${local.apply_role}",
"arn:aws:iam::${local.account_id}:role/${local.plan_role}",
]
}
statement {
sid = "RefreshManagedPolicies"
effect = "Allow"
actions = [
"iam:GetPolicy",
"iam:GetPolicyVersion",
]
resources = ["*"]
}
statement {
sid = "RefreshS3"
effect = "Allow"
actions = [
"s3:GetAccelerateConfiguration",
"s3:GetBucketAcl",
"s3:GetBucketCORS",
"s3:GetBucketLocation",
"s3:GetBucketLogging",
"s3:GetBucketNotification",
"s3:GetBucketObjectLockConfiguration",
"s3:GetBucketOwnershipControls",
"s3:GetBucketPolicy",
"s3:GetBucketPolicyStatus",
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketRequestPayment",
"s3:GetBucketTagging",
"s3:GetBucketVersioning",
"s3:GetEncryptionConfiguration",
"s3:GetLifecycleConfiguration",
"s3:GetReplicationConfiguration",
"s3:ListBucket",
]
resources = [
"arn:aws:s3:::${local.backup_bucket_name}",
"arn:aws:s3:::${local.replica_bucket_name}",
"arn:aws:s3:::${local.artifacts_bucket_name}",
]
}
statement {
sid = "RefreshEc2"
effect = "Allow"
actions = [
"ec2:DescribeAccountAttributes",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeImages",
"ec2:DescribeInstanceAttribute",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstances",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroupRules",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeTags",
"ec2:DescribeVolumeAttribute",
"ec2:DescribeVolumeStatus",
"ec2:DescribeVolumes",
"ec2:DescribeVpcAttribute",
"ec2:DescribeVpcs",
]
resources = ["*"]
}
statement {
sid = "RefreshLoadBalancer"
effect = "Allow"
actions = [
"elasticloadbalancing:DescribeListenerAttributes",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:DescribeTags",
"elasticloadbalancing:DescribeTargetGroupAttributes",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
]
resources = ["*"]
}
statement {
sid = "RefreshCertificate"
effect = "Allow"
actions = [
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:ListTagsForCertificate",
]
resources = ["*"]
}
statement {
sid = "RefreshLambda"
effect = "Allow"
actions = [
"lambda:GetFunction",
"lambda:GetFunctionCodeSigningConfig",
"lambda:GetFunctionConfiguration",
"lambda:GetPolicy",
"lambda:GetRuntimeManagementConfig",
"lambda:ListTags",
"lambda:ListVersionsByFunction",
]
resources = [
"arn:aws:lambda:${var.aws_region}:${local.account_id}:function:forgejo-backup-verification",
]
}
statement {
sid = "RefreshSchedules"
effect = "Allow"
actions = [
"events:DescribeRule",
"events:ListTagsForResource",
"events:ListTargetsByRule",
]
resources = [
"arn:aws:events:${var.aws_region}:${local.account_id}:rule/forgejo-backup-*",
]
}
statement {
sid = "RefreshLogs"
effect = "Allow"
actions = [
"logs:DescribeLogGroups",
"logs:ListTagsForResource",
]
resources = [
"arn:aws:logs:${var.aws_region}:${local.account_id}:log-group:${local.verification_log_group}",
"arn:aws:logs:${var.aws_region}:${local.account_id}:log-group:${local.verification_log_group}:*",
]
}
statement {
sid = "RefreshAlarms"
effect = "Allow"
actions = [
"cloudwatch:DescribeAlarms",
"cloudwatch:ListTagsForResource",
]
resources = ["*"]
}
statement {
sid = "RefreshParameter"
effect = "Allow"
actions = [
"ssm:GetParameter",
"ssm:ListTagsForResource",
]
resources = [
"arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter/forgejo/*",
]
}
statement {
sid = "RefreshSnapshots"
effect = "Allow"
actions = [
"dlm:GetLifecyclePolicy",
"dlm:ListTagsForResource",
]
resources = ["arn:aws:dlm:${var.aws_region}:${local.account_id}:policy/*"]
}
statement {
sid = "RefreshSns"
effect = "Allow"
actions = [
"sns:GetTopicAttributes",
"sns:ListTagsForResource",
]
resources = [local.site_alerts_arn]
}
}
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
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
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
}
# Managed, not inline: the enumerated EC2 list plus scoped-iam-management
# exceeds the 10,240-character inline quota. Bootstrap creates this on the
# first apply. Later document edits need that window (CreatePolicyVersion
# is denied on hcptf-forgejo).
resource "aws_iam_policy" "hcptf_apply_services" {
name = "forgejo-services"
path = "/tf-managed/"
description = "Forgejo HCP apply service permissions (PLAT-80)."
policy = data.aws_iam_policy_document.hcptf_apply_services.json
}
resource "aws_iam_role_policy" "hcptf_plan_refresh" {
name = "forgejo-plan-refresh"
role = aws_iam_role.hcptf_plan.id
policy = data.aws_iam_policy_document.hcptf_plan_refresh.json
}
resource "aws_iam_role_policy_attachment" "hcptf_plan_view_only" {
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_apply" {
role_name = aws_iam_role.hcptf_apply.name
policy_arns = [
aws_iam_policy.hcptf_apply_services.arn,
]
}
resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" {
role_name = aws_iam_role.hcptf_plan.name
policy_arns = [
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
]
}