QA Engineer Skills 2026QA-2026Policy as Code

Policy as Code

Why Policies Must Be Code

Static analysis tools catch what terraform validate cannot: security misconfigurations, compliance violations, and organizational policy breaches. The critical insight is that policies written in documentation get ignored, while policies written in code get enforced automatically on every commit.

Policy-as-code transforms statements like "all S3 buckets must be encrypted" from a wiki page that developers forget to read into an automated gate that blocks non-compliant infrastructure from reaching production.


Tool Comparison

Tool Language Scope Speed Custom Rules CI Integration
tfsec Go Terraform only Very fast Rego, YAML Native GitHub Action
Checkov Python Terraform, CloudFormation, K8s, Helm, Dockerfiles Fast Python All major CI systems
Trivy (config mode) Go Terraform, CloudFormation, K8s, Dockerfiles Very fast Rego GitHub Action, GitLab template
OPA/Conftest Rego Any structured data (JSON, YAML, HCL) Fast Rego (native) Any CI via CLI
Bridgecrew SaaS + Python Multi-framework Fast Python (extends Checkov) Full platform + CLI
Snyk IaC SaaS + CLI Terraform, CloudFormation, K8s, ARM Moderate UI-based All major CI systems

The choice between these tools depends on your stack, team skills, and compliance requirements. Many teams use multiple tools in combination -- for example, Checkov for broad coverage plus OPA for custom organizational policies.


OPA/Rego: The Universal Policy Engine

Open Policy Agent (OPA) evaluates structured data against Rego policies. This is the most flexible approach because it works with any infrastructure format -- Terraform plans, Kubernetes manifests, Docker Compose files, or any JSON/YAML configuration.

Writing Rego Policies

# policy/terraform/s3.rego
package terraform.s3

# Deny S3 buckets without encryption
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    not has_encryption(resource)
    msg := sprintf("S3 bucket '%s' must have server-side encryption enabled", [resource.address])
}

has_encryption(resource) {
    resource.change.after.server_side_encryption_configuration[_].rule[_].apply_server_side_encryption_by_default[_].sse_algorithm
}

# Deny public ACLs
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    acl := resource.change.after.acl
    acl != "private"
    msg := sprintf("S3 bucket '%s' has ACL '%s' -- must be 'private'", [resource.address, acl])
}

# Deny buckets without versioning
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    not has_versioning(resource)
    msg := sprintf("S3 bucket '%s' must have versioning enabled", [resource.address])
}

has_versioning(resource) {
    resource.change.after.versioning[_].enabled == true
}

Running OPA with Conftest

Conftest is the CLI tool that runs OPA policies against configuration files:

# Run policy against a Terraform plan
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy policy/terraform/

# Run against Kubernetes manifests
conftest test k8s/deployment.yaml --policy policy/kubernetes/

# Run against Dockerfiles
conftest test Dockerfile --policy policy/docker/

# Run with multiple policy directories
conftest test tfplan.json \
  --policy policy/security/ \
  --policy policy/compliance/ \
  --policy policy/cost/

Organizing Rego Policies by Concern

A well-organized policy repository separates concerns:

policy/
  terraform/
    s3.rego          # S3-specific rules
    rds.rego         # RDS-specific rules
    iam.rego         # IAM policy rules
    networking.rego  # VPC, security group rules
    tagging.rego     # Resource tagging requirements
  kubernetes/
    security.rego    # Pod security, RBAC
    resources.rego   # CPU/memory limits
    networking.rego  # Network policies
  docker/
    best_practices.rego  # Dockerfile linting
  common/
    helpers.rego     # Shared helper functions

Advanced Rego Patterns

# policy/terraform/iam.rego
package terraform.iam

# Deny IAM policies with wildcard actions
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_iam_policy"
    policy_doc := json.unmarshal(resource.change.after.policy)
    statement := policy_doc.Statement[_]
    statement.Effect == "Allow"
    statement.Action[_] == "*"
    msg := sprintf("IAM policy '%s' grants wildcard actions (*). Use specific actions.",
        [resource.address])
}

# Deny IAM policies with wildcard resources on mutating actions
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_iam_policy"
    policy_doc := json.unmarshal(resource.change.after.policy)
    statement := policy_doc.Statement[_]
    statement.Effect == "Allow"
    statement.Resource[_] == "*"
    action := statement.Action[_]
    not is_readonly_action(action)
    msg := sprintf("IAM policy '%s' uses wildcard resource with mutating action '%s'",
        [resource.address, action])
}

is_readonly_action(action) {
    endswith(action, ":Get*")
}
is_readonly_action(action) {
    endswith(action, ":List*")
}
is_readonly_action(action) {
    endswith(action, ":Describe*")
}

Checkov: Batteries Included

Checkov ships with 1000+ built-in rules and requires zero configuration to start catching issues. It is particularly strong for teams that want immediate value without writing custom policies.

Quick Start

# Scan a Terraform directory
checkov -d ./terraform/ --framework terraform

# Output example:
# Passed checks: 42, Failed checks: 3, Skipped: 0
#
# Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
#   FAILED for resource: aws_s3_bucket.data
#   File: /main.tf:15-22
#
# Check: CKV_AWS_145: "Ensure S3 bucket is encrypted with KMS"
#   FAILED for resource: aws_s3_bucket.data
#   File: /main.tf:15-22

# Run only specific checks
checkov -d ./terraform/ --check CKV_AWS_18,CKV_AWS_145

# Skip specific checks (with justification)
checkov -d ./terraform/ --skip-check CKV_AWS_18 \
  --skip-check-reason "Access logging handled by CloudTrail"

# Output as JSON for CI parsing
checkov -d ./terraform/ --output json > checkov-results.json

# Scan multiple frameworks at once
checkov -d . --framework terraform,kubernetes,dockerfile

Custom Checkov Policies in Python

When built-in checks are not enough, write custom policies in Python:

# custom_checks/s3_naming_convention.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories

class S3NamingConvention(BaseResourceCheck):
    """Ensure S3 bucket names follow organizational naming convention."""

    def __init__(self):
        name = "Ensure S3 bucket follows naming convention: {team}-{env}-{purpose}"
        id = "CUSTOM_S3_001"
        supported_resources = ["aws_s3_bucket"]
        categories = [CheckCategories.CONVENTION]
        super().__init__(name=name, id=id, categories=categories,
                        supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        bucket_name = conf.get("bucket", [""])[0]
        # Pattern: team-environment-purpose
        parts = bucket_name.split("-")
        if len(parts) < 3:
            return CheckResult.FAILED
        valid_envs = ["dev", "staging", "prod", "test"]
        if parts[1] not in valid_envs:
            return CheckResult.FAILED
        return CheckResult.PASSED

check = S3NamingConvention()

Pre-Commit Integration

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/bridgecrewio/checkov
    rev: '3.2.0'
    hooks:
      - id: checkov
        args: ['--compact', '--framework', 'terraform']

Combining Tools in a CI Pipeline

The strongest approach uses multiple tools, each catching different classes of issues:

# .github/workflows/iac-policy.yml
name: IaC Policy Checks
on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'k8s/**'

jobs:
  policy-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: tfsec (fast, Terraform-specific)
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          working_directory: ./terraform/
          soft_fail: false

      - name: Checkov (broad coverage, multi-framework)
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform,kubernetes,dockerfile
          output_format: cli,json
          output_file_path: console,checkov-results.json

      - name: OPA/Conftest (custom organizational policies)
        run: |
          terraform -chdir=terraform plan -out=tfplan
          terraform -chdir=terraform show -json tfplan > tfplan.json
          conftest test tfplan.json --policy policy/ --output json > opa-results.json

      - name: Trivy config scan (Dockerfiles + K8s)
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          severity: 'HIGH,CRITICAL'

Choosing Your Combination

Team Profile Recommended Stack Rationale
Small startup, Terraform only tfsec + Checkov Fast setup, broad coverage, no custom policies needed
Enterprise, multi-cloud Checkov + OPA/Conftest Checkov for broad checks, OPA for custom compliance
Security-focused tfsec + OPA + Snyk IaC Defense in depth, multiple scanning engines
Kubernetes-heavy Checkov + Polaris + OPA K8s-specific + custom policies

The key principle: static policy checks should run in under 60 seconds and block merges for critical violations. They are your most cost-effective quality gate.