From af0d7ea76a4fd79d619ac9bca9b9f923eb4fccc8 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 3 Oct 2025 16:33:56 -0500 Subject: [PATCH 1/5] feat: manage pypi aws tf groups and memberships --- terraform/aws-iam/datadog.tf | 111 ++++++++++++++++++++++++++++++ terraform/aws-iam/main.tf | 129 +++++++++++++++++++++++++++++++++++ terraform/aws-iam/outputs.tf | 24 +++++++ terraform/main.tf | 4 ++ 4 files changed, 268 insertions(+) create mode 100644 terraform/aws-iam/datadog.tf create mode 100644 terraform/aws-iam/main.tf create mode 100644 terraform/aws-iam/outputs.tf diff --git a/terraform/aws-iam/datadog.tf b/terraform/aws-iam/datadog.tf new file mode 100644 index 0000000..df9c04d --- /dev/null +++ b/terraform/aws-iam/datadog.tf @@ -0,0 +1,111 @@ +# Datadog AWS Integration Resources + +resource "aws_iam_policy" "datadog_integration" { + name = "AWSDataDogIntegration" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "autoscaling:Describe*", + "budgets:ViewBudget", + "cloudfront:GetDistributionConfig", + "cloudfront:ListDistributions", + "cloudtrail:DescribeTrails", + "cloudtrail:GetTrailStatus", + "cloudwatch:Describe*", + "cloudwatch:Get*", + "cloudwatch:List*", + "codedeploy:List*", + "codedeploy:BatchGet*", + "directconnect:Describe*", + "dynamodb:List*", + "dynamodb:Describe*", + "ec2:Describe*", + "ec2:Get*", + "ecs:Describe*", + "ecs:List*", + "elasticache:Describe*", + "elasticache:List*", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:DescribeTags", + "elasticloadbalancing:Describe*", + "elasticmapreduce:List*", + "elasticmapreduce:Describe*", + "es:ListTags", + "es:ListDomainNames", + "es:DescribeElasticsearchDomains", + "health:DescribeEvents", + "health:DescribeEventDetails", + "health:DescribeAffectedEntities", + "kinesis:List*", + "kinesis:Describe*", + "lambda:AddPermission", + "lambda:GetPolicy", + "lambda:List*", + "lambda:RemovePermission", + "logs:Get*", + "logs:Describe*", + "logs:FilterLogEvents", + "logs:TestMetricFilter", + "rds:Describe*", + "rds:List*", + "redshift:DescribeClusters", + "redshift:DescribeLoggingStatus", + "route53:List*", + "s3:GetBucketTagging", + "s3:ListAllMyBuckets", + "s3:GetBucketLogging", + "s3:GetBucketLocation", + "s3:GetBucketNotification", + "s3:ListAllMyBuckets", + "s3:PutBucketNotification", + "ses:Get*", + "sns:List*", + "sns:Publish", + "sqs:ListQueues", + "support:*", + "tag:getResources", + "tag:getTagKeys", + "tag:getTagValues", + "apigateway:GET", + "ec2:SearchTransitGatewayRoutes", + "elasticfilesystem:DescribeAccessPoints", + "fsx:DescribeFileSystems", + "states:ListStateMachines", + "apigateway:GET" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role" "datadog_integration" { + name = "AWSDataDogIntegration" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::464622532012:root" + } + Action = "sts:AssumeRole" + Condition = { + StringEquals = { + "sts:ExternalId" = "63ce1985605d40499b0a2a0091d76b0e" + } + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "datadog_integration" { + role = aws_iam_role.datadog_integration.name + policy_arn = aws_iam_policy.datadog_integration.arn +} diff --git a/terraform/aws-iam/main.tf b/terraform/aws-iam/main.tf new file mode 100644 index 0000000..e511923 --- /dev/null +++ b/terraform/aws-iam/main.tf @@ -0,0 +1,129 @@ +# Data source to get current AWS account info +data "aws_caller_identity" "current" {} + +# =========================== +# IAM Groups +# =========================== + +resource "aws_iam_group" "administrator" { + name = "administrator" + path = "/" +} + +resource "aws_iam_group" "billing" { + name = "billing" + path = "/" +} + +resource "aws_iam_group" "kops" { + name = "kops" + path = "/" +} + +resource "aws_iam_group" "readonly" { + name = "readonly" + path = "/" +} + +# =========================== +# Group Memberships +# =========================== + +resource "aws_iam_group_membership" "administrator" { + name = "administrator-membership" + group = aws_iam_group.administrator.name + users = [ + "di", + "dstufft", + "coffee", + "terraform-pypi" + "ee", + ] +} + +resource "aws_iam_group_membership" "kops" { + name = "kops-membership" + group = aws_iam_group.kops.name + users = [ + "kops" + ] +} + +# =========================== +# IAM Policies +# =========================== + +resource "aws_iam_policy" "billing_full_access" { + name = "BillingFullAccess" + description = "Provide Full Access to all billing related interfaces" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "Stmt1430745153000" + Effect = "Allow" + Action = [ + "aws-portal:*" + ] + Resource = [ + "*" + ] + } + ] + }) +} + +# =========================== +# Group Policy Attachments +# =========================== + +resource "aws_iam_group_policy_attachment" "administrator_admin_access" { + group = aws_iam_group.administrator.name + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" +} + +resource "aws_iam_group_policy_attachment" "billing_full_access" { + group = aws_iam_group.billing.name + policy_arn = aws_iam_policy.billing_full_access.arn +} + +resource "aws_iam_group_policy_attachment" "readonly_view_only" { + group = aws_iam_group.readonly.name + policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_route53" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonRoute53FullAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_ec2" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_iam" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_vpc" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_sqs" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSQSReadOnlyAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_s3" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" +} + +resource "aws_iam_group_policy_attachment" "kops_eventbridge" { + group = aws_iam_group.kops.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess" +} diff --git a/terraform/aws-iam/outputs.tf b/terraform/aws-iam/outputs.tf new file mode 100644 index 0000000..0170889 --- /dev/null +++ b/terraform/aws-iam/outputs.tf @@ -0,0 +1,24 @@ +output "group_arns" { + description = "ARNs of IAM groups" + value = { + administrator = aws_iam_group.administrator.arn + billing = aws_iam_group.billing.arn + kops = aws_iam_group.kops.arn + readonly = aws_iam_group.readonly.arn + } +} + +output "role_arns" { + description = "ARNs of IAM roles" + value = { + datadog_integration = aws_iam_role.datadog_integration.arn + } +} + +output "policy_arns" { + description = "ARNs of custom IAM policies" + value = { + billing_full_access = aws_iam_policy.billing_full_access.arn + datadog_integration = aws_iam_policy.datadog_integration.arn + } +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf index e231b6f..17eef6d 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -284,6 +284,10 @@ module "inspector" { ngwaf_percent_enabled = 100 } +module "aws-iam" { + source = "./aws-iam" +} + output "nameservers" { value = module.dns.nameservers } output "pypi-ses_delivery_topic" { value = module.email.delivery_topic } output "testpypi-ses_delivery_topic" { value = module.testpypi-email.delivery_topic } From 0f72bfcd989b05d785bdfac22c4fc38c3b470aaf Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 10 Oct 2025 10:26:49 -0500 Subject: [PATCH 2/5] fix: clean up things we dont use/are default --- terraform/aws-iam/main.tf | 104 +---------------------------------- terraform/aws-iam/outputs.tf | 4 -- 2 files changed, 1 insertion(+), 107 deletions(-) diff --git a/terraform/aws-iam/main.tf b/terraform/aws-iam/main.tf index e511923..661a915 100644 --- a/terraform/aws-iam/main.tf +++ b/terraform/aws-iam/main.tf @@ -1,34 +1,13 @@ # Data source to get current AWS account info data "aws_caller_identity" "current" {} -# =========================== # IAM Groups -# =========================== - resource "aws_iam_group" "administrator" { name = "administrator" path = "/" } -resource "aws_iam_group" "billing" { - name = "billing" - path = "/" -} - -resource "aws_iam_group" "kops" { - name = "kops" - path = "/" -} - -resource "aws_iam_group" "readonly" { - name = "readonly" - path = "/" -} - -# =========================== # Group Memberships -# =========================== - resource "aws_iam_group_membership" "administrator" { name = "administrator-membership" group = aws_iam_group.administrator.name @@ -36,94 +15,13 @@ resource "aws_iam_group_membership" "administrator" { "di", "dstufft", "coffee", - "terraform-pypi" + "terraform-pypi", "ee", ] } -resource "aws_iam_group_membership" "kops" { - name = "kops-membership" - group = aws_iam_group.kops.name - users = [ - "kops" - ] -} - -# =========================== -# IAM Policies -# =========================== - -resource "aws_iam_policy" "billing_full_access" { - name = "BillingFullAccess" - description = "Provide Full Access to all billing related interfaces" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "Stmt1430745153000" - Effect = "Allow" - Action = [ - "aws-portal:*" - ] - Resource = [ - "*" - ] - } - ] - }) -} - -# =========================== # Group Policy Attachments -# =========================== - resource "aws_iam_group_policy_attachment" "administrator_admin_access" { group = aws_iam_group.administrator.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" } - -resource "aws_iam_group_policy_attachment" "billing_full_access" { - group = aws_iam_group.billing.name - policy_arn = aws_iam_policy.billing_full_access.arn -} - -resource "aws_iam_group_policy_attachment" "readonly_view_only" { - group = aws_iam_group.readonly.name - policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_route53" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonRoute53FullAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_ec2" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_iam" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_vpc" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonVPCFullAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_sqs" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonSQSReadOnlyAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_s3" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" -} - -resource "aws_iam_group_policy_attachment" "kops_eventbridge" { - group = aws_iam_group.kops.name - policy_arn = "arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess" -} diff --git a/terraform/aws-iam/outputs.tf b/terraform/aws-iam/outputs.tf index 0170889..b6e12f4 100644 --- a/terraform/aws-iam/outputs.tf +++ b/terraform/aws-iam/outputs.tf @@ -2,9 +2,6 @@ output "group_arns" { description = "ARNs of IAM groups" value = { administrator = aws_iam_group.administrator.arn - billing = aws_iam_group.billing.arn - kops = aws_iam_group.kops.arn - readonly = aws_iam_group.readonly.arn } } @@ -18,7 +15,6 @@ output "role_arns" { output "policy_arns" { description = "ARNs of custom IAM policies" value = { - billing_full_access = aws_iam_policy.billing_full_access.arn datadog_integration = aws_iam_policy.datadog_integration.arn } } \ No newline at end of file From 80bdb04bab207218c24b624afabedaab722c1781 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 10 Oct 2025 12:17:08 -0500 Subject: [PATCH 3/5] feat: codify polciies into tf --- terraform/aws-iam/pypi_policies.tf | 214 +++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 terraform/aws-iam/pypi_policies.tf diff --git a/terraform/aws-iam/pypi_policies.tf b/terraform/aws-iam/pypi_policies.tf new file mode 100644 index 0000000..9c15189 --- /dev/null +++ b/terraform/aws-iam/pypi_policies.tf @@ -0,0 +1,214 @@ +# PyPI IAM Policies + +# to clean up (?) +# pypi-bandersnatch-mirror - 1031 days ago +# pypi-db-backup-archive - 795 days ago +# PyPIReadOnly - 913 days ago + +# DB Backup Archive Policy - not used in 795 days +# resource "aws_iam_policy" "pypi_db_backup_archive" { +# name = "pypi-db-backup-archive" +# +# policy = jsonencode({ +# Version = "2012-10-17" +# Statement = [ +# { +# Effect = "Allow" +# Action = "s3:ListAllMyBuckets" +# Resource = "*" +# }, +# { +# Effect = "Allow" +# Action = "s3:*" +# Resource = [ +# "arn:aws:s3:::pypi-db-backup-archive", +# "arn:aws:s3:::pypi-db-backup-archive/*" +# ] +# } +# ] +# }) +# } + +# opensearch +resource "aws_iam_policy" "pypi_elasticsearch" { + name = "PyPIElasticSearch" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "VisualEditor0" + Effect = "Allow" + Action = [ + "es:DescribeReservedElasticsearchInstanceOfferings", + "es:ESHttpGet", + "es:ListTags", + "es:DescribeElasticsearchDomainConfig", + "es:GetUpgradeHistory", + "es:DescribeReservedElasticsearchInstances", + "es:ESHttpHead", + "es:ListDomainNames", + "es:DescribeElasticsearchDomain", + "es:GetCompatibleElasticsearchVersions", + "es:GetUpgradeStatus", + "es:DescribeElasticsearchDomains", + "es:ListElasticsearchInstanceTypes", + "es:ListElasticsearchVersions", + "es:DescribeElasticsearchInstanceTypeLimits" + ] + Resource = "*" + }, + { + Sid = "VisualEditor1" + Effect = "Allow" + Action = "es:*" + Resource = "arn:aws:es:us-east-2:220435833635:domain/warehouse-7/production*" + }, + { + Sid = "VisualEditor2" + Effect = "Allow" + Action = "es:*" + Resource = "arn:aws:es:us-east-2:220435833635:domain/warehouse-opensearch/production*" + } + ] + }) +} + +# amazon ses +resource "aws_iam_policy" "pypi_email" { + name = "PyPIEmail" + description = "Allows sending email as pypi.org" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ses:SendEmail", + "ses:SendRawEmail" + ] + Resource = "arn:aws:ses:us-west-2:220435833635:identity/pypi.org" + }, + { + Effect = "Allow" + Action = [ + "sns:ConfirmSubscription" + ] + Resource = "arn:aws:sns:us-west-2:220435833635:pypi-ses-delivery-events-topic" + } + ] + }) +} + +# pypi files/docs ro - unused 913 days +# resource "aws_iam_policy" "pypi_readonly" { +# name = "PyPIReadOnly" +# description = "PyPI Files/Docs Read-Only Access" +# +# policy = jsonencode({ +# Version = "2012-10-17" +# Statement = [ +# { +# Effect = "Allow" +# Action = [ +# "s3:GetObject", +# "s3:ListBucket" +# ] +# Resource = [ +# "arn:aws:s3:::pypi-docs", +# "arn:aws:s3:::pypi-docs/*", +# "arn:aws:s3:::pypi-files", +# "arn:aws:s3:::pypi-files/*" +# ] +# } +# ] +# }) +# } + +# s3 r/w +resource "aws_iam_policy" "pypi_s3_access" { + name = "PyPIS3Access" + description = "R/W Access to the PyPI S3 Buckets" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "s3:ListAllMyBuckets" + Resource = "*" + }, + { + Effect = "Allow" + Action = "s3:*" + Resource = [ + "arn:aws:s3:::pypi-docs", + "arn:aws:s3:::pypi-docs/*" + ] + }, + { + Effect = "Allow" + Action = "s3:*" + Resource = [ + "arn:aws:s3:::pypi-files", + "arn:aws:s3:::pypi-files/*", + "arn:aws:s3:::pypi-files-archive", + "arn:aws:s3:::pypi-files-archive/*" + ] + }, + { + Effect = "Deny" + Action = [ + "s3:DeleteBucket", + "s3:DeleteBucketPolicy", + "s3:DeleteBucketWebsite", + "s3:DeleteObject", + "s3:DeleteObjectVersion" + ] + Resource = [ + "arn:aws:s3:::pypi-files", + "arn:aws:s3:::pypi-files/*", + "arn:aws:s3:::pypi-files-archive", + "arn:aws:s3:::pypi-files-archive/*" + ] + } + ] + }) +} + +# amazon sqs - unused 231 days +resource "aws_iam_policy" "pypi_worker_sqs" { + name = "PyPIWorkerSQS" + description = "R/W Access to PyPI's SQS Worker Queue" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "sqs:ListQueues" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:PurgeQueue", + "sqs:ReceiveMessage", + "sqs:SendMessage", + "sqs:ChangeMessageVisibility" + ] + Resource = [ + "arn:aws:sqs:us-east-2:220435833635:pypi-worker", + "arn:aws:sqs:us-east-2:220435833635:pypi-worker-default", + "arn:aws:sqs:us-east-2:220435833635:pypi-worker-malware" + ] + } + ] + }) +} From 84dbb7800c3d6f3603fa8d7ab873b955c7281473 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 10 Oct 2025 15:25:58 -0500 Subject: [PATCH 4/5] apply code reviiew --- terraform/aws-iam/main.tf | 25 ------------- terraform/aws-iam/pypi_policies.tf | 60 ------------------------------ 2 files changed, 85 deletions(-) diff --git a/terraform/aws-iam/main.tf b/terraform/aws-iam/main.tf index 661a915..af7b62e 100644 --- a/terraform/aws-iam/main.tf +++ b/terraform/aws-iam/main.tf @@ -1,27 +1,2 @@ # Data source to get current AWS account info data "aws_caller_identity" "current" {} - -# IAM Groups -resource "aws_iam_group" "administrator" { - name = "administrator" - path = "/" -} - -# Group Memberships -resource "aws_iam_group_membership" "administrator" { - name = "administrator-membership" - group = aws_iam_group.administrator.name - users = [ - "di", - "dstufft", - "coffee", - "terraform-pypi", - "ee", - ] -} - -# Group Policy Attachments -resource "aws_iam_group_policy_attachment" "administrator_admin_access" { - group = aws_iam_group.administrator.name - policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" -} diff --git a/terraform/aws-iam/pypi_policies.tf b/terraform/aws-iam/pypi_policies.tf index 9c15189..bae860e 100644 --- a/terraform/aws-iam/pypi_policies.tf +++ b/terraform/aws-iam/pypi_policies.tf @@ -5,30 +5,6 @@ # pypi-db-backup-archive - 795 days ago # PyPIReadOnly - 913 days ago -# DB Backup Archive Policy - not used in 795 days -# resource "aws_iam_policy" "pypi_db_backup_archive" { -# name = "pypi-db-backup-archive" -# -# policy = jsonencode({ -# Version = "2012-10-17" -# Statement = [ -# { -# Effect = "Allow" -# Action = "s3:ListAllMyBuckets" -# Resource = "*" -# }, -# { -# Effect = "Allow" -# Action = "s3:*" -# Resource = [ -# "arn:aws:s3:::pypi-db-backup-archive", -# "arn:aws:s3:::pypi-db-backup-archive/*" -# ] -# } -# ] -# }) -# } - # opensearch resource "aws_iam_policy" "pypi_elasticsearch" { name = "PyPIElasticSearch" @@ -176,39 +152,3 @@ resource "aws_iam_policy" "pypi_s3_access" { ] }) } - -# amazon sqs - unused 231 days -resource "aws_iam_policy" "pypi_worker_sqs" { - name = "PyPIWorkerSQS" - description = "R/W Access to PyPI's SQS Worker Queue" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "sqs:ListQueues" - ] - Resource = "*" - }, - { - Effect = "Allow" - Action = [ - "sqs:DeleteMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl", - "sqs:PurgeQueue", - "sqs:ReceiveMessage", - "sqs:SendMessage", - "sqs:ChangeMessageVisibility" - ] - Resource = [ - "arn:aws:sqs:us-east-2:220435833635:pypi-worker", - "arn:aws:sqs:us-east-2:220435833635:pypi-worker-default", - "arn:aws:sqs:us-east-2:220435833635:pypi-worker-malware" - ] - } - ] - }) -} From 4711d30a0be3613b7b9863eafee27880cc794246 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 10 Oct 2025 15:27:28 -0500 Subject: [PATCH 5/5] fix: dont output role now that we removed it --- terraform/aws-iam/outputs.tf | 7 ------- 1 file changed, 7 deletions(-) diff --git a/terraform/aws-iam/outputs.tf b/terraform/aws-iam/outputs.tf index b6e12f4..7fa7bcc 100644 --- a/terraform/aws-iam/outputs.tf +++ b/terraform/aws-iam/outputs.tf @@ -1,10 +1,3 @@ -output "group_arns" { - description = "ARNs of IAM groups" - value = { - administrator = aws_iam_group.administrator.arn - } -} - output "role_arns" { description = "ARNs of IAM roles" value = {