Skip to content

CI gates: build, IaC and registry

Three commands gate a pipeline before something is published or deployed. Each writes a JSON receipt, prints a verdict and exits 0 (passed), 1 (blocked) or 2 (incomplete or error). Treat 2 as a failure, never as a pass.

None of them run your workload, start a container, call a cloud API on your behalf or upload anything. Sending the receipt to the console is a separate, authenticated step.

Build scan

Run untrust buildscan after the artifact is built:

untrust buildscan \
  --artifact dist/rootfs.tar --target container \
  --commit "$GIT_SHA" --run-id "$RUN_ID" \
  --fail-on high \
  --output dist/build-report.json --sarif dist/build-report.sarif
--target --artifact
container, gcp, nvidia An uncompressed, flattened root-filesystem tar, for example from docker export of a stopped container. Not a docker save archive or an OCI layout.
nitro The enclave image file (EIF) from nitro-cli build-enclave.
vm-snp, vm-tdx A tar of the VM's boot artifacts plus a manifest.json ("schema": "untrust.vm-bundle/v1") naming the firmware and, optionally, kernel, initrd, command line, root filesystem and the expected launch measurement.

To produce a root-filesystem tar from an image your build already created:

container_id=$(docker create "$BUILT_IMAGE")
docker export --output dist/rootfs.tar "$container_id"
docker rm -v "$container_id"

What it checks: source and configuration in the artifact (Python, shell, Ruby, JavaScript and Go files, service units, Dockerfiles, sensitive file names) against the build rules. For VM bundles it also checks the firmware, the measured kernel command line, root-filesystem integrity, baked-in SSH keys and passwords, and computes the launch measurement. It does not verify signatures, run the workload, scan dependencies for CVEs or inspect model weights.

Useful options:

  • --fail-on sets the lowest severity that blocks (critical, high (default), medium, low, info).
  • --image <registry>/<repo>@sha256:<digest> records the image digest your registry reported, so the console can join this build to the registry artifact.
  • --custom-rules rules.json tests draft custom rules locally. See Custom rules.

Commit and run labels are supplied by the caller. They are labels, not proof of what was scanned.

Limits

Source files up to 8 MB, selected content and decompressed ramdisks up to 512 MB, 50,000 entries, EIF input up to 1 GB, root-filesystem tar up to 8 GB, and a 120-second analysis deadline by default (--timeout, at most 600). Anything over a limit returns ERROR; nothing is silently treated as clean.

IaC scan

Run untrust iacscan on the infrastructure plan before it is applied:

terraform show -json plan.tfplan > plan.json
untrust iacscan --plan plan.json \
  --commit "$GIT_SHA" --run-id "$RUN_ID" \
  --fail-on high --output iac-report.json

The format is detected automatically: Terraform plan JSON, CloudFormation, Azure ARM, Azure Bicep, GCP Deployment Manager or a Kubernetes manifest. The input must be a regular UTF-8 file of at most 2 MB. --source-dir adds a best-effort file:line to each finding by searching your configuration directory as text. Nothing is executed.

A Bicep parameter or other expression is not evaluated, so a rule that depends on one is not assessed. Resources that a rule cannot resolve from the plan (for example a security group supplied by a module output) read SKIP, never PASS, and are left to the live scan.

The rule list is in the IaC rule reference.

Registry scan

untrust registry scan resolves each artifact to its digest and verifies it against a trust policy: signed by a trusted identity, deployed by digest rather than a mutable tag, and, for enclave images, the measurement your key policy expects.

# With a local trust policy file
untrust registry scan --policy registry-policy.json --output registry.json

# Or fetch the policy you keep in the console, using the CI identity
untrust registry scan --policy-url https://app.untrustsecurity.com/v1/ci-policy/<org>/<binding> \
  --github-oidc --output registry.json

# Or inspect one image with keyless signing expectations
untrust registry scan --image <registry>/<repo>@sha256:<digest> \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity <workflow-identity> --json

OCI signature verification uses the cosign binary (on PATH, or UNTRUST_COSIGN). Then:

  • untrust registry pins --registry registry.json --format kms prints a Nitro KMS key policy condition that admits only the verified PCR0 values; --format wif prints the equivalent Confidential Space WIF condition on image_digest.
  • untrust registry bind --registry registry.json --runtime report.json checks that the measurements a deployment runs belong to verified artifacts (REG-BIND-01).

Pull-request feedback

untrust ci-feedback posts a gate result back to the pull request. It is off unless you add it.

Provider What is posted Permission
GitHub A Check Run with a summary and annotations on the PR head checks: write
GitHub SARIF to code scanning security-events: write
GitLab One merge-request note, edited on re-runs, and a commit status A project access token with api scope in UNTRUST_GITLAB_TOKEN
untrust ci-feedback --kind build --report dist/build-report.json \
  --scanner-exit "$GATE_EXIT" --summary-file "$GITHUB_STEP_SUMMARY" --exit-with-verdict

--mode regression fails the job only on blocking findings that are new or worse than --base-report, the base branch's last report. ERROR, armed custom-rule blocks and an unusable base are never relaxed: they fall back to the full gate. Take the base report only from trusted runs on the base branch, never from an artifact a pull request can write, and never run this under pull_request_target.

--rule-settings applies your organization's rule levels (the JSON from GET /v1/organizations/<org>/rule-overrides) so the pull-request check agrees with what the console will compute. Load that file from the API at run time, not from the repository being gated. --dry-run prints exactly what would be sent.

Tokens are read only from the job environment, sent only in a request header and never printed. Redirects are refused.

Rule levels

Administrators can set each built-in build or IaC rule to block, warn, report or off, change its severity, and pick a profile: standard (default), audit (every non-floor rule reports only) or strict (blocks at medium and above). Every per-rule setting needs a reason and an expiry of at most 365 days. Findings are never dropped: a level changes the gate verdict only, and the receipt records the verdict before and after levels were applied. Floor rules, the attestation-critical ones, always block.

  1. Build the artifact.
  2. Run untrust buildscan (and untrust iacscan on the plan).
  3. Keep the JSON receipts as job artifacts, even when the gate fails.
  4. Send the receipts to the console with the pipeline's OIDC identity.
  5. Publish or deploy only when the gate job succeeded. Do not add continue-on-error to the gate.

The console's pipeline setup generates GitHub Actions and GitLab CI configuration for each target, artifact path and threshold. Download it and merge it into your pipeline. For GitLab step by step, and for Jenkins, Buildkite and other systems, see CI systems.