Sign in
Menu
Sign up
Docs
Check before applyCI/CD integration

GitHub Actions

For teams that run Terraform from GitHub Actions. When you finish, your workflow's terraform init downloads compliance.tf modules with a repository secret, and plan and apply run as before. Authentication concepts shared by every platform are on CI/CD integration.

Before you start

  • An organization-owned registry token for this pipeline, created on the organization's tokens screen: see Create a token. CI/CD integration explains why a personal or terraform login token will not do.
  • A repository whose configuration uses compliance.tf module sources, and permission to add repository secrets.

Step 1: Store the token

  1. Go to your repository on GitHub
  2. Navigate to Settings > Secrets and variables > Actions
  3. Click New repository secret
  4. Name: CTF_TOKEN
  5. Value: paste the organization token

For multi-environment setups, use GitHub Environments with separate secrets per environment.

Step 2: Configure the pipeline

Terraform gets registry credentials from TF_TOKEN_<hostname> environment variables. Dots in the hostname become underscores. For the SOC 2 host soc2.compliance.tf, the variable is TF_TOKEN_soc2_compliance_tf. Pick the variable that matches the hostname in your module source:

module "s3_bucket" {
  source  = "soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = "5.0.0"
  # ...
}

If your Terraform workflow already exists, add only this line. Put it at job level so every step that runs Terraform sees it:

env:
  TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}

Checkout, setup, init, plan and apply stay the workflow you already run. Sources in the HTTPS form (https://soc2.compliance.tf/...) read .netrc instead; CI/CD integration has the step.

Complete example

This workflow runs terraform plan on pull requests and terraform apply on merge to main. The env block authenticates the compliance.tf registry; the rest is standard Terraform CI: init, validate, plan, save the plan as JSON and upload it, comment the plan on the pull request, fail the job on a failed plan, and apply on main.

name: Terraform

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read
  pull-requests: write

jobs:
  terraform:
    name: Terraform Plan & Apply
    runs-on: ubuntu-latest

    env:
      # compliance.tf registry authentication
      TF_TOKEN_soc2_compliance_tf: ${{ secrets.CTF_TOKEN }}
      # Disable interactive input prompts
      TF_INPUT: "false"

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.12.0"

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        continue-on-error: true

      - name: Save Plan JSON
        if: steps.plan.outcome == 'success'
        run: terraform show -json tfplan > plan.json

      - name: Upload Plan Artifacts
        if: steps.plan.outcome == 'success'
        uses: actions/upload-artifact@v4
        with:
          name: terraform-plan
          path: |
            tfplan
            plan.json
          retention-days: 90  # set to the retention you agree with your assessor; see CI/CD integration

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan: \`${{ steps.plan.outcome }}\`

            <details><summary>Plan Output</summary>

            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`

            </details>

            *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

      - name: Plan Status Check
        if: steps.plan.outcome == 'failure'
        run: exit 1

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan

Artifact retention is covered once on CI/CD integration.

Optional checks

Checkov

Modules from compliance.tf enforce controls at the module level during terraform plan. Checkov adds an independent second-opinion scan of the plan.

Add these steps after Save Plan JSON; they consume the plan.json that step produces. The always() && steps.plan.outcome == 'success' condition exists because the plan step uses continue-on-error, so Checkov runs only on successful plans.

      - name: Run Checkov
        if: always() && steps.plan.outcome == 'success'
        uses: bridgecrewio/checkov-action@v12
        with:
          file: plan.json
          framework: terraform_plan
          output_format: cli,json
          output_file_path: console,checkov-report.json
          quiet: true

      - name: Upload Checkov Report
        if: always() && steps.plan.outcome == 'success'
        uses: actions/upload-artifact@v4
        with:
          name: checkov-report
          path: checkov-report.json
          retention-days: 90  # same retention as the plan artifacts

Checkov also reports checks that compliance.tf does not enforce. So before you call a finding a gap, compare it against the framework page's enforced and mapped-only lists.

Module source governance

This step flags upstream module sources that should be using compliance.tf (terraform-aws-modules/ without a compliance.tf prefix) and disabled controls (?disable=) for audit review:

      - name: Check Module Sources
        if: github.event_name == 'pull_request'
        run: |
          echo "## Module Source Governance Report" >> $GITHUB_STEP_SUMMARY

          # Flag terraform-aws-modules sources not using compliance.tf
          UPSTREAM=$(grep -rn 'source.*=.*"terraform-aws-modules/' --include='*.tf' . \
            | grep -v 'compliance\.tf/' || true)
          if [ -n "$UPSTREAM" ]; then
            echo "### ⚠️ Upstream modules without compliance.tf" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "$UPSTREAM" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "::warning::Found terraform-aws-modules sources not using compliance.tf"
          else
            echo "✅ All terraform-aws-modules sources use compliance.tf" >> $GITHUB_STEP_SUMMARY
          fi

          # Flag any disabled controls for audit visibility
          DISABLED=$(grep -rn 'disable=' --include='*.tf' . || true)
          if [ -n "$DISABLED" ]; then
            echo "### ℹ️ Disabled controls (requires audit justification)" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "$DISABLED" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
          fi

The step writes a summary in the GitHub Actions UI. To block the pull request, put exit 1 at the end of the if [ -n "$UPSTREAM" ] block. For teams in active migration, start with warnings. Switch to blocking once the flagged modules have moved. See Migrate the rest of the estate.

ctfkit by compliance.tf

ctfkit by compliance.tf is open source under Apache-2.0. It replaces the grep-based governance step above with a pinned analyzer: it flags upstream module sources, disabled controls, unpinned sources and sensitive plan changes, and names the compliance.tf module that closes each gap. A pre_plan scan needs only a checkout, and the action downloads ctfkit with the job token:

      - uses: compliancetf/ctfkit-action@v0
        with:
          command: scan
          stage: pre_plan
          chdir: infra
          sarif: 'false'
          pr-comment: 'true'

@v0 follows the newest action release, and without version it installs the newest ctfkit release; pin both for a reproducible pipeline. Permissions, code scanning upload and a complete workflow are on the ctfkit GitHub Actions page.

Check the result

Open the run of the workflow on a pull request. In the Terraform Init step, each compliance.tf module is listed as downloaded from its host, for example Downloading soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws 5.0.0 for s3_bucket..., and the plan comment appears on the pull request. An init that stops at the registry is one of the errors below.

Troubleshooting

  • Error accessing remote module registry in the Terraform Init step. The variable is not visible to that step, or its name does not match the host in source. Set it in the job-level env block, as in the complete example.
  • 401 Unauthorized or 403 Forbidden. The token is revoked or short-lived, or your subscription does not cover that framework host. See Troubleshooting on CI/CD integration for these and the shared errors.
  • plan.json is not valid JSON. The setup-terraform wrapper, described under the complete example.

Next steps

On this page

Ask AI about this

Help improve this page