Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/platform-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ jobs:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/javabin-ci-infra
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/javabin-ci-infra-plan
aws-region: ${{ env.AWS_REGION }}
role-session-name: javabin-platform-${{ github.run_id }}
role-session-name: javabin-platform-plan-${{ github.run_id }}

- name: Terraform Init
working-directory: ${{ env.TF_ROOT }}
Expand Down
134 changes: 123 additions & 11 deletions terraform/platform/iam/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,109 @@ data "aws_iam_openid_connect_provider" "github" {
################################################################################

################################################################################
# 1. javabin-ci-infra — Platform repo only (plan + apply)
# 1a. javabin-ci-infra-plan — Platform repo plan + review (read-only)
#
# Trust: GitHub OIDC, only repo:javaBin/platform on main branch.
# Trust: GitHub OIDC, repo:javaBin/platform on ANY ref (PRs need plan too).
# Permissions: ReadOnlyAccess + specific writes for plan operations:
# - S3: upload plan artifacts
# - DynamoDB: Terraform state locking
# - Bedrock: LLM plan review (inline in plan job)
# - SSM: read Slack webhook for HIGH risk alerts
#
# This role CANNOT create, modify, or delete infrastructure.
################################################################################

resource "aws_iam_role" "ci_infra_plan" {
name = "${var.project}-ci-infra-plan"
permissions_boundary = aws_iam_policy.developer_boundary.arn

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = data.aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Any ref — PRs and main both need plan
"token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/platform:*"
}
}
}
]
})

tags = {
Name = "${var.project}-ci-infra-plan"
}
}

resource "aws_iam_role_policy_attachment" "ci_infra_plan_readonly" {
role = aws_iam_role.ci_infra_plan.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

resource "aws_iam_role_policy" "ci_infra_plan_extras" {
name = "plan-specific-writes"
role = aws_iam_role.ci_infra_plan.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TerraformStateLocking"
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
]
Resource = "arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/${var.project}-terraform-infra-lock"
},
{
Sid = "UploadPlanArtifacts"
Effect = "Allow"
Action = [
"s3:PutObject",
]
Resource = "arn:aws:s3:::${var.project}-ci-plan-artifacts-${var.aws_account_id}/*"
},
{
Sid = "BedrockForReview"
Effect = "Allow"
Action = [
"bedrock:InvokeModel",
"bedrock:Converse",
]
Resource = "arn:aws:bedrock:${var.region}:${var.aws_account_id}:inference-profile/eu.anthropic.*"
},
{
Sid = "SSMReadSlackWebhook"
Effect = "Allow"
Action = "ssm:GetParameter"
Resource = "arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/slack/*"
},
{
# Pricing API for cost estimation during review
Sid = "PricingRead"
Effect = "Allow"
Action = "pricing:GetProducts"
Resource = "*"
},
]
})
}

################################################################################
# 1b. javabin-ci-infra — Platform repo apply only
#
# Trust: GitHub OIDC, ONLY main branch (not PRs).
# Permissions: Allow *:* with permission boundary + explicit deny on operations
# that should never happen via CI (organizations, IAM users, destructive security ops).
################################################################################
Expand All @@ -45,8 +145,8 @@ resource "aws_iam_role" "ci_infra" {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
# Allow main branch pushes and PR merge refs (plan runs on PRs too)
"token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/platform:*"
# Main branch ONLY — apply never runs on PRs
"token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/platform:ref:refs/heads/main"
}
}
}
Expand Down Expand Up @@ -569,17 +669,29 @@ resource "aws_iam_role" "ci_registry" {
}
}

resource "aws_iam_role_policy" "ci_registry_lambda" {
name = "invoke-team-provisioner"
resource "aws_iam_role_policy" "ci_registry" {
name = "registry-operations"
role = aws_iam_role.ci_registry.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = "arn:aws:lambda:${var.region}:${var.aws_account_id}:function:${var.project}-team-provisioner"
}]
Statement = [
{
Sid = "InvokeTeamProvisioner"
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = "arn:aws:lambda:${var.region}:${var.aws_account_id}:function:${var.project}-team-provisioner"
},
{
Sid = "SSMReadGoogleCreds"
Effect = "Allow"
Action = "ssm:GetParameter"
Resource = [
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/platform/google-admin-sa",
"arn:aws:ssm:${var.region}:${var.aws_account_id}:parameter/${var.project}/platform/google-admin-email",
]
},
]
})
}

Expand Down
6 changes: 6 additions & 0 deletions terraform/platform/imports.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# One-time import: ci-infra-plan role was pre-created via CLI to bootstrap
# the plan job. Remove this file after the first successful apply.
import {
to = module.iam.aws_iam_role.ci_infra_plan
id = "javabin-ci-infra-plan"
}
Loading