# 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 hcptf-* inline policies and to policy/tf-managed/forgejo-services # need the org-account role. The scoped role cannot PutRolePolicy or # CreatePolicyVersion. 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. # DescribeLogGroups is a list API. Its resource is log-group::log-stream:, not the group ARN. statement { sid = "CreateVerificationLogGroup" effect = "Allow" actions = ["logs:CreateLogGroup"] resources = ["*"] } statement { sid = "DescribeVerificationLogGroups" effect = "Allow" actions = ["logs:DescribeLogGroups"] 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/*", ] } # DescribeParameters does not accept a parameter ARN. The provider calls it on *. statement { sid = "DescribeBackupParameters" effect = "Allow" actions = ["ssm:DescribeParameters"] resources = ["*"] } 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:GetBucketWebsite", "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}", ] } # aws_s3_object refresh calls HeadObject and object metadata reads. Scope # them to the verification package. Backup and replica objects stay unread. statement { sid = "RefreshLambdaPackage" effect = "Allow" actions = [ "s3:GetObject", "s3:GetObjectAcl", "s3:GetObjectAttributes", "s3:GetObjectTagging", ] resources = [ "arn:aws:s3:::${local.artifacts_bucket_name}/functions/forgejo-backup-verification.zip", ] } 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: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}:*", ] } # DescribeLogGroups is a list API. Its resource is log-group::log-stream:, not the group ARN. statement { sid = "DescribeVerificationLogGroups" effect = "Allow" actions = ["logs:DescribeLogGroups"] resources = ["*"] } 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 = "DescribeBackupParameters" effect = "Allow" actions = ["ssm:DescribeParameters"] resources = ["*"] } 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", ] }