From a5c2f4a2de46af10e6931759090d9a695c38a1b8 Mon Sep 17 00:00:00 2001 From: Oleksandr Kuzminskyi Date: Sat, 4 Jul 2026 14:10:54 -0700 Subject: [PATCH 1/2] Harden service-repo workflows: pin actions, fix semgrep findings, add docs Resolves the 26 blocking Semgrep SAST findings that the injected vuln-scanner-pr.yml sast-check job reports against the workflow templates this module ships (surfaced in aws-service-infrahouse-app PR #25). Workflow templates: - Pin every third-party action to a full 40-char commit SHA with a # vN comment (github-actions-mutable-action-tag). trufflehog@main, a mutable branch ref, is pinned to v3.95.8. Same-repo reusable-workflow calls stay unpinned. - vuln-scanner-pr.yml: move GITHUB_TOKEN out of the workflow-level env into the two steps that use it (gha-workflow-env-secret). - terraform-drift-wrapper.yml: drop `secrets: inherit` (secrets-inherit). The called workflow only uses the automatic GITHUB_TOKEN, which is always available to reusable workflows and cannot be listed in a secrets: map. - Render environment matrices as YAML block sequences instead of jsonencode(...) flow arrays so output passes yamllint's commas rule; add the missing truthy disable on the wrapper's on: key. Keep the pins current: - Add renovate.json (service-repo previously injected none) with helpers:pinGitHubActionDigests, dashboard-approval gated, no automerge. Docs: - Ship .github/README.md into each service repo (repo-README.md) explaining the managed workflows and renovate.json, since renovate.json cannot carry a managed-by banner comment. - Add a maintainer-facing templates/README.md resource map. Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/service-repo/files.tf | 26 +++++++++ modules/service-repo/templates/README.md | 42 ++++++++++++++ modules/service-repo/templates/renovate.json | 16 +++++ modules/service-repo/templates/repo-README.md | 58 +++++++++++++++++++ .../templates/secrets-scanner.yml | 4 +- .../templates/terraform-CD.yml.tftpl | 13 +++-- .../templates/terraform-CI.yml.tftpl | 13 +++-- .../terraform-drift-wrapper.yml.tftpl | 10 ++-- .../templates/terraform-drift.yml | 10 ++-- .../templates/vuln-scanner-pr.yml | 15 ++--- 10 files changed, 179 insertions(+), 28 deletions(-) create mode 100644 modules/service-repo/templates/README.md create mode 100644 modules/service-repo/templates/renovate.json create mode 100644 modules/service-repo/templates/repo-README.md diff --git a/modules/service-repo/files.tf b/modules/service-repo/files.tf index 036e435..db66cbd 100644 --- a/modules/service-repo/files.tf +++ b/modules/service-repo/files.tf @@ -13,6 +13,32 @@ resource "github_repository_file" "gitignore" { } } +resource "github_repository_file" "renovate" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = "renovate.json" + content = file("${path.module}/templates/renovate.json") + commit_message = "Add renovate.json" + overwrite_on_create = true +} + +# Consumer-facing docs for the managed workflows + renovate.json. Placed under .github/ so it does +# not shadow a hand-written root README.md. +resource "github_repository_file" "repo_readme" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = ".github/README.md" + content = file("${path.module}/templates/repo-README.md") + commit_message = "Add .github/README.md documenting managed CI/CD" + overwrite_on_create = true +} + resource "github_repository_file" "makefile_root" { count = var.archived ? 0 : 1 depends_on = [ diff --git a/modules/service-repo/templates/README.md b/modules/service-repo/templates/README.md new file mode 100644 index 0000000..dd8a94e --- /dev/null +++ b/modules/service-repo/templates/README.md @@ -0,0 +1,42 @@ + + +# service-repo templates + +Files in this directory are pushed verbatim (or rendered from `.tftpl`) into every repository managed +by the `service-repo` module. Downstream repos must **not** edit them; they are overwritten on the +next `terraform apply`. To change behavior, edit the template here and open a PR against +[github-control](https://github.com/infrahouse/github-control). + +The consumer-facing explanation of what each workflow and `renovate.json` does is itself shipped to +the repos as `.github/README.md` (from `repo-README.md`) — read that file for the prose. + +## Resource map + +| Template | Injected as | Defined in | +|----------|-------------|------------| +| `repo-README.md` | `.github/README.md` | `../files.tf` | +| `renovate.json` | `renovate.json` | `../files.tf` | +| `terraform-CI.yml.tftpl` | `.github/workflows/terraform-CI.yml` | `../workflows.tf` | +| `terraform-CD.yml.tftpl` | `.github/workflows/terraform-CD.yml` | `../workflows.tf` | +| `terraform-drift.yml` | `.github/workflows/terraform-drift.yml` | `../workflows.tf` | +| `terraform-drift-wrapper.yml.tftpl` | `.github/workflows/terraform-drift-wrapper.yml` | `../workflows.tf` | +| `secrets-scanner.yml` | `.github/workflows/secrets-scanner.yml` | `../workflows.tf` | +| `vuln-scanner-pr.yml` | `.github/workflows/vuln-scanner-pr.yml` | `../workflows.tf` | + +Scaffolding templates (`gitignore`, `Makefile.root`, `makefile.mk`, `Makefile.env`, +`requirements.txt`, `terraform.tf.tftpl`, `terraform.tfvars.tftpl`, `releases.auto.tfvars.tftpl`) +build the Terraform root modules and tooling per environment — see `../files.tf` and +`../environments.tf`. + +## Conventions + +- **Action pinning.** Every third-party action is pinned to a full 40-char commit SHA with a trailing + `# vN` comment, satisfying the Semgrep `github-actions-mutable-action-tag` rule. Same-repo + reusable-workflow calls (`./.github/workflows/...`) stay unpinned. Renovate keeps the SHAs current. +- **Matrix rendering.** Environment matrices render as YAML block sequences (`- "env"`), not + `jsonencode(...)` flow arrays, so the output passes yamllint's `commas` rule. +- **`renovate.json` has no banner.** JSON has no comments, so it cannot carry the managed-by header + the other templates use. It is managed the same way regardless. diff --git a/modules/service-repo/templates/renovate.json b/modules/service-repo/templates/renovate.json new file mode 100644 index 0000000..c34d1ab --- /dev/null +++ b/modules/service-repo/templates/renovate.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigests" + ], + "dependencyDashboardApproval": true, + "prConcurrentLimit": 1, + "rebaseWhen": "conflicted", + "packageRules": [ + { + "matchManagers": ["github-actions"], + "automerge": false + } + ] +} diff --git a/modules/service-repo/templates/repo-README.md b/modules/service-repo/templates/repo-README.md new file mode 100644 index 0000000..ce74cb0 --- /dev/null +++ b/modules/service-repo/templates/repo-README.md @@ -0,0 +1,58 @@ + + +# CI/CD & automation (managed) + +This repository's `.github/workflows/` and `renovate.json` are generated and kept in sync by +[github-control](https://github.com/infrahouse/github-control) through the `service-repo` Terraform +module. **Do not edit those files here** — any change is reverted on the next `terraform apply`. To +change how CI/CD behaves, open a PR against github-control. + +## Workflows + +### `terraform-CI.yml` — plan on every PR + +Runs on `pull_request`. For each environment it runs `terraform plan`, posts the plan as a PR comment +(`ih-plan publish`), and uploads the plan file to the state bucket (`ih-plan upload`) so the exact +reviewed plan can be applied after merge. + +### `terraform-CD.yml` — apply on merge + +Runs when a PR is merged. For each environment it downloads the plan CI stored, applies it, removes +the stored plan, then runs `make smoke`. Environments deploy in the order set by each environment's +`deploy_order`. + +### `terraform-drift.yml` — reusable drift check + +A `workflow_call` reusable workflow. Runs `terraform plan -detailed-exitcode` for one environment; if +drift is detected it opens a `config-drift`-labeled PR to reconcile. + +### `terraform-drift-wrapper.yml` — scheduled drift sweep + +Runs daily (and on demand via `workflow_dispatch`). Calls `terraform-drift.yml` for every environment +and posts to the `#infra` Slack channel on failure. + +### `secrets-scanner.yml` — leaked-secret scan + +Runs on PRs and the merge queue. Uses TruffleHog to scan the diff for verified secrets. + +### `vuln-scanner-pr.yml` — dependency + SAST scan + +Runs on PRs and the merge queue. One job runs OSV-Scanner for dependency CVEs; another runs Semgrep +SAST (`semgrep scan --error`), which blocks the PR on findings. + +## `renovate.json` + +Configures [Renovate](https://docs.renovatebot.com/) for this repo. Every GitHub Action in the +workflows above is pinned to a full commit SHA (with a trailing `# vN` comment) to prevent +supply-chain attacks from silently repointed tags or branches. Renovate's +`helpers:pinGitHubActionDigests` preset keeps those pins current — it opens a PR bumping the SHA and +comment when a new version ships. Updates are queued on the Dependency Dashboard for approval +(`dependencyDashboardApproval`) and never automerge. + +`renovate.json` is strict JSON, so it carries no "managed by Terraform" banner comment, but it is +managed exactly like the workflows — edit it in github-control, not here. diff --git a/modules/service-repo/templates/secrets-scanner.yml b/modules/service-repo/templates/secrets-scanner.yml index 0d6cb75..21d732a 100644 --- a/modules/service-repo/templates/secrets-scanner.yml +++ b/modules/service-repo/templates/secrets-scanner.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@main + uses: trufflesecurity/trufflehog@00155c9dc586f34d189adc83d3ac2698c2ec551f # v3.95.8 with: path: ./ base: ${{ github.event.repository.default_branch }} diff --git a/modules/service-repo/templates/terraform-CD.yml.tftpl b/modules/service-repo/templates/terraform-CD.yml.tftpl index c94b4d1..1bb69c3 100644 --- a/modules/service-repo/templates/terraform-CD.yml.tftpl +++ b/modules/service-repo/templates/terraform-CD.yml.tftpl @@ -24,7 +24,10 @@ jobs: strategy: fail-fast: false matrix: - env: ${jsonencode(environments_by_order[order])} + env: +%{ for env in environments_by_order[order] ~} + - "${env}" +%{ endfor ~} name: "Terraform Apply $${{ matrix.env }}" runs-on: ubuntu-24.04 environment: "live-$${{ matrix.env }}" @@ -42,7 +45,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v6" + uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6 - name: "Extract Variables" id: "extract_vars" @@ -55,7 +58,7 @@ jobs: echo "ROLE_STATE_MANAGER=$ROLE_SM" >> "$GITHUB_OUTPUT" - name: "Configure AWS Credentials" - uses: "aws-actions/configure-aws-credentials@v6" + uses: "aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b" # v6 with: role-to-assume: "$${{ steps.extract_vars.outputs.ROLE_GITHUB }}" role-session-name: "github-actions-$${{ matrix.env }}" @@ -67,12 +70,12 @@ jobs: echo "IH_TF_VERSION=$(cat .terraform-version)" >> "$GITHUB_OUTPUT" - name: "Setup Terraform" - uses: "hashicorp/setup-terraform@v4" + uses: "hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e" # v4 with: terraform_version: "$${{ steps.terraform_version.outputs.IH_TF_VERSION }}" - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" # v6 with: python-version: "3.14" diff --git a/modules/service-repo/templates/terraform-CI.yml.tftpl b/modules/service-repo/templates/terraform-CI.yml.tftpl index c7d72f3..3d197d2 100644 --- a/modules/service-repo/templates/terraform-CI.yml.tftpl +++ b/modules/service-repo/templates/terraform-CI.yml.tftpl @@ -18,7 +18,10 @@ jobs: strategy: fail-fast: false matrix: - env: ${jsonencode(environments)} + env: +%{ for env in environments ~} + - "${env}" +%{ endfor ~} name: "Terraform Plan $${{ matrix.env }}" runs-on: ubuntu-24.04 environment: "continuous-integration-$${{ matrix.env }}" @@ -36,7 +39,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v6" + uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6 - name: "Extract Variables" id: "extract_vars" @@ -49,7 +52,7 @@ jobs: echo "ROLE_STATE_MANAGER=$ROLE_SM" >> "$GITHUB_OUTPUT" - name: "Configure AWS Credentials" - uses: "aws-actions/configure-aws-credentials@v6" + uses: "aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b" # v6 with: role-to-assume: "$${{ steps.extract_vars.outputs.ROLE_GITHUB }}" role-session-name: "github-actions-$${{ matrix.env }}" @@ -61,12 +64,12 @@ jobs: echo "IH_TF_VERSION=$(cat .terraform-version)" >> "$GITHUB_OUTPUT" - name: "Setup Terraform" - uses: "hashicorp/setup-terraform@v4" + uses: "hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e" # v4 with: terraform_version: "$${{ steps.terraform_version.outputs.IH_TF_VERSION }}" - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" # v6 with: python-version: "3.14" diff --git a/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl b/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl index 8206f48..529cf58 100644 --- a/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl +++ b/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl @@ -1,7 +1,7 @@ --- name: "[Meta] Terraform Drift Detection" -on: +on: # yamllint disable-line rule:truthy workflow_dispatch: schedule: # Runs daily at 10:XX AM UTC @@ -11,11 +11,13 @@ jobs: config-drift-check: strategy: matrix: - env: ${jsonencode(environments)} + env: +%{ for env in environments ~} + - "${env}" +%{ endfor ~} uses: "./.github/workflows/terraform-drift.yml" with: env: "$${{ matrix.env }}" - secrets: "inherit" notify_on_failure: name: "Notify #infra slack on failure" @@ -24,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Post to Slack - uses: "slackapi/slack-github-action@v2.1.1" + uses: "slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a" # v2.1.1 with: method: "chat.postMessage" token: $${{ secrets.SLACK_BOT_TOKEN }} diff --git a/modules/service-repo/templates/terraform-drift.yml b/modules/service-repo/templates/terraform-drift.yml index 11252fc..7386032 100644 --- a/modules/service-repo/templates/terraform-drift.yml +++ b/modules/service-repo/templates/terraform-drift.yml @@ -35,7 +35,7 @@ jobs: steps: - name: "Checkout" - uses: "actions/checkout@v6" + uses: "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10" # v6 - name: "Extract Variables" id: "extract_vars" @@ -47,7 +47,7 @@ jobs: echo "REGION=$REGION" >> "$GITHUB_OUTPUT" - name: "Configure AWS Credentials" - uses: "aws-actions/configure-aws-credentials@v6" + uses: "aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b" # v6 with: role-to-assume: "${{ vars.ROLE_GITHUB }}" role-session-name: "github-actions-${{ inputs.env }}" @@ -59,12 +59,12 @@ jobs: echo "IH_TF_VERSION=$(cat .terraform-version)" >> "$GITHUB_OUTPUT" - name: "Setup Terraform" - uses: "hashicorp/setup-terraform@v4" + uses: "hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e" # v4 with: terraform_version: "${{ steps.terraform_version.outputs.IH_TF_VERSION }}" - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" # v6 with: python-version: "3.14" @@ -108,7 +108,7 @@ jobs: - name: "Create Pull Request" if: "steps.check_label.outputs.label_exists == 'false'" - uses: "peter-evans/create-pull-request@v8" + uses: "peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1" # v8 with: token: "${{ steps.app-token.outputs.token }}" base: "main" diff --git a/modules/service-repo/templates/vuln-scanner-pr.yml b/modules/service-repo/templates/vuln-scanner-pr.yml index 5ceca85..b425aa7 100644 --- a/modules/service-repo/templates/vuln-scanner-pr.yml +++ b/modules/service-repo/templates/vuln-scanner-pr.yml @@ -13,16 +13,13 @@ permissions: contents: read pull-requests: write -env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - jobs: vulnerability-check: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.14" - name: Install dependencies @@ -33,6 +30,8 @@ jobs: chmod +x /usr/local/bin/osv-scanner osv-scanner --version - name: Detect vulnerabilities + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | if [ -n "${{ github.event.pull_request.number }}" ]; then ih-github scan \ @@ -45,15 +44,17 @@ jobs: sast-check: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.14" - name: Install dependencies run: pip install -r requirements.txt - name: SAST + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" run: | if [ -n "${{ github.event.pull_request.number }}" ]; then ih-github run \ From 9cde0a0524d96b9aeb63f6553ee30ea2d5fdd741 Mon Sep 17 00:00:00 2001 From: Oleksandr Kuzminskyi Date: Sat, 4 Jul 2026 14:33:56 -0700 Subject: [PATCH 2/2] Fix plan drift: keep infrahouse8 team maintainer, stabilize makefile commit message The full CI plan surfaced two pre-existing drifts unrelated to the workflow hardening; since CD applies the whole plan on merge, fix them here so they don't ride along. - teams.tf set only `username` in the members block, so Terraform defaulted every team member to role "member" and wanted to demote infrahouse8 (the out-of-band admin) from maintainer. Add an explicit `team_role` per member (infrahouse8 = maintainer, others = member) and set it in the content block. - service-repo makefile_fragment had commit_message "Add makefiles/Makefile" while state held "Update makefiles/Makefile", producing metadata-only churn. Match the config to state. Only aws-service-infrahouse-app uses the module. Co-Authored-By: Claude Opus 4.8 (1M context) --- locals.tf | 9 ++++++--- modules/service-repo/files.tf | 2 +- teams.tf | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/locals.tf b/locals.tf index 82d859d..916b697 100644 --- a/locals.tf +++ b/locals.tf @@ -7,21 +7,24 @@ locals { team_members = { "akuzminsky" = { - role = "member" + role = "member" + team_role = "member" teams = [ github_team.dev.name, github_team.admins.name, ] } "infrahouse8" = { - role = "admin" + role = "admin" + team_role = "maintainer" teams = [ github_team.dev.name, github_team.admins.name, ] } "naumenko" = { - role = "member" + role = "member" + team_role = "member" teams = [ github_team.dev.name, ] diff --git a/modules/service-repo/files.tf b/modules/service-repo/files.tf index db66cbd..aeb0fe9 100644 --- a/modules/service-repo/files.tf +++ b/modules/service-repo/files.tf @@ -59,7 +59,7 @@ resource "github_repository_file" "makefile_fragment" { repository = github_repository.this.name file = "makefiles/Makefile" content = file("${path.module}/templates/makefile.mk") - commit_message = "Add makefiles/Makefile" + commit_message = "Update makefiles/Makefile" overwrite_on_create = true } diff --git a/teams.tf b/teams.tf index 69a3a74..a58aa21 100644 --- a/teams.tf +++ b/teams.tf @@ -15,6 +15,7 @@ resource "github_team_members" "dev" { ) content { username = members.key + role = try(local.team_members[members.key].team_role, "member") } } } @@ -36,6 +37,7 @@ resource "github_team_members" "admins" { ) content { username = members.key + role = try(local.team_members[members.key].team_role, "member") } } }