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
13 changes: 12 additions & 1 deletion .github/workflows/base-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,18 @@ jobs:
name: lambda-${{ needs.metadata.outputs.tag }}
path: ./dist

- name: "Configure AWS Credentials"
- name: "Configure AWS Credentials (IAM Bootstrap Role)"
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
aws-region: eu-west-2

- name: "Deploy IAM roles (iams-developer-roles stack)"
working-directory: ./infrastructure
run: |
make terraform env=${{ needs.metadata.outputs.environment }} stack=iams-developer-roles tf-command=apply workspace=default

- name: "Configure AWS Credentials (Main Deployment Role)"
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/cicd-2-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,18 @@ jobs:
name: lambda-${{ needs.metadata.outputs.version }}
path: dist/lambda.zip

- name: "Configure AWS Credentials"
- name: "Configure AWS Credentials (IAM Bootstrap Role)"
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
aws-region: eu-west-2

- name: "Deploy IAM roles (iams-developer-roles stack)"
working-directory: ./infrastructure
run: |
make terraform env=dev stack=iams-developer-roles tf-command=apply workspace=default

- name: "Configure AWS Credentials (Main Deployment Role)"
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/iam-bootstrap-deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Manual IAM deployment for emergency or ad-hoc use.
# Normal IAM deployments happen automatically as part of cicd-2-publish and base-deploy.
name: "IAM Bootstrap | Deploy IAM Roles"

on:
workflow_dispatch:
inputs:
environment:
description: "Environment to deploy"
required: true
type: choice
options:
- dev
- test
- preprod
- prod

concurrency:
group: iam-bootstrap-${{ inputs.environment }}
cancel-in-progress: false

permissions:
contents: read
id-token: write

jobs:
deploy:
name: "Deploy IAM roles → ${{ inputs.environment }}"
runs-on: ubuntu-latest
timeout-minutes: 15
environment: ${{ inputs.environment }}
steps:
- name: "Checkout code"
uses: actions/checkout@v6

- name: "Resolve Terraform version"
id: vars
run: |
echo "terraform_version=$(grep '^terraform' .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT

- name: "Setup Terraform"
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ steps.vars.outputs.terraform_version }}

- name: "Configure AWS Credentials (IAM Bootstrap Role)"
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
aws-region: eu-west-2

- name: "Terraform Plan"
working-directory: ./infrastructure
run: |
make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=plan workspace=default

- name: "Terraform Apply"
working-directory: ./infrastructure
run: |
make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=apply workspace=default
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# IAM management policy – scoped to project resources
resource "aws_iam_policy" "iam_bootstrap_iam_management" {
name = "${upper(var.project_name)}-iam-bootstrap-iam-management"
description = "Allows the IAM bootstrap role to manage project IAM resources"
path = "/service-policies/"

policy = data.aws_iam_policy_document.iam_bootstrap_iam_management.json

tags = merge(local.tags, { Name = "${upper(var.project_name)}-iam-bootstrap-iam-management" })
}

data "aws_iam_policy_document" "iam_bootstrap_iam_management" {
# Full IAM access for project-scoped resources
statement {
sid = "IamManageProjectResources"
effect = "Allow"
actions = [
"iam:GetRole*",
"iam:GetPolicy*",
"iam:ListRole*",
"iam:ListPolicies",
"iam:ListAttachedRolePolicies",
"iam:ListPolicyVersions",
"iam:ListPolicyTags",
"iam:ListOpenIDConnectProviders",
"iam:ListOpenIDConnectProviderTags",
"iam:GetOpenIDConnectProvider",
"iam:CreateRole",
"iam:DeleteRole",
"iam:UpdateRole",
"iam:UpdateAssumeRolePolicy",
"iam:PutRolePolicy",
"iam:PutRolePermissionsBoundary",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:CreatePolicy",
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:TagRole",
"iam:TagPolicy",
"iam:UntagRole",
"iam:UntagPolicy",
"iam:PassRole",
"iam:TagOpenIDConnectProvider",
"iam:UntagOpenIDConnectProvider",
"iam:CreateOpenIDConnectProvider",
"iam:DeleteOpenIDConnectProvider",
"iam:UpdateOpenIDConnectProviderThumbprint",
"iam:AddClientIDToOpenIDConnectProvider",
"iam:RemoveClientIDFromOpenIDConnectProvider",
]
resources = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-api-deployment-role",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-iam-bootstrap-role",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.project_name}-terraform-developer-role",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/terraform-developer-role",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${upper(var.project_name)}-*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${lower(var.project_name)}-*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/service-policies/*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${local.stack_name}-*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com",
]
}

# Read-only IAM access for Terraform plan/discovery
statement {
sid = "IamReadOnly"
effect = "Allow"
actions = [
"iam:Get*",
"iam:List*",
]
resources = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/*",
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/*",
]
}

# DENY: Prevent modifying the bootstrap role itself
statement {
sid = "DenySelfModification"
effect = "Deny"
actions = [
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:UpdateAssumeRolePolicy",
"iam:PutRolePermissionsBoundary",
"iam:DeleteRolePermissionsBoundary",
]
resources = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-iam-bootstrap-role",
]
}

# DENY: Prevent modifying the bootstrap permissions boundary
statement {
sid = "DenyBootstrapBoundaryModification"
effect = "Deny"
actions = [
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
]
resources = [
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${lower(var.project_name)}-iam-bootstrap-permissions-boundary",
]
}
}

# Terraform state management policy
resource "aws_iam_policy" "iam_bootstrap_terraform_state" {
name = "${upper(var.project_name)}-iam-bootstrap-terraform-state"
description = "Allows the IAM bootstrap role to manage Terraform state for the iams-developer-roles stack"
path = "/service-policies/"

policy = data.aws_iam_policy_document.iam_bootstrap_terraform_state.json

tags = merge(local.tags, { Name = "${upper(var.project_name)}-iam-bootstrap-terraform-state" })
}

data "aws_iam_policy_document" "iam_bootstrap_terraform_state" {
# S3 state bucket access
statement {
sid = "TerraformStateS3Access"
effect = "Allow"
actions = [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
]
resources = [
"${local.terraform_state_bucket_arn}",
"${local.terraform_state_bucket_arn}/*",
]
}
}

resource "aws_iam_role_policy_attachment" "iam_bootstrap_iam_management" {
role = aws_iam_role.github_actions_iam_bootstrap.name
policy_arn = aws_iam_policy.iam_bootstrap_iam_management.arn
}

resource "aws_iam_role_policy_attachment" "iam_bootstrap_terraform_state" {
role = aws_iam_role.github_actions_iam_bootstrap.name
policy_arn = aws_iam_policy.iam_bootstrap_terraform_state.arn
}
61 changes: 61 additions & 0 deletions infrastructure/stacks/iams-developer-roles/github_actions_role.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,64 @@ resource "aws_iam_role" "github_actions" {
}
)
}


# GitHub Actions IAM Bootstrap Role
# It can update the main deployment role's policies but cannot modify itself.
resource "aws_iam_role" "github_actions_iam_bootstrap" {
name = "github-actions-iam-bootstrap-role"
description = "Role for GitHub Actions to deploy IAM infrastructure (iams-developer-roles stack only)"
permissions_boundary = aws_iam_policy.iam_bootstrap_permissions_boundary.arn
path = "/service-roles/"

assume_role_policy = data.aws_iam_policy_document.github_actions_iam_bootstrap_assume_role.json

tags = merge(
local.tags,
{
Name = "github-actions-iam-bootstrap-role"
}
)
}

data "aws_iam_policy_document" "github_actions_iam_bootstrap_assume_role" {
statement {
sid = "OidcAssumeRoleForIamBootstrap"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]

principals {
type = "Federated"
identifiers = [
aws_iam_openid_connect_provider.github.arn
]
}

condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}

# Only allow from main branch (and events triggered from main)
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = [
"repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main",
"repo:${var.github_org}/${var.github_repo}:environment:*",
]
}

# Only allow from the IAM bootstrap and base deployment workflows
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:job_workflow_ref"
values = [
"${var.github_org}/${var.github_repo}/.github/workflows/iam-bootstrap-deploy.yaml@*",
"${var.github_org}/${var.github_repo}/.github/workflows/base-deploy.yml@*",
"${var.github_org}/${var.github_repo}/.github/workflows/cicd-2-publish.yaml@*",
]
}
}
}
Loading
Loading