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 logintoken will not do. - A repository whose configuration uses compliance.tf module sources, and permission to add repository secrets.
Step 1: Store the token
- Go to your repository on GitHub
- Navigate to Settings > Secrets and variables > Actions
- Click New repository secret
- Name:
CTF_TOKEN - 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 tfplanArtifact 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 artifactsCheckov 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
fiThe 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 registryin the Terraform Init step. The variable is not visible to that step, or its name does not match the host insource. Set it in the job-levelenvblock, as in the complete example.401 Unauthorizedor403 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.jsonis not valid JSON. The setup-terraform wrapper, described under the complete example.
Next steps
- Audit evidence - turn the plan artifacts this workflow uploads into evidence an auditor accepts
- Turn controls on and off - relax or add controls per module in the
sourceline - CI/CD integration - multi-environment setup, retention guidance and shared troubleshooting