Distribuée
Cloud & AI Security
← All insights

Ecosystem

Wiz + Terramate: check cloud configs before apply, not after the incident

DevSecOps case analysis: orchestrate changed Terramate stacks, scan the Terraform plan with wizcli, block critical misconfigs in CI, then correlate with Wiz runtime.

· 8 min · #wiz#terramate#terraform#iac#cspm#github-actions#cloud-config#ecosystem

Wiz is good at scanning what already runs in the cloud. Terramate is good at orchestrating dozens of Terraform / OpenTofu stacks. The usual gap: the misconfig lands at apply time, and the CSPM finds it hours or days later. Security group 0.0.0.0/0, bucket without public access block, IAM role with Action: "*" — PR review often misses them, especially when the plan touches three modules and two workspaces.

There is no native “Wiz button” inside Terramate Cloud. That is fine: the robust pattern is client-side. Terramate selects changed stacks; terraform plan produces a resolved plan; wizcli scans that plan (or the HCL) with the same policy engine as the rest of Wiz; CI blocks before apply. Then the Wiz cloud connector takes over for runtime and code-to-cloud via state.

This article describes the pipeline I set up when a team already has Terramate and Wiz (or is evaluating both): threat → CI control → GitHub Actions artefact → evidence.

Terramate → plan → Wiz scan → apply → runtime pipeline

The threat: cloud config validated too late

Three process failures — not exotic CVEs.

Plans too large to review. Terramate exists precisely because Terraform monorepos grow fast. Without terramate list --changed, CI either plans everything or nothing. Reviewers look at the HCL diff, not the real effect of variables and modules. A conditional count or for_each creates a public resource nobody saw in raw HCL.

HCL-only scans without a plan. A scanner that only reads .tf files misses what appears after resolution: remote modules, environment-injected variables, data sources. Wiz CLI can scan a Terraform directory and a plan JSON (terraform show -json) — the latter is closer to what will be applied.

Runtime CSPM as the only net. Useful, even essential (see the Wiz vs native comparison). But once the SG is open, the exposure window exists. Shift-left does not replace the CSPM: it reduces the flow of “we just shot ourselves in the foot” findings.

Roles: who does what

LayerToolJob
Monorepo orchestrationTerramatestacks, run --changed, order, tags, codegen
ProvisioningTerraform / OpenTofuplan / apply per stack
IaC policy (pre-apply)Wiz CLI (wizcli)misconfigs, secrets in code/plan, block thresholds
Runtime postureWiz CSPM / Security Graphlive findings, attack paths, code-to-cloud via state

Terramate does not replace Wiz; Wiz does not select your changed stacks. The glue is a CI workflow — and that glue is what 80% of “we have Wiz” demos are missing.

Wiz prerequisites

  1. Dedicated Wiz CI service account (not a human): WIZ_CLIENT_ID / WIZ_CLIENT_SECRET as GitHub secrets (OIDC to a secrets manager if you can).
  2. An IaC policy in Wiz with CLI enforcement set to BLOCK on Critical (and ideally High for CIS controls you have already remediated).
  3. Cloud connector (AWS/Azure/GCP) already attached — otherwise you only get preventive scanning, not the code-to-cloud loop.
  4. Optional but useful: VCS connector to map files / authors; Terraform state remains the most reliable bridge between live resource and declaration.

Artefact: GitHub Actions — changed stacks → plan JSON → wizcli

Minimal OIDC-ready workflow for AWS, aligned with the Terramate preview plus a Wiz step after plan. Adjust versions and IAM role.

name: terramate-wiz-preview

on:
  pull_request:
    paths:
      - "stacks/**"
      - "**.tf"
      - "**.tm.hcl"

permissions:
  id-token: write
  contents: read
  pull-requests: write
  security-events: write

jobs:
  preview-and-scan:
    runs-on: ubuntu-latest
    env:
      WIZ_IAC_POLICY: "Default IaC policy"
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install Terramate
        uses: terramate-io/terramate-action@v3
        with:
          version: "0.14.0"

      - name: Install Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.12.2"
          terraform_wrapper: false

      - name: List changed stacks
        id: list
        run: |
          CHANGED=$(terramate list --changed)
          echo "changed<<EOF" >> "$GITHUB_OUTPUT"
          echo "$CHANGED" >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"
          if [ -z "$CHANGED" ]; then
            echo "No changed stacks — skip."
          else
            echo "Changed stacks:"
            echo "$CHANGED"
          fi

      - name: Configure AWS credentials (OIDC)
        if: steps.list.outputs.changed != ''
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN_PLAN }}
          aws-region: eu-west-3

      - name: Terraform init (changed stacks)
        if: steps.list.outputs.changed != ''
        run: |
          terramate run --changed --parallel 1 -- \
            terraform init -lock-timeout=5m

      - name: Terraform plan (changed stacks)
        if: steps.list.outputs.changed != ''
        run: |
          terramate run --changed --parallel 5 -- \
            terraform plan -out out.tfplan -lock=false

      - name: Export plan JSON per stack
        if: steps.list.outputs.changed != ''
        run: |
          terramate run --changed -- \
            bash -c 'terraform show -json out.tfplan > out.tfplan.json'

      - name: Install wizcli
        if: steps.list.outputs.changed != ''
        run: |
          curl -fsSL -o wizcli \
            https://downloads.wiz.io/wizcli/latest/wizcli-linux-amd64
          chmod +x wizcli
          sudo mv wizcli /usr/local/bin/wizcli
          wizcli version

      - name: Authenticate wizcli
        if: steps.list.outputs.changed != ''
        env:
          WIZ_CLIENT_ID: ${{ secrets.WIZ_CLIENT_ID }}
          WIZ_CLIENT_SECRET: ${{ secrets.WIZ_CLIENT_SECRET }}
        run: wizcli auth --id "$WIZ_CLIENT_ID" --secret "$WIZ_CLIENT_SECRET"

      - name: Wiz IaC scan on plan JSON (fail on policy)
        if: steps.list.outputs.changed != ''
        run: |
          set -euo pipefail
          FAIL=0
          while IFS= read -r stack; do
            [ -z "$stack" ] && continue
            echo "::group::Wiz scan $stack"
            if ! wizcli iac scan \
              --path "${stack}/out.tfplan.json" \
              --name "pr-${{ github.event.pull_request.number }}-${stack}" \
              --policy "$WIZ_IAC_POLICY" \
              --format sarif \
              --output "${stack}/wiz-iac.sarif"; then
              FAIL=1
            fi
            echo "::endgroup::"
          done <<< "${{ steps.list.outputs.changed }}"
          exit $FAIL

      - name: Upload SARIF
        if: always() && steps.list.outputs.changed != ''
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: stacks

Field notes:

  • --changed is non-negotiable: scanning the whole monorepo on every PR burns Wiz quota and drowns the signal.
  • Scan the plan JSON, not only the .tf tree, whenever you have remote modules or CI-injected variables.
  • Exact CLI flags (wizcli iac scan vs version variants) evolve: validate with wizcli iac scan --help on a pinned CI image; pin the binary by hash rather than latest in production.
  • The apply job (protected branch / GitHub environment) starts only if this workflow is green — no continue-on-error on the scan.

Terramate script to standardize the scan

To avoid duplicating logic across five workflows, a Terramate script in the repo:

# stacks/networking/prod/stack.tm.hcl (excerpt)
stack {
  name        = "networking-prod"
  description = "VPC, SG, endpoints — prod"
  tags        = ["aws", "prod", "networking"]
}
# root terramate.tm.hcl
scripts {
  wiz-scan-plan {
    description = "plan → JSON → wizcli iac scan"
    jobs = [
      {
        name = "plan"
        commands = [
          ["terraform", "plan", "-out", "out.tfplan", "-lock=false"],
          ["terraform", "show", "-json", "out.tfplan", ">", "out.tfplan.json"],
        ]
      },
      {
        name = "wiz"
        commands = [
          [
            "wizcli", "iac", "scan",
            "--path", "out.tfplan.json",
            "--policy", "${env.WIZ_IAC_POLICY}",
            "--name", "${terramate.stack.path.absolute}",
          ],
        ]
      },
    ]
  }
}

Then in CI: terramate script run --changed wiz-scan-plan (syntax depends on your pinned Terramate Scripts version). The idea matters more than the DSL: one repo command = plan + scan, reusable locally by developers with the same pre-prod service account.

What Wiz should block first

Do not start from a 400-control policy. Enable blocking first on:

  1. Accidental public exposure (SG/NACL 0.0.0.0/0 on sensitive ports, public buckets/objects, unrestricted load balancers).
  2. Dangerous identities (Action / Resource *, overly broad trust policies, missing oidc conditions on CI roles).
  3. Secrets in the clear in the plan (non-sensitive variables leaking into plan JSON — treat CI artefacts as sensitive, short retention).
  4. Unencrypted storage / missing CMK where your policy requires it.

Everything else (full CIS) can stay WARN until remediation capacity catches up.

Runtime loop: close code-to-cloud

After apply, Wiz often attaches the live finding to the Terraform file via state. Conditions:

  • State is reachable by the intended connector/integration (S3 backend + dedicated read rights, or sync via a platform like HCP/Scalr if you use one).
  • Resources are tagged stably (Owner, Stack, ManagedBy=terramate) for human triage when automatic mapping fails.
  • A Critical runtime finding with no ticket linked to the Terramate stack is a process debt, not just a scanner gap.

Without that loop you have a nice CI gate and a CSPM that do not talk — two silos.

Evidence for audit / customer

terramate list --changed

# Example local summary
jq '.runs[0].results | length' out/wiz-summary.json

# In GitHub: required check "Wiz IaC scan" on main
# In Wiz: filter reports name=pr-* + CLI enforcement BLOCK

Typical engagement deliverables: the workflow above, a documented Wiz policy (which controls block), a Terramate tag ↔ cloud account matrix, and five historical findings showing “would have been blocked in CI”.

When not to force Wiz into Terramate

  • You do not have Wiz yet: start with Checkov / Trivy / OPA on the plan — cheaper, already useful. Wiz comes when the runtime graph and code-to-cloud justify the ticket (decision grid).
  • Your monorepo has fewer than ~5 stacks: Terramate is optional; terraform plan + wizcli on the folder is enough.
  • Nobody owns SARIF results: a failing gate without a remediation owner becomes noise. Name an owner per Terramate tag.

Conclusion

Wiz and Terramate complement each other once you accept there is no magic connector: Terramate chooses what to plan, wizcli decides whether the plan may be applied, Wiz CSPM verifies what is running. To scope this pipeline (OIDC, block policy, stack boundaries, link to native Security Hub), a Cloud Security Audit or a supply-chain engagement depending on whether the bottleneck is IaC or runtime.

Sources: Terramate — GitHub Actions preview workflow, Terramate — security & data access, Wiz × HashiCorp / Terraform run tasks, Wiz + HCP Terraform (code-to-cloud).

Found this useful? Share it.

Go further

A topic, a project, a question?

Distribuée supports demanding SMBs on cloud security, CI/CD supply chain and AI security.

Book 15 min