Skip to content

Commit 6f27043

Browse files
authored
Merge pull request #580 from NHSDigital/feature/eja-eli-445-create-github-role-bootstrap-role
Feature/eja eli 445 create GitHub role bootstrap role
2 parents 4ccf0ea + fecea11 commit 6f27043

6 files changed

Lines changed: 443 additions & 2 deletions

File tree

.github/workflows/base-deploy.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,18 @@ jobs:
187187
name: lambda-${{ needs.metadata.outputs.tag }}
188188
path: ./dist
189189

190-
- name: "Configure AWS Credentials"
190+
- name: "Configure AWS Credentials (IAM Bootstrap Role)"
191+
uses: aws-actions/configure-aws-credentials@v6
192+
with:
193+
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
194+
aws-region: eu-west-2
195+
196+
- name: "Deploy IAM roles (iams-developer-roles stack)"
197+
working-directory: ./infrastructure
198+
run: |
199+
make terraform env=${{ needs.metadata.outputs.environment }} stack=iams-developer-roles tf-command=apply workspace=default
200+
201+
- name: "Configure AWS Credentials (Main Deployment Role)"
191202
uses: aws-actions/configure-aws-credentials@v6
192203
with:
193204
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role

.github/workflows/cicd-2-publish.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,18 @@ jobs:
8787
name: lambda-${{ needs.metadata.outputs.version }}
8888
path: dist/lambda.zip
8989

90-
- name: "Configure AWS Credentials"
90+
- name: "Configure AWS Credentials (IAM Bootstrap Role)"
91+
uses: aws-actions/configure-aws-credentials@v6
92+
with:
93+
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
94+
aws-region: eu-west-2
95+
96+
- name: "Deploy IAM roles (iams-developer-roles stack)"
97+
working-directory: ./infrastructure
98+
run: |
99+
make terraform env=dev stack=iams-developer-roles tf-command=apply workspace=default
100+
101+
- name: "Configure AWS Credentials (Main Deployment Role)"
91102
uses: aws-actions/configure-aws-credentials@v6
92103
with:
93104
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Manual IAM deployment for emergency or ad-hoc use.
2+
# Normal IAM deployments happen automatically as part of cicd-2-publish and base-deploy.
3+
name: "IAM Bootstrap | Deploy IAM Roles"
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
environment:
9+
description: "Environment to deploy"
10+
required: true
11+
type: choice
12+
options:
13+
- dev
14+
- test
15+
- preprod
16+
- prod
17+
18+
concurrency:
19+
group: iam-bootstrap-${{ inputs.environment }}
20+
cancel-in-progress: false
21+
22+
permissions:
23+
contents: read
24+
id-token: write
25+
26+
jobs:
27+
deploy:
28+
name: "Deploy IAM roles → ${{ inputs.environment }}"
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 15
31+
environment: ${{ inputs.environment }}
32+
steps:
33+
- name: "Checkout code"
34+
uses: actions/checkout@v6
35+
36+
- name: "Resolve Terraform version"
37+
id: vars
38+
run: |
39+
echo "terraform_version=$(grep '^terraform' .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT
40+
41+
- name: "Setup Terraform"
42+
uses: hashicorp/setup-terraform@v3
43+
with:
44+
terraform_version: ${{ steps.vars.outputs.terraform_version }}
45+
46+
- name: "Configure AWS Credentials (IAM Bootstrap Role)"
47+
uses: aws-actions/configure-aws-credentials@v6
48+
with:
49+
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-iam-bootstrap-role
50+
aws-region: eu-west-2
51+
52+
- name: "Terraform Plan"
53+
working-directory: ./infrastructure
54+
run: |
55+
make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=plan workspace=default
56+
57+
- name: "Terraform Apply"
58+
working-directory: ./infrastructure
59+
run: |
60+
make terraform env=${{ inputs.environment }} stack=iams-developer-roles tf-command=apply workspace=default
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# IAM management policy – scoped to project resources
2+
resource "aws_iam_policy" "iam_bootstrap_iam_management" {
3+
name = "${upper(var.project_name)}-iam-bootstrap-iam-management"
4+
description = "Allows the IAM bootstrap role to manage project IAM resources"
5+
path = "/service-policies/"
6+
7+
policy = data.aws_iam_policy_document.iam_bootstrap_iam_management.json
8+
9+
tags = merge(local.tags, { Name = "${upper(var.project_name)}-iam-bootstrap-iam-management" })
10+
}
11+
12+
data "aws_iam_policy_document" "iam_bootstrap_iam_management" {
13+
# Full IAM access for project-scoped resources
14+
statement {
15+
sid = "IamManageProjectResources"
16+
effect = "Allow"
17+
actions = [
18+
"iam:GetRole*",
19+
"iam:GetPolicy*",
20+
"iam:ListRole*",
21+
"iam:ListPolicies",
22+
"iam:ListAttachedRolePolicies",
23+
"iam:ListPolicyVersions",
24+
"iam:ListPolicyTags",
25+
"iam:ListOpenIDConnectProviders",
26+
"iam:ListOpenIDConnectProviderTags",
27+
"iam:GetOpenIDConnectProvider",
28+
"iam:CreateRole",
29+
"iam:DeleteRole",
30+
"iam:UpdateRole",
31+
"iam:UpdateAssumeRolePolicy",
32+
"iam:PutRolePolicy",
33+
"iam:PutRolePermissionsBoundary",
34+
"iam:AttachRolePolicy",
35+
"iam:DetachRolePolicy",
36+
"iam:CreatePolicy",
37+
"iam:CreatePolicyVersion",
38+
"iam:DeletePolicy",
39+
"iam:DeletePolicyVersion",
40+
"iam:SetDefaultPolicyVersion",
41+
"iam:TagRole",
42+
"iam:TagPolicy",
43+
"iam:UntagRole",
44+
"iam:UntagPolicy",
45+
"iam:PassRole",
46+
"iam:TagOpenIDConnectProvider",
47+
"iam:UntagOpenIDConnectProvider",
48+
"iam:CreateOpenIDConnectProvider",
49+
"iam:DeleteOpenIDConnectProvider",
50+
"iam:UpdateOpenIDConnectProviderThumbprint",
51+
"iam:AddClientIDToOpenIDConnectProvider",
52+
"iam:RemoveClientIDFromOpenIDConnectProvider",
53+
]
54+
resources = [
55+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-api-deployment-role",
56+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-iam-bootstrap-role",
57+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.project_name}-terraform-developer-role",
58+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/terraform-developer-role",
59+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${upper(var.project_name)}-*",
60+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${lower(var.project_name)}-*",
61+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/service-policies/*",
62+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${local.stack_name}-*",
63+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com",
64+
]
65+
}
66+
67+
# Read-only IAM access for Terraform plan/discovery
68+
statement {
69+
sid = "IamReadOnly"
70+
effect = "Allow"
71+
actions = [
72+
"iam:Get*",
73+
"iam:List*",
74+
]
75+
resources = [
76+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/*",
77+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/*",
78+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/*",
79+
]
80+
}
81+
82+
# DENY: Prevent modifying the bootstrap role itself
83+
statement {
84+
sid = "DenySelfModification"
85+
effect = "Deny"
86+
actions = [
87+
"iam:AttachRolePolicy",
88+
"iam:DetachRolePolicy",
89+
"iam:PutRolePolicy",
90+
"iam:DeleteRolePolicy",
91+
"iam:UpdateAssumeRolePolicy",
92+
"iam:PutRolePermissionsBoundary",
93+
"iam:DeleteRolePermissionsBoundary",
94+
]
95+
resources = [
96+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-roles/github-actions-iam-bootstrap-role",
97+
]
98+
}
99+
100+
# DENY: Prevent modifying the bootstrap permissions boundary
101+
statement {
102+
sid = "DenyBootstrapBoundaryModification"
103+
effect = "Deny"
104+
actions = [
105+
"iam:CreatePolicyVersion",
106+
"iam:DeletePolicy",
107+
"iam:DeletePolicyVersion",
108+
"iam:SetDefaultPolicyVersion",
109+
]
110+
resources = [
111+
"arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${lower(var.project_name)}-iam-bootstrap-permissions-boundary",
112+
]
113+
}
114+
}
115+
116+
# Terraform state management policy
117+
resource "aws_iam_policy" "iam_bootstrap_terraform_state" {
118+
name = "${upper(var.project_name)}-iam-bootstrap-terraform-state"
119+
description = "Allows the IAM bootstrap role to manage Terraform state for the iams-developer-roles stack"
120+
path = "/service-policies/"
121+
122+
policy = data.aws_iam_policy_document.iam_bootstrap_terraform_state.json
123+
124+
tags = merge(local.tags, { Name = "${upper(var.project_name)}-iam-bootstrap-terraform-state" })
125+
}
126+
127+
data "aws_iam_policy_document" "iam_bootstrap_terraform_state" {
128+
# S3 state bucket access
129+
statement {
130+
sid = "TerraformStateS3Access"
131+
effect = "Allow"
132+
actions = [
133+
"s3:ListBucket",
134+
"s3:GetObject",
135+
"s3:PutObject",
136+
"s3:DeleteObject",
137+
]
138+
resources = [
139+
"${local.terraform_state_bucket_arn}",
140+
"${local.terraform_state_bucket_arn}/*",
141+
]
142+
}
143+
}
144+
145+
resource "aws_iam_role_policy_attachment" "iam_bootstrap_iam_management" {
146+
role = aws_iam_role.github_actions_iam_bootstrap.name
147+
policy_arn = aws_iam_policy.iam_bootstrap_iam_management.arn
148+
}
149+
150+
resource "aws_iam_role_policy_attachment" "iam_bootstrap_terraform_state" {
151+
role = aws_iam_role.github_actions_iam_bootstrap.name
152+
policy_arn = aws_iam_policy.iam_bootstrap_terraform_state.arn
153+
}

infrastructure/stacks/iams-developer-roles/github_actions_role.tf

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,64 @@ resource "aws_iam_role" "github_actions" {
3030
}
3131
)
3232
}
33+
34+
35+
# GitHub Actions IAM Bootstrap Role
36+
# It can update the main deployment role's policies but cannot modify itself.
37+
resource "aws_iam_role" "github_actions_iam_bootstrap" {
38+
name = "github-actions-iam-bootstrap-role"
39+
description = "Role for GitHub Actions to deploy IAM infrastructure (iams-developer-roles stack only)"
40+
permissions_boundary = aws_iam_policy.iam_bootstrap_permissions_boundary.arn
41+
path = "/service-roles/"
42+
43+
assume_role_policy = data.aws_iam_policy_document.github_actions_iam_bootstrap_assume_role.json
44+
45+
tags = merge(
46+
local.tags,
47+
{
48+
Name = "github-actions-iam-bootstrap-role"
49+
}
50+
)
51+
}
52+
53+
data "aws_iam_policy_document" "github_actions_iam_bootstrap_assume_role" {
54+
statement {
55+
sid = "OidcAssumeRoleForIamBootstrap"
56+
effect = "Allow"
57+
actions = ["sts:AssumeRoleWithWebIdentity"]
58+
59+
principals {
60+
type = "Federated"
61+
identifiers = [
62+
aws_iam_openid_connect_provider.github.arn
63+
]
64+
}
65+
66+
condition {
67+
test = "StringEquals"
68+
variable = "token.actions.githubusercontent.com:aud"
69+
values = ["sts.amazonaws.com"]
70+
}
71+
72+
# Only allow from main branch (and events triggered from main)
73+
condition {
74+
test = "StringLike"
75+
variable = "token.actions.githubusercontent.com:sub"
76+
values = [
77+
"repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main",
78+
"repo:${var.github_org}/${var.github_repo}:environment:*",
79+
]
80+
}
81+
82+
# Only allow from the IAM bootstrap and base deployment workflows
83+
condition {
84+
test = "StringLike"
85+
variable = "token.actions.githubusercontent.com:job_workflow_ref"
86+
values = [
87+
"${var.github_org}/${var.github_repo}/.github/workflows/iam-bootstrap-deploy.yaml@*",
88+
"${var.github_org}/${var.github_repo}/.github/workflows/base-deploy.yml@*",
89+
"${var.github_org}/${var.github_repo}/.github/workflows/cicd-2-publish.yaml@*",
90+
]
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)