Skip to content
Open
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
87 changes: 87 additions & 0 deletions .github/workflows/tf-apply.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Terraform Apply

on:
push:
branches: [main]
paths:
- 'infra/terraform/website/**'
- '.github/workflows/tf-apply.yml'
workflow_dispatch:
inputs:
ref:
description: 'Git ref to apply (default: current default branch)'
required: false
type: string

concurrency:
group: tf-apply-website
cancel-in-progress: false

permissions:
contents: read
id-token: write

jobs:
apply:
name: terraform apply (infra/terraform/website)
runs-on: ubuntu-latest
environment: iii-website-prod-tf-apply
timeout-minutes: 15

env:
AWS_REGION: us-east-1
TF_IN_AUTOMATION: 'true'

steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.9.8'
terraform_wrapper: false

- name: Configure AWS credentials (GitHub OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_TF_APPLY_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Terraform init
working-directory: infra/terraform/website
run: terraform init -input=false

- name: Terraform apply
id: apply
working-directory: infra/terraform/website
env:
TF_VAR_alarm_email: ${{ secrets.ALARM_EMAIL }}
run: |
set -o pipefail
terraform apply -input=false -auto-approve -no-color 2>&1 | tee apply.txt
{
echo 'apply<<TF_APPLY_EOF'
tail -c 60000 apply.txt
echo 'TF_APPLY_EOF'
} >> "$GITHUB_OUTPUT"

- name: Job summary
if: always()
env:
APPLY: ${{ steps.apply.outputs.apply }}
run: |
{
echo "## terraform apply — \`infra/terraform/website\`"
echo
echo "- Commit: \`${{ github.sha }}\`"
echo "- Ref: \`${{ inputs.ref || github.ref }}\`"
echo
echo '<details><summary>Apply output</summary>'
echo
echo '```'
echo "${APPLY:-(no apply output captured)}"
echo '```'
echo
echo '</details>'
} >> "$GITHUB_STEP_SUMMARY"
Comment on lines +69 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim the raw apply log from the job summary.

Writing the full terraform apply output into $GITHUB_STEP_SUMMARY makes it more durable and easier to skim than the job log, which increases the chance that sensitive diffs or provider error details get surfaced unnecessarily. Consider replacing it with a short status message and a link back to the run logs, or redacting the captured output first.

Suggested change
-            echo '```'
-            echo "${APPLY:-(no apply output captured)}"
-            echo '```'
+            echo "Apply completed; see the job log for full output."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tf-apply.yml around lines 69 - 87, The job currently
writes the full terraform apply output (the APPLY env variable) into
$GITHUB_STEP_SUMMARY; change this to avoid exposing raw logs by replacing the
line that echoes "${APPLY:-(no apply output captured)}" with a short status
message (e.g., "Apply completed; see the job log for full output.") or a
redacted/trimmed version, and ensure the modified output still appends to
$GITHUB_STEP_SUMMARY rather than the raw APPLY content; update the same block
that builds the Job summary (the echo lines around APPLY and the surrounding
details tags) so only the safe status text or link is emitted.

40 changes: 38 additions & 2 deletions infra/terraform/website/iam_github_oidc.tf
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,44 @@ resource "aws_iam_role_policy_attachment" "github_deploy_website" {
policy_arn = aws_iam_policy.github_deploy_website.arn
}

# Read-only role for `tf-plan.yml`. Separate from the deploy role so a malicious
# PR can't accidentally run `terraform apply` with write permissions.
data "aws_iam_policy_document" "github_tf_apply_trust" {
statement {
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"]
}

condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = [
"repo:${var.github_repo}:environment:${var.github_tf_apply_environment}",
]
}
}
}

resource "aws_iam_role" "github_tf_apply" {
name = "iii-website-prod-github-tf-apply"
description = "Assumed by GitHub Actions from the ${var.github_tf_apply_environment} environment to run `terraform apply` against infra/terraform/website. Configure that environment with required reviewers in repo settings to gate applies."
assume_role_policy = data.aws_iam_policy_document.github_tf_apply_trust.json
max_session_duration = 3600
}

resource "aws_iam_role_policy_attachment" "github_tf_apply_admin" {
role = aws_iam_role.github_tf_apply.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}
Comment on lines +123 to +126
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace AdministratorAccess with a scoped policy.

Attaching the AWS managed admin policy gives this GitHub OIDC role full account-wide power, which is far broader than the website module needs and makes a workflow compromise much more damaging. Please scope this to the Terraform resources the module actually manages, or split out a narrower apply role.

🧰 Tools
🪛 Checkov (3.2.525)

[high] 123-126: Disallow IAM roles, users, and groups from using the AWS AdministratorAccess policy

(CKV_AWS_274)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/terraform/website/iam_github_oidc.tf` around lines 123 - 126, The role
aws_iam_role.github_tf_apply is currently attached to the overly-broad AWS
managed AdministratorAccess via
aws_iam_role_policy_attachment.github_tf_apply_admin; replace this with a scoped
policy by creating a least-privilege aws_iam_policy (e.g.,
aws_iam_policy.github_tf_apply_policy) that only grants the specific
actions/resources the website Terraform needs (state S3 bucket, DynamoDB lock
table, CloudFront/S3 deploy, Route53, etc.), then update
aws_iam_role_policy_attachment.github_tf_apply_admin to use policy_arn =
aws_iam_policy.github_tf_apply_policy. Alternatively, split into separate
narrower roles if apply and other workflows need different scopes and reference
those role names in place of aws_iam_role.github_tf_apply.


data "aws_iam_policy_document" "github_tf_plan_trust" {
statement {
effect = "Allow"
Expand Down
5 changes: 5 additions & 0 deletions infra/terraform/website/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ output "github_tf_plan_role_arn" {
value = aws_iam_role.github_tf_plan.arn
}

output "github_tf_apply_role_arn" {
description = "Admin IAM role ARN assumed by the tf-apply GitHub Actions workflow on push to main — set as repo-level GitHub secret AWS_TF_APPLY_ROLE_ARN"
value = aws_iam_role.github_tf_apply.arn
}

output "sns_alarms_topic_arn" {
description = "SNS topic ARN that receives production alarms"
value = aws_sns_topic.alarms.arn
Expand Down
19 changes: 10 additions & 9 deletions infra/terraform/website/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,28 @@ variable "github_environment" {
default = "iii-website-prod"
}

variable "github_tf_apply_environment" {
# Distinct from github_environment so the apply env can be configured with
# required reviewers without gating routine S3 deploys.
description = "GitHub environment scoping the tf-apply role. Configure required reviewers on this env in repo settings to gate applies."
type = string
default = "iii-website-prod-tf-apply"
}

variable "csp_report_only" {
description = "Send CSP as report-only instead of enforcing."
type = bool
default = true
}

variable "manage_apex_records" {
# Leave false until Phase 4 cutover. When false, the apex records are not managed
# by this module (they already exist — manually created in Route53, not
# External-DNS-owned). Flip to true after importing the existing records.
description = "Whether Terraform manages the iii.dev apex A/AAAA Route53 records."
type = bool
default = false
default = true
}

variable "manage_www_records" {
# Leave false until www.iii.dev has been released by External-DNS (argocd PR
# removing the iii-dev-www Ingress has merged + synced). Flip to true after
# importing the existing records. Decoupled from apex so the two can be
# cut over independently to limit blast radius.
description = "Whether Terraform manages the www.iii.dev A/AAAA Route53 records."
type = bool
default = false
default = true
}
Loading