CloudBoosterDocs

Gate pull requests in CI

Run the CBX Guard engine on your Terraform plan in GitLab CI or GitHub Actions. A dangerous change fails the pipeline and blocks the merge, with the verdict posted as a PR/MR comment.

Gate pull requests in CI

The CBX Guard gate runs the same deterministic engine that watches your live cloud, but before a change ships — over a Terraform plan in your pipeline. A broad-internet ingress to a sensitive port on a production security group makes the engine exit non-zero, which fails the job and blocks the merge, and the verdict is posted as a pull-request / merge-request comment.

There are two ways to run it:

  • Managed by the Console. When you enable the gate while connecting a repository (or from Getting started), CBX Guard posts the cbxguard/policy check for you — via the GitHub App's webhook, or a merge request that adds the job on GitLab.
  • In your own CI. Add the workflow below yourself. This page is the reference for that job — the same engine, same flags, whichever CI you run.

The default path is credential-free: it plans offline from empty state (plan_only=true + dummy AWS creds), so the gate runs on the very first PR with zero cloud secrets. For a real diff against your deployed baseline, swap in your remote-state backend.

This is one of two lanes. The CI gate reasons about the diff alone, offline. CBX Guard also runs an independent account-aware review on its own servers that resolves the same change against your captured live account — and can reach a sharper verdict on a diff the offline gate passes as clean. See Two lanes.


How the gate decides

Every gate job does the same three things:

  1. Render the plan. terraform planterraform show -json produces plan.json.
  2. Run the engine. The cloudguard CLI evaluates the plan and writes a machine report.
  3. Report + gate. A human report is posted as a PR/MR comment, and the step exits non-zero when the change is gated — that non-zero exit is what fails the pipeline.

The CLI is an unpublished private workspace package, so the job runs it straight from a pinned checkout of the cbx-guard repo with Bun. Pin CBX_GUARD_REF to a commit you've verified and bump it deliberately.

Key flags:

FlagMeaning
--provider terraformThe plan format being evaluated.
--account / --regionThe URN coordinates for the change.
--trusted-cidrYour office/VPN CIDR, so legitimate admin ingress isn't flagged.
--fail-onThe gate floor. watch is recommended: a fresh exposure resolves to watch offline (reachability can't be confirmed from a plan), so high would let it pass.
--suppressionsAdded only when .cbxguard/suppressions.yaml exists (see below).

Conditional --suppressions hardening. The flag is passed only when the suppressions file is present. This keeps the invocation identical across GitLab and GitHub and, crucially, decouples the gate from a suppress-capable CLI: a pinned CBX_GUARD_REF that predates the flag no longer fails with a usage error. Never pass --suppressions unconditionally.


GitLab CI

Include the reusable job and wire it into a guard stage. It clones the pinned cbx-guard via the built-in CI_JOB_TOKEN (the cbx-guard project allowlists the job token for inbound CI, so no long-lived secret is needed).

# .gitlab-ci.yml (excerpt)
stages:
  - guard
 
.cb-guard:
  stage: guard
  image: oven/bun:1.3
  variables:
    # Pinned cbx-guard commit (the verdict engine was verified at this SHA). Bump deliberately.
    CBX_GUARD_REF: "ce95a932bcd3b055732d123ca7346d0e470bf7dc"
    TF_VERSION: "1.9.8"
    # The gate floor. `watch` is recommended (a fresh exposure resolves to `watch` offline).
    GUARD_FAIL_ON: "watch"
    GUARD_ACCOUNT: "$AWS_ACCOUNT_ID"
    GUARD_REGION: "$AWS_REGION"
    GUARD_TRUSTED_CIDR: "$OFFICE_CIDR"
    # Dummy creds so the AWS provider configures without reaching AWS during an offline plan.
    AWS_ACCESS_KEY_ID: "plan-only"
    AWS_SECRET_ACCESS_KEY: "plan-only"
    AWS_DEFAULT_REGION: "$AWS_REGION"
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl unzip git ca-certificates
    - curl -fsSL -o /tmp/tf.zip "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip"
    - unzip -o /tmp/tf.zip -d /usr/local/bin && terraform version
    # Pinned checkout of cbx-guard via the built-in CI job token (no long-lived secret needed).
    - git clone --filter=blob:none
        "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/cloudbooster.io/platform/cbx-guard.git"
        /tmp/cbx-guard
    - git -C /tmp/cbx-guard checkout "$CBX_GUARD_REF"
    - (cd /tmp/cbx-guard && bun install)
  script:
    - cd terraform
    - terraform init -backend=false
    - terraform plan -refresh=false -input=false -var "plan_only=true" -out=plan.bin
    - terraform show -json plan.bin > ../plan.json
    - cd ..
    # Add --suppressions ONLY when the file exists (keeps the invocation portable + CLI-version-safe).
    - |
      SUPPRESS=""
      if [ -f .cbxguard/suppressions.yaml ]; then SUPPRESS="--suppressions .cbxguard/suppressions.yaml"; fi
      bun run /tmp/cbx-guard/apps/cli/src/index.ts check plan.json \
        --provider terraform \
        --account "$GUARD_ACCOUNT" \
        --region "$GUARD_REGION" \
        --trusted-cidr "$GUARD_TRUSTED_CIDR" \
        --fail-on "$GUARD_FAIL_ON" \
        $SUPPRESS \
        --json > guard.json || true
    # Render a human report, post it as an MR comment (if GUARD_REPORT_TOKEN is set), and exit
    # non-zero when the change is gated — that is what fails the pipeline.
    - bun run scripts/guard-report.ts guard.json
  artifacts:
    when: always
    paths:
      - guard-report.md
      - guard.json
      - plan.json
    expire_in: 1 week

Set AWS_ACCOUNT_ID, AWS_REGION, and OFFICE_CIDR as CI/CD variables. GUARD_REPORT_TOKEN (optional) lets the job post the report as an MR comment.

These mirror the canonical reference templates, ci/cb-guard.gitlab-ci.yml and ci/cb-guard.github.yml. The terraform plan line takes whatever -vars your configuration needs — the reference templates pass plan_only=true (offline plan) plus a dummy value for their sample stack's variables. Keep plan_only=true for the credential-free path; add your own -vars as your Terraform requires.


GitHub Actions

The GitHub job is the twin of the GitLab one — same engine, same flags — only the CI plumbing differs. Add it as .github/workflows/cb-guard.yml.

# .github/workflows/cb-guard.yml
name: CBX Guard
 
on:
  pull_request:
 
# The report step posts the verdict as a PR comment with the built-in GITHUB_TOKEN.
permissions:
  contents: read
  pull-requests: write
 
jobs:
  cb-guard:
    runs-on: ubuntu-latest
    env:
      CBX_GUARD_REF: "ce95a932bcd3b055732d123ca7346d0e470bf7dc"
      TF_VERSION: "1.9.8"
      GUARD_FAIL_ON: "watch"
      GUARD_ACCOUNT: ${{ vars.AWS_ACCOUNT_ID }}
      GUARD_REGION: ${{ vars.AWS_REGION }}
      OFFICE_CIDR: ${{ vars.OFFICE_CIDR }}
      AWS_ACCESS_KEY_ID: "plan-only"
      AWS_SECRET_ACCESS_KEY: "plan-only"
      AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
    steps:
      - name: Checkout the repo under test
        uses: actions/checkout@v4
        with:
          fetch-depth: 50 # so the report can diff the PR base for the offending-change block
 
      - name: Set up Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: "1.3"
 
      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
          terraform_wrapper: false
 
      # Cross-repo private checkout has no CI_JOB_TOKEN equivalent, so this one read-only secret is required.
      - name: Fetch the cbx-guard CLI (pinned)
        env:
          CBX_GUARD_TOKEN: ${{ secrets.CBX_GUARD_TOKEN }}
        run: |
          git clone --filter=blob:none \
            "https://gitlab-ci-token:${CBX_GUARD_TOKEN}@gitlab.com/cloudbooster.io/platform/cbx-guard.git" \
            "$RUNNER_TEMP/cbx-guard"
          git -C "$RUNNER_TEMP/cbx-guard" checkout "$CBX_GUARD_REF"
          (cd "$RUNNER_TEMP/cbx-guard" && bun install)
 
      - name: Terraform plan → JSON
        working-directory: terraform
        run: |
          terraform init -backend=false
          terraform plan -refresh=false -input=false -var "plan_only=true" -out=plan.bin
          terraform show -json plan.bin > "$GITHUB_WORKSPACE/plan.json"
 
      # Identical invocation to the GitLab gate; --suppressions is added only when present.
      - name: Run cbx-guard
        run: |
          SUPPRESS=()
          if [ -f .cbxguard/suppressions.yaml ]; then SUPPRESS=(--suppressions .cbxguard/suppressions.yaml); fi
          bun run "$RUNNER_TEMP/cbx-guard/apps/cli/src/index.ts" check plan.json \
            --provider terraform \
            --account "$GUARD_ACCOUNT" \
            --region "$GUARD_REGION" \
            --trusted-cidr "$OFFICE_CIDR" \
            --fail-on "$GUARD_FAIL_ON" \
            "${SUPPRESS[@]}" \
            --json > guard.json || true
 
      # Render the report into the job log, post it as a PR comment, and exit non-zero when gated.
      - name: Report + gate
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: bun run scripts/guard-report-github.ts guard.json
 
      - name: Upload artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cbx-guard
          path: |
            guard-report.md
            guard.json
            plan.json
          retention-days: 7

Configure the repo (Settings → Secrets and variables → Actions):

  • Variables: AWS_ACCOUNT_ID, AWS_REGION, OFFICE_CIDR (your trusted office/VPN CIDR).
  • Secrets: CBX_GUARD_TOKEN — a read-only token for the private cbx-guard repo. GitHub has no cross-repo CI_JOB_TOKEN equivalent, so this one secret is required. The built-in GITHUB_TOKEN authors the PR comment.

Make it block merges

The job failing isn't enough on its own — mark it a required status check:

  • GitHub: Settings → Branches → branch protection rule for your default branch → Require status checks to pass before merging → select CBX Guard.
  • GitLab: the guard job runs on merge requests; require the pipeline to succeed under Settings → Merge requests → Pipelines must succeed.

Suppressing a known finding

To waive a finding you've reviewed, commit .cbxguard/suppressions.yaml to the repo. When the file is present, the job passes it to the engine and the waived finding passes with a warning instead of failing the gate. When it's absent, the flag is omitted entirely (the hardening above).


Next steps

  • Connect a repository — GitHub App or GitLab OAuth/bot, then arm the managed gate.
  • Getting started — connect a repo and a cloud account, enable the gate, read your first verdict.

On this page