← All guides

IaC & misconfiguration scanning

Build & artifacts
On this page

In plain terms

Modern infrastructure is defined in files: Terraform for cloud resources, Kubernetes manifests for workloads, Dockerfiles for images, Helm charts, CloudFormation. Infrastructure-as-Code (IaC) scanning, also called misconfiguration scanning, checks those files for insecure settings before they're applied to a real environment.

The most damaging cloud incidents are rarely exotic exploits; they're misconfigurations, a storage bucket left public, a security group open to the world, a container running as root.

Why it matters

An IaC misconfiguration is a production incident waiting for terraform apply. Because the insecurity lives in a text file, it can be caught in code review, turning "we found an open database in prod" into "we blocked a pull request." This is one of the highest-leverage places to shift security left.

Where it fits

  • Artifact: IaC definitions in your repo, *.tf, Kubernetes YAML, Dockerfile, Helm, etc.
  • Stage: on the pull request that changes infrastructure, before it's provisioned.

How it works

A scanner parses each IaC file into a structured model and evaluates it against a library of policies ("no public S3 buckets," "pods must set runAsNonRoot," "no :latest image tags"). Many tools let you write custom policies (often in Rego, the language of the Open Policy Agent (OPA)) to encode your own organization's rules.

ToolNotes
Trivy (config/misconfig)OSS; scans Terraform, K8s, Dockerfile, Helm
CheckovOSS, from Bridgecrew/Prisma; large built-in policy set
tfsecOSS, Terraform-focused (now folded into Trivy)
KICSOSS, from Checkmarx; broad IaC coverage
kube-benchOSS; CIS benchmarks for live clusters (see posture)

Manifests are not the cluster

Scanning the manifests is not the same as checking the cluster. IaC scanning reads what you declared; a cluster drifts from that the first time someone changes a setting during an incident, and it was never covered for anything applied before the manifests existed.

The complementary check is a posture scan against the running system. The CIS Kubernetes Benchmark is the usual baseline, covering RBAC, service accounts, Pod Security Standards and network policies as they are.

An IaC scan answers about the file, for as long as the file exists A Kubernetes manifest declares that a pod must not run as root, and applying it produces a pod that matches. Later somebody edits the setting directly on the cluster, during an incident, and it is never written back. The manifest is unchanged, so every subsequent scan of the repository reports the declared value and passes. Nothing in that output is wrong; it is an answer about a file rather than about a cluster. ONE SETTING, THREE MOMENTS deployment.yaml runAsNonRoot: true the IaC scan reads this file kubectl apply the running pod runAsNonRoot: true declaration and reality agree somebody changes it on the cluster at 2am, and never writes it back the running pod runAsNonRoot: false and the file still says true Every scan of that repository from here on passes, and nothing in its output is wrong. It is answering a question about a file.
Drift is the distance between two different questions rather than a gap in the tool, and closing it means putting the second question to the running system.

Common pitfalls

  • Defaults are noisy. Out-of-the-box policy packs flag many low-impact issues; tune to what matters for your environment.
  • Scanning files ≠ scanning reality. IaC scanning catches what's declared; drift (manual changes in the console) needs runtime posture checks too.
  • Custom policy is where the value is. The generic rules are table stakes; encoding your own guardrails is what prevents your recurring mistakes.

How Draugr fits. The iac control runs Trivy's misconfiguration scanner over a component's repositories, Terraform and Kubernetes manifests and Dockerfiles and Helm, normalized to SARIF alongside every other control and ranked by what the component declares rather than by the policy pack's own severity.

Trivy evaluates a limited number of module calls in each Terraform module and exits clean without checking the rest. When it skips one, the root module is listed under Caveats as unread, with the calls it left out, as in 32 of 33 module calls checked; "zbad" skipped. With failOnCaveats: [unread-files] in the descriptor's gate, that fails the run.

Your own guardrails are the half worth having. checks takes paths to your Rego, and Draugr reads each check's package line to know what to evaluate, so the rule that encodes the mistake your team actually repeats sits beside the generic ones and gates the same way.

Files answer one question, and the other is a control of its own:

What it readsWhen it runs
iacthe manifests in the repository, at the revision being scannedon the pull request, before anything reaches a cluster
kubernetesthe cluster itself, through the Kubernetes APIon a schedule, against whatever is running now

kubernetes is served by draugr-k8s-policies, native Go against the API rather than a tool Draugr shells out to. Nothing to install, nothing created in your cluster, and a namespaces list narrows it to the part you own where the cluster is shared. Every check in the benchmark's policies section is reported either way: the ones it can settle carry a verdict read from the cluster, and the rest say they need manual review, which is what CIS says about them. The report states how many of each, so a clean result cannot be mistaken for a complete one.

Run both and the difference between their answers is your drift. See CIS benchmarks and posture for what the second one covers, and the control catalog for the scanners behind each.

Keep learning