Somewhere between “looks fine in the PR” and “why does this bucket have no block-public-access setting,” there’s a gap — between what a Terraform plan says it will do and what anyone actually checked before it did it. Close that gap early enough, and a misconfiguration is a one-line diff comment. Close it late, and it’s an incident channel at 2 a.m.
The 2 a.m. version of this story
Priya, a platform engineer, merges a Terraform change that stands up a new service: a security group, an EC2 instance, an S3 bucket, an IAM role for a Lambda. The plan applies cleanly. CI is green. Everyone moves on.
Three weeks later, a routine cloud security audit flags the security group: ingress open on all ports, all protocols, from 0.0.0.0/0. The S3 bucket has no public-access block. The IAM role’s inline policy grants “Resource”: “*”. None of it was malicious — it was scaffolding meant to be tightened “later.” Later never got scheduled, because nothing forced it onto anyone’s calendar until an auditor did.
That’s the normal failure mode for infrastructure-as-code security: the tooling that would catch the problem runs after the resources are already live. By then, fixing it means a change ticket and a maintenance window, not a code review comment.
Introducing Nirmata Control: Policy Checks Before the Apply, Not After
terraform plan is a complete, structured description of every resource about to be created — before any of it happens. Rendered as JSON, it’s just data. Nothing about it requires a live AWS account to check for policy violations.
That’s exactly what Nirmata Control does. Nirmata Control — its CLI is nctl — is Nirmata’s policy-as-code engine for shift-left security. It reads a Terraform plan and evaluates it against your organization’s policies (built on Kyverno, the same engine Nirmata uses for Kubernetes admission control) before a single cloud API call is made. The advantage over a traditional cloud security scanner: there’s nothing to remediate after the fact, because nothing was created yet. A violation is a failed CI check on a pull request, not a ticket against a live resource.
terraform plan -out=plan.tfplan terraform show -json plan.tfplan > plan.json nctl scan terraform -p ./policies/aws-security -r plan.json
Priya’s change, run through a CIS-flavored policy set, comes back like this — before the PR merges:
Policy Result Message
sg-no-unrestricted-ingress-ports fail allows all ports from 0.0.0.0/0
s3-block-public-access fail block_public_acls must be true
iam-no-wildcard-resource fail wildcard (*) in Resource
lambda-require-vpc fail not deployed in a VPC
Same four problems the audit would have found — eighteen minutes into a pull request instead of eighteen days into production.
(Running Terraform Cloud specifically? The same policy engine is also available as a native Run Task integration. Nirmata Control is the CLI-first path that works with any CI system.)
Making it a gate, not a suggestion
A scan someone has to remember to run is a scan that eventually doesn’t get run. What actually shifts the check left is wiring it into a required CI job — GitHub Actions, Atlantis, whatever already gates merges — that fails the check on any violation. The policies live in the same repository as the infrastructure they govern, reviewed the same way the Terraform itself is reviewed.
That’s most of the story — until the gate catches something that isn’t actually a mistake.
The Problem: A Gate That’s Right, But Shouldn’t Block Everything
Say a team is mid-migration on a fleet of security groups. Two of them — legacy, already scheduled for decommission — allow all protocols on all ports. A third, brand-new one, has exactly the same shape because someone copy-pasted a module without thinking about it. The policy is right about all three. But blocking the PR that merely re-applies the two known, tracked legacy groups doesn’t make anyone safer — it just trains the team to stop trusting the gate.
The reflexive fix — disable the policy, or allowlist the whole resource type — is the actual regression. It doesn’t except those two groups. It accepts every security group, forever, including the one shipped next month with no migration ticket behind it.
The Solution: Exceptions Scoped to One Resource, Not the Whole Policy
Nirmata Control supports a narrower answer: a PolicyException — a YAML resource, checked into the repo, reviewed like any other change — that names exactly the resource it covers and nothing else. Instead of turning a rule off, the rule itself checks a list of explicitly approved exceptions and lets only those through:
# the policy itself consults the exception list directly
– expression: >-
# the policy itself consults the exception list directly
- expression: >-
securityGroups.all(sg, sg.address in exceptions.allowedValues ||
sg.values.ingress.all(rule, rule.protocol != '-1'))
# the exception names exactly the resources it covers
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
name: legacy-sg-migration-q3
spec:
policyRefs:
- name: sg-require-protocol-specified
kind: ValidatingPolicy
allowedValues:
- "aws_security_group.legacy_app"
- "aws_security_group.legacy_batch"
Run the scan with that exception attached: the two migration-tracked groups pass, the copy-pasted new one still fails — by name, in the message. Nobody touched the policy, and nobody accidentally protected next quarter’s mistake along with this quarter’s known exception.
Why This Is Safer Than the Alternatives
The mechanism above is almost incidental — what actually makes it safer is where the exception lives and how visible it is:
- Reviewable — granting one is a pull request, scrutinized the same way the original change was.
- Specific — the exception names a resource address, not a resource type. It can’t silently widen.
- Discoverable — one command finds every active exception across the org, reason included.
- Revocable — deleting the file is the whole rollback.
Compare that to the usual alternative: someone with admin rights clicks “ignore,” or grants a standing waiver nobody put an expiry on. That works fine right up until nobody remembers why it exists.
What Your Team Gains
Put together, this is what changes for a platform or security team:
- Faster remediation — misconfigurations surface as a PR comment minutes after a commit, not weeks later in an audit finding.
- Fewer audit surprises — the same policies that gate merges are the ones an auditor would check, so there’s nothing left to discover after the fact.
- Developer velocity without a bypass — teams keep shipping through CI; only genuine violations stop a merge, and a legitimate exception doesn’t require disabling a control for everyone else.
- A defensible audit trail — every exception is a dated, justified, reviewed pull request, not a verbal agreement or a dashboard toggle nobody remembers.
Worth putting to your team
- Does anything stop a Terraform plan from merging today if it opens a security group to the world?
- When someone needs a legitimate exception, is it a reviewed, dated, revocable file — or a policy quietly switched off?
- Could you list every active security exception across your infrastructure in one command right now?
If the honest answer to any of these is “not really,” that’s the gap Nirmata Control closes: a CI-enforced policy gate, and exceptions that are as narrow and auditable as the policies they carve out of.
Want help wiring this into your own Terraform pipeline? Reach out at sales@nirmata.com.
— The Nirmata Team

