From 1d255e5d061b6ec02fa1156eb07dccfc7bb6afce Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:32:00 +0000 Subject: [PATCH 01/10] eli-445 adding github bootstrap role --- .../github_actions_role.tf | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_role.tf b/infrastructure/stacks/iams-developer-roles/github_actions_role.tf index a1305fdea..ed3c7598c 100644 --- a/infrastructure/stacks/iams-developer-roles/github_actions_role.tf +++ b/infrastructure/stacks/iams-developer-roles/github_actions_role.tf @@ -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@*", + ] + } + } +} From 53dbcbdceef54cfd8a6726dea819c51f5a997fdf Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:34:08 +0000 Subject: [PATCH 02/10] eli-445 adding github role bootstrap role policies --- .../github_actions_iam_bootstrap_policies.tf | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf b/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf new file mode 100644 index 000000000..920dfcce0 --- /dev/null +++ b/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf @@ -0,0 +1,149 @@ +# 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 = ["*"] + } + + # 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 +} From 63af80bb41b939dd28e93f599e7e642452be7565 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:35:02 +0000 Subject: [PATCH 03/10] eli-445 adding github bootstrap role permissions boundary --- .../iams_permissions_boundary.tf | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf index 2dca544d0..6c5b2b063 100644 --- a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf +++ b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf @@ -295,3 +295,144 @@ resource "aws_iam_policy" "permissions_boundary" { } ) } + +data "aws_iam_policy_document" "iam_bootstrap_permissions_boundary" { + # Allow IAM operations on project-scoped resources + statement { + sid = "AllowProjectIamOperations" + 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:DeletePolicy*", + "iam:TagRole", + "iam:TagPolicy", + "iam:UntagRole", + "iam:UntagPolicy", + "iam:PassRole", + "iam:CreateServiceLinkedRole", + "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", + ] + } + + # Allow read-only IAM access for Terraform plan/state discovery + statement { + sid = "AllowIamReadAccess" + effect = "Allow" + actions = [ + "iam:Get*", + "iam:List*", + ] + resources = ["*"] + } + + # Allow Terraform state bucket access + statement { + sid = "AllowTerraformStateAccess" + effect = "Allow" + actions = [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + ] + resources = [ + "${local.terraform_state_bucket_arn}", + "${local.terraform_state_bucket_arn}/*", + ] + } + + # Allow Terraform state locking via DynamoDB + statement { + sid = "AllowTerraformStateLocking" + effect = "Allow" + actions = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:DeleteItem", + ] + resources = [ + "arn:aws:dynamodb:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:table/${var.project_name}-*-terraform-lock", + ] + } + + # DENY: Prevent the bootstrap role from modifying its own policies + statement { + sid = "DenyBootstrapSelfModification" + 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 the bootstrap role from modifying its own 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", + ] + } +} + +resource "aws_iam_policy" "iam_bootstrap_permissions_boundary" { + name = "${lower(var.project_name)}-iam-bootstrap-permissions-boundary" + description = "Permissions boundary for the GitHub Actions IAM Bootstrap role - scoped to IAM and Terraform state only" + policy = data.aws_iam_policy_document.iam_bootstrap_permissions_boundary.json + + tags = merge( + local.tags, + { + Stack = "iams-developer-roles" + } + ) +} From f3cc535cfcaf6deb01f7c324af65dba047f6bc2a Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:35:59 +0000 Subject: [PATCH 04/10] eli-445 adding bootstrap workflow for github actions --- .github/workflows/iam-bootstrap-deploy.yaml | 231 ++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 .github/workflows/iam-bootstrap-deploy.yaml diff --git a/.github/workflows/iam-bootstrap-deploy.yaml b/.github/workflows/iam-bootstrap-deploy.yaml new file mode 100644 index 000000000..a84b50ef7 --- /dev/null +++ b/.github/workflows/iam-bootstrap-deploy.yaml @@ -0,0 +1,231 @@ +name: "IAM Bootstrap | Deploy IAM Roles" + +on: + push: + branches: + - main + paths: + - "infrastructure/stacks/iams-developer-roles/**" + workflow_dispatch: + inputs: + environment: + description: "Environment to deploy (leave blank for all)" + required: false + type: choice + options: + - all + - dev + - test + - preprod + - prod + workflow_call: + inputs: + environment: + description: "Environment to deploy" + required: false + type: string + default: "all" + +concurrency: + group: iam-bootstrap-deploy + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +jobs: + metadata: + name: "Resolve CI/CD metadata" + runs-on: ubuntu-latest + timeout-minutes: 2 + outputs: + terraform_version: ${{ steps.vars.outputs.terraform_version }} + target_env: ${{ steps.vars.outputs.target_env }} + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Set variables" + id: vars + run: | + echo "terraform_version=$(grep '^terraform' .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + + # Determine which environment(s) to deploy + INPUT_ENV="${{ inputs.environment || 'all' }}" + echo "target_env=$INPUT_ENV" >> $GITHUB_OUTPUT + echo "Target environment: $INPUT_ENV" + + deploy-dev: + name: "Deploy IAM roles → dev" + needs: metadata + if: >- + needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'dev' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: dev + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.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 Init" + working-directory: ./infrastructure + run: | + make terraform-init env=dev stack=iams-developer-roles + + - name: "Terraform Plan" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: | + terraform plan -var="environment=dev" -out=tfplan + echo "### Dev IAM Plan" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: "Terraform Apply" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: terraform apply -auto-approve tfplan + + deploy-test: + name: "Deploy IAM roles → test (approval required)" + needs: [metadata, deploy-dev] + if: >- + always() && + (needs.deploy-dev.result == 'success' || needs.deploy-dev.result == 'skipped') && + (needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'test') + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: test + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.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 Init" + working-directory: ./infrastructure + run: | + make terraform-init env=test stack=iams-developer-roles + + - name: "Terraform Plan" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: | + terraform plan -var="environment=test" -out=tfplan + echo "### Test IAM Plan" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: "Terraform Apply" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: terraform apply -auto-approve tfplan + + deploy-preprod: + name: "Deploy IAM roles → preprod (approval required)" + needs: [metadata, deploy-test] + if: >- + always() && + (needs.deploy-test.result == 'success' || needs.deploy-test.result == 'skipped') && + (needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'preprod') + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: preprod + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.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 Init" + working-directory: ./infrastructure + run: | + make terraform-init env=preprod stack=iams-developer-roles + + - name: "Terraform Plan" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: | + terraform plan -var="environment=preprod" -out=tfplan + echo "### Preprod IAM Plan" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: "Terraform Apply" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: terraform apply -auto-approve tfplan + + deploy-prod: + name: "Deploy IAM roles → prod (approval required)" + needs: [metadata, deploy-preprod] + if: >- + always() && + (needs.deploy-preprod.result == 'success' || needs.deploy-preprod.result == 'skipped') && + (needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'prod') + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: prod + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.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 Init" + working-directory: ./infrastructure + run: | + make terraform-init env=prod stack=iams-developer-roles + + - name: "Terraform Plan" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: | + terraform plan -var="environment=prod" -out=tfplan + echo "### Prod IAM Plan" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: "Terraform Apply" + working-directory: ./infrastructure/stacks/iams-developer-roles + run: terraform apply -auto-approve tfplan From 20ba078ffb79c7f2e7e82572c098a4007c5cd10d Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:36:50 +0000 Subject: [PATCH 05/10] eli-445 adding bootstrap deploy to base deploy workflow --- .github/workflows/base-deploy.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/base-deploy.yml b/.github/workflows/base-deploy.yml index a174e5487..8a7e0f0c4 100644 --- a/.github/workflows/base-deploy.yml +++ b/.github/workflows/base-deploy.yml @@ -187,7 +187,22 @@ 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)" + env: + ENVIRONMENT: ${{ needs.metadata.outputs.environment }} + working-directory: ./infrastructure + run: | + make terraform-init env=$ENVIRONMENT stack=iams-developer-roles + terraform -chdir=./stacks/iams-developer-roles plan -var="environment=$ENVIRONMENT" -out=tfplan + terraform -chdir=./stacks/iams-developer-roles apply -auto-approve tfplan + + - 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 From f2d42cad8088a1e7e086e3014f894a2b38f53ee0 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:37:29 +0000 Subject: [PATCH 06/10] eli-445 adding bootstrap deploy to publish workflow --- .github/workflows/cicd-2-publish.yaml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cicd-2-publish.yaml b/.github/workflows/cicd-2-publish.yaml index e59eeb0b0..910dbf6a3 100644 --- a/.github/workflows/cicd-2-publish.yaml +++ b/.github/workflows/cicd-2-publish.yaml @@ -87,7 +87,22 @@ 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)" + env: + ENVIRONMENT: dev + working-directory: ./infrastructure + run: | + make terraform-init env=$ENVIRONMENT stack=iams-developer-roles + terraform -chdir=./stacks/iams-developer-roles plan -var="environment=$ENVIRONMENT" -out=tfplan + terraform -chdir=./stacks/iams-developer-roles apply -auto-approve tfplan + + - 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 From 592c858b49ed4154dfc62536f84320bb919e1567 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:46:08 +0000 Subject: [PATCH 07/10] eli-445 correcting linting and checkov --- .../github_actions_iam_bootstrap_policies.tf | 6 +++++- .../iams-developer-roles/iams_permissions_boundary.tf | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf b/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf index 920dfcce0..281ee9190 100644 --- a/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf +++ b/infrastructure/stacks/iams-developer-roles/github_actions_iam_bootstrap_policies.tf @@ -72,7 +72,11 @@ data "aws_iam_policy_document" "iam_bootstrap_iam_management" { "iam:Get*", "iam:List*", ] - resources = ["*"] + 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 diff --git a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf index 6c5b2b063..91c1e94d6 100644 --- a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf +++ b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf @@ -357,7 +357,11 @@ data "aws_iam_policy_document" "iam_bootstrap_permissions_boundary" { "iam:Get*", "iam:List*", ] - resources = ["*"] + 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/*", + ] } # Allow Terraform state bucket access From e444f100f799d69bdd1cedbf2f40438e948c8bb2 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:47:22 +0000 Subject: [PATCH 08/10] eli-445 linting --- .github/workflows/iam-bootstrap-deploy.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/iam-bootstrap-deploy.yaml b/.github/workflows/iam-bootstrap-deploy.yaml index a84b50ef7..784d028f9 100644 --- a/.github/workflows/iam-bootstrap-deploy.yaml +++ b/.github/workflows/iam-bootstrap-deploy.yaml @@ -104,8 +104,10 @@ jobs: if: >- always() && (needs.deploy-dev.result == 'success' || needs.deploy-dev.result == 'skipped') && - (needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'test') + ( + needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'test' + ) runs-on: ubuntu-latest timeout-minutes: 15 environment: test @@ -148,8 +150,10 @@ jobs: if: >- always() && (needs.deploy-test.result == 'success' || needs.deploy-test.result == 'skipped') && - (needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'preprod') + ( + needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'preprod' + ) runs-on: ubuntu-latest timeout-minutes: 15 environment: preprod @@ -192,8 +196,10 @@ jobs: if: >- always() && (needs.deploy-preprod.result == 'success' || needs.deploy-preprod.result == 'skipped') && - (needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'prod') + ( + needs.metadata.outputs.target_env == 'all' || + needs.metadata.outputs.target_env == 'prod' + ) runs-on: ubuntu-latest timeout-minutes: 15 environment: prod From 6079ce2df9c837bb49675ba84ba2afc4c1be4977 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:16:58 +0000 Subject: [PATCH 09/10] eli-445 stripping down the manual job to just deploy to a single, chosen environment --- .github/workflows/iam-bootstrap-deploy.yaml | 203 ++------------------ 1 file changed, 13 insertions(+), 190 deletions(-) diff --git a/.github/workflows/iam-bootstrap-deploy.yaml b/.github/workflows/iam-bootstrap-deploy.yaml index 784d028f9..30378258a 100644 --- a/.github/workflows/iam-bootstrap-deploy.yaml +++ b/.github/workflows/iam-bootstrap-deploy.yaml @@ -1,33 +1,22 @@ +# 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: - push: - branches: - - main - paths: - - "infrastructure/stacks/iams-developer-roles/**" workflow_dispatch: inputs: environment: - description: "Environment to deploy (leave blank for all)" - required: false + description: "Environment to deploy" + required: true type: choice options: - - all - dev - test - preprod - prod - workflow_call: - inputs: - environment: - description: "Environment to deploy" - required: false - type: string - default: "all" concurrency: - group: iam-bootstrap-deploy + group: iam-bootstrap-${{ inputs.environment }} cancel-in-progress: false permissions: @@ -35,90 +24,24 @@ permissions: id-token: write jobs: - metadata: - name: "Resolve CI/CD metadata" + deploy: + name: "Deploy IAM roles → ${{ inputs.environment }}" runs-on: ubuntu-latest - timeout-minutes: 2 - outputs: - terraform_version: ${{ steps.vars.outputs.terraform_version }} - target_env: ${{ steps.vars.outputs.target_env }} + timeout-minutes: 15 + environment: ${{ inputs.environment }} steps: - name: "Checkout code" uses: actions/checkout@v6 - - name: "Set variables" + - name: "Resolve Terraform version" id: vars run: | echo "terraform_version=$(grep '^terraform' .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - # Determine which environment(s) to deploy - INPUT_ENV="${{ inputs.environment || 'all' }}" - echo "target_env=$INPUT_ENV" >> $GITHUB_OUTPUT - echo "Target environment: $INPUT_ENV" - - deploy-dev: - name: "Deploy IAM roles → dev" - needs: metadata - if: >- - needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'dev' - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: dev - steps: - - name: "Checkout code" - uses: actions/checkout@v6 - - - name: "Setup Terraform" - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ needs.metadata.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 Init" - working-directory: ./infrastructure - run: | - make terraform-init env=dev stack=iams-developer-roles - - - name: "Terraform Plan" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: | - terraform plan -var="environment=dev" -out=tfplan - echo "### Dev IAM Plan" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: "Terraform Apply" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: terraform apply -auto-approve tfplan - - deploy-test: - name: "Deploy IAM roles → test (approval required)" - needs: [metadata, deploy-dev] - if: >- - always() && - (needs.deploy-dev.result == 'success' || needs.deploy-dev.result == 'skipped') && - ( - needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'test' - ) - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: test - steps: - - name: "Checkout code" - uses: actions/checkout@v6 - - name: "Setup Terraform" uses: hashicorp/setup-terraform@v3 with: - terraform_version: ${{ needs.metadata.outputs.terraform_version }} + terraform_version: ${{ steps.vars.outputs.terraform_version }} - name: "Configure AWS Credentials (IAM Bootstrap Role)" uses: aws-actions/configure-aws-credentials@v6 @@ -126,112 +49,12 @@ jobs: 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 Init" - working-directory: ./infrastructure - run: | - make terraform-init env=test stack=iams-developer-roles - - name: "Terraform Plan" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: | - terraform plan -var="environment=test" -out=tfplan - echo "### Test IAM Plan" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: "Terraform Apply" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: terraform apply -auto-approve tfplan - - deploy-preprod: - name: "Deploy IAM roles → preprod (approval required)" - needs: [metadata, deploy-test] - if: >- - always() && - (needs.deploy-test.result == 'success' || needs.deploy-test.result == 'skipped') && - ( - needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'preprod' - ) - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: preprod - steps: - - name: "Checkout code" - uses: actions/checkout@v6 - - - name: "Setup Terraform" - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ needs.metadata.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 Init" working-directory: ./infrastructure run: | - make terraform-init env=preprod stack=iams-developer-roles - - - name: "Terraform Plan" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: | - terraform plan -var="environment=preprod" -out=tfplan - echo "### Preprod IAM Plan" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=plan workspace=default - name: "Terraform Apply" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: terraform apply -auto-approve tfplan - - deploy-prod: - name: "Deploy IAM roles → prod (approval required)" - needs: [metadata, deploy-preprod] - if: >- - always() && - (needs.deploy-preprod.result == 'success' || needs.deploy-preprod.result == 'skipped') && - ( - needs.metadata.outputs.target_env == 'all' || - needs.metadata.outputs.target_env == 'prod' - ) - runs-on: ubuntu-latest - timeout-minutes: 15 - environment: prod - steps: - - name: "Checkout code" - uses: actions/checkout@v6 - - - name: "Setup Terraform" - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ needs.metadata.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 Init" working-directory: ./infrastructure run: | - make terraform-init env=prod stack=iams-developer-roles - - - name: "Terraform Plan" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: | - terraform plan -var="environment=prod" -out=tfplan - echo "### Prod IAM Plan" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - terraform show -no-color tfplan >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: "Terraform Apply" - working-directory: ./infrastructure/stacks/iams-developer-roles - run: terraform apply -auto-approve tfplan + make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=apply workspace=default From 3937be029f465929538b87bd812ae70505f2b94a Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:19:43 +0000 Subject: [PATCH 10/10] eli-445 tidying up workflows --- .github/workflows/base-deploy.yml | 6 +----- .github/workflows/cicd-2-publish.yaml | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/base-deploy.yml b/.github/workflows/base-deploy.yml index 8a7e0f0c4..6679d6694 100644 --- a/.github/workflows/base-deploy.yml +++ b/.github/workflows/base-deploy.yml @@ -194,13 +194,9 @@ jobs: aws-region: eu-west-2 - name: "Deploy IAM roles (iams-developer-roles stack)" - env: - ENVIRONMENT: ${{ needs.metadata.outputs.environment }} working-directory: ./infrastructure run: | - make terraform-init env=$ENVIRONMENT stack=iams-developer-roles - terraform -chdir=./stacks/iams-developer-roles plan -var="environment=$ENVIRONMENT" -out=tfplan - terraform -chdir=./stacks/iams-developer-roles apply -auto-approve tfplan + 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 diff --git a/.github/workflows/cicd-2-publish.yaml b/.github/workflows/cicd-2-publish.yaml index 910dbf6a3..2fe0121b5 100644 --- a/.github/workflows/cicd-2-publish.yaml +++ b/.github/workflows/cicd-2-publish.yaml @@ -94,13 +94,9 @@ jobs: aws-region: eu-west-2 - name: "Deploy IAM roles (iams-developer-roles stack)" - env: - ENVIRONMENT: dev working-directory: ./infrastructure run: | - make terraform-init env=$ENVIRONMENT stack=iams-developer-roles - terraform -chdir=./stacks/iams-developer-roles plan -var="environment=$ENVIRONMENT" -out=tfplan - terraform -chdir=./stacks/iams-developer-roles apply -auto-approve tfplan + 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