Skip to main content

Catching over-permissive IAM in the pull request, not in production

Why I built Cloud Policy Guard: a small Python CLI that reads a Terraform plan, scores IAM risk, and fails the pipeline before a wildcard policy ever reaches AWS.

Updated 3 min readMarkdown
Catching over-permissive IAM in the pull request, not in production

Over-permissive IAM is one of the most common AWS misconfigurations: "Action": "*", iam:PassRole granted on every resource, a trust policy with "Principal": "*". The problem is usually found during an audit, after the policy has already been live for weeks.

I wanted that check to happen where the change is made: in the pull request. So I built Cloud Policy Guard, a Policy-as-Code security gate for Terraform.

Scan the plan, not the account

The tool reads the JSON form of a Terraform plan, which describes exactly what Terraform is about to create:

terraform plan -out=tfplan
terraform show -json tfplan > plan.json
cloud-policy-guard scan --terraform-plan plan.json

Because it works on the plan file, it runs fully offline and needs no AWS credentials. Nothing has to be deployed, and no secrets have to be handed to the CI job.

The rules

ID Detects Severity
LP001 Wildcard action, Action: "*" Critical
LP002 Wildcard resource, Resource: "*" (context-aware) High / Medium
LP003 Administrative permissions (* on *, or admin managed policies) Critical
LP004 Sensitive IAM actions such as iam:PassRole, iam:PutRolePolicy High
LP005 Service-level wildcards such as s3:*, iam:*, secretsmanager:* High / Medium
LP006 Broad trust policies, Principal: "*" or insecure cross-account trust Critical

The rules are context-aware: they evaluate the combination of actions and resources to set the severity, instead of flagging every wildcard the same way.

Turning findings into a pass/fail decision

Each finding adds to a deterministic risk score: Critical = 10, High = 7, Medium = 4, Low = 1. A pipeline fails if any finding reaches the fail_on severity, or if the total score passes max_risk_score:

# .cpg.yaml
fail_on: critical       # fail if any finding is critical or worse
max_risk_score: 50      # fail if the total risk score exceeds 50
exclude_rules:
  - LP005               # rules can be disabled per repo
format: text            # text for humans, json for pipelines

The exit code does the rest: 0 for PASS, 1 for FAIL. Any CI system (GitHub Actions, GitLab CI, Jenkins) blocks the merge without extra integration work.

In a GitHub Actions workflow

- name: Terraform Plan
  run: |
    terraform init
    terraform plan -out=tfplan
    terraform show -json tfplan > plan.json

- name: Scan IAM least privilege
  # exit code 1 fails the job and blocks the PR
  run: cloud-policy-guard scan --terraform-plan plan.json

Testing it honestly

The repo ships with a deliberately vulnerable Terraform example and a secure one. The vulnerable plan must FAIL and the secure plan must PASS, and a pytest suite keeps the rules from regressing. The tool is also containerised with Docker, so it behaves the same on my laptop and in CI.

What I took away

  • Shifting left only works if the check is fast, offline and gives a clear yes/no.

  • A severity model plus a score threshold lets teams adopt a gate gradually instead of all at once.

  • Two outputs matter: coloured text that developers will actually read, and JSON that pipelines can parse.

No comments yet. Be the first to comment.