Best Practice: Policy-as-Code
Context
Security policies as PDF documents in SharePoint are declarative, not enforceable. They describe the target state but never verify the actual state. Policy-as-Code solves this problem: policies are formulated as versioned, executable code artifacts – and integrated into CI/CD pipelines, deployment gates, and continuous monitoring systems.
The result: compliance is not claimed but proven.
Typical problems without Policy-as-Code:
-
Security reviews take place in manual review rounds – does not scale
-
Configuration drift between compliance audit and the next change
-
Every developer interprets security policies differently
-
Audit preparation means weeks of manual work
-
New resources are not automatically subject to the same security standard
Related Controls
-
WAF-SEC-090 – Policy-as-Code & Compliance Automation
-
WAF-SEC-080 – Security Monitoring & Threat Detection
What Policy-as-Code Means
Policy-as-Code (PaC) is the principle of formulating security and compliance requirements in machine-executable form. The core characteristics:
-
Versioned: Policies are managed in Git – changes are traceable and peer-reviewed like any other code
-
Executable: Policies can be automatically executed against infrastructure, Kubernetes manifests, or Terraform plans
-
Idempotent: The same policy always produces the same result on the same inputs
-
Documented through execution: The policy test is simultaneously the documentation of the requirement
Tooling Comparison
| Tool | Strengths | Weaknesses | Recommended Use |
|---|---|---|---|
wafpass (WAF++) |
Native for WAF++ controls; pillar-based filter; direct YAML control integration; check Terraform plan + live state |
WAF++-specific; no Kubernetes manifests |
Primary tool for all WAF-SEC controls |
OPA / Conftest |
Highly flexible; Rego language for complex rules; Kubernetes + Terraform + Docker; large community; Gatekeeper for K8s admission control |
Rego learning curve; no native WAF++ reference |
Kubernetes admission control; custom business rules |
HashiCorp Sentinel |
Deeply integrated in Terraform Enterprise; policy sets across multiple workspaces; enforcement levels (advisory, soft-mandatory, hard-mandatory) |
Only Terraform Enterprise / HCP Terraform; proprietary |
Terraform Enterprise environments |
AWS Config Rules |
Native AWS integration; managed rules without code; continuous monitoring; Config Aggregator for multi-account |
AWS-only; Lambda overhead for custom rules; no Terraform plan integration |
Continuous live state monitoring; AWS-specific compliance |
Checkov |
Open source; broad Terraform coverage; CI integration; SARIF output |
No WAF++ reference; many false positives; less flexible than OPA |
CI pipeline as supplement to wafpass |
WAF++ wafpass in CI/CD
wafpass is the native policy-as-code tool of the WAF++ framework. It evaluates Terraform plans and state against WAF-SEC (and other pillar) controls.
Basic Usage
# Check all security controls
wafpass check --pillar security
# Check only critical controls (CI gate: no deployment on failure)
wafpass check --pillar security --severity critical --fail-on critical
# Check specific controls
wafpass check --controls WAF-SEC-010,WAF-SEC-020,WAF-SEC-030
# Check Terraform plan (pre-deployment)
terraform plan -out=plan.tfplan
wafpass check --plan plan.tfplan --pillar security
# Output report as JSON
wafpass check --pillar security --output json > security-report.json
# Output report as HTML for management
wafpass check --pillar security --output html > security-report.html
wafpass in GitHub Actions
name: Security Policy Check
on:
pull_request:
paths: ['terraform/**', '*.tf']
push:
branches: [main]
jobs:
wafpass-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: terraform/
- name: Terraform Plan
run: terraform plan -out=plan.tfplan
working-directory: terraform/
env:
AWS_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE }}
- name: wafpass – Security Pillar Check
run: |
wafpass check \
--plan terraform/plan.tfplan \
--pillar security \
--fail-on critical \
--output sarif \
> wafpass-results.sarif
- name: Upload wafpass Results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: wafpass-results.sarif
category: wafpass-security
The --fail-on critical flag ensures that critical control violations
block the deployment process, while High/Medium/Low findings are visible as warnings
in the SARIF report.
OPA for Kubernetes: Gatekeeper
Open Policy Agent (OPA) with Gatekeeper implements a Kubernetes admission controller – every resource is checked against policies before being created:
Installing Gatekeeper
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper/gatekeeper \
--name-template=gatekeeper \
--namespace gatekeeper-system \
--create-namespace \
--set replicas=3 \
--set auditInterval=30
ConstraintTemplate: No Privileged Containers
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snoprivilegedcontainer
spec:
crd:
spec:
names:
kind: K8sNoPrivilegedContainer
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snoprivilegedcontainer
violation[{"msg": msg}] {
c := input.review.object.spec.containers[_]
c.securityContext.privileged == true
msg := sprintf(
"Container '%v' must run as non-privileged (WAF-SEC-120)",
[c.name]
)
}
violation[{"msg": msg}] {
c := input.review.object.spec.initContainers[_]
c.securityContext.privileged == true
msg := sprintf(
"Init container '%v' must run as non-privileged (WAF-SEC-120)",
[c.name]
)
}
# Constraint: activating the template rule for the production namespace
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoPrivilegedContainer
metadata:
name: no-privileged-containers
spec:
enforcementAction: deny # deny blocks; warn only warns
match:
namespaces: ["production", "staging"]
Conftest for Terraform Plans
Conftest uses OPA/Rego policies for Terraform plans:
# policies/security/no_public_s3.rego
package terraform.security
import rego.v1
deny contains msg if {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.acl == "public-read"
msg := sprintf(
"S3 bucket '%v' must not be set to public-read (WAF-SEC-130)",
[resource.address]
)
}
deny contains msg if {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf(
"S3 bucket '%v' must have Block Public ACLs enabled (WAF-SEC-130)",
[resource.address]
)
}
terraform show -json plan.tfplan | conftest test - --policy policies/security/
AWS Config: Continuous Live Monitoring
AWS Config monitors the current state of the infrastructure and reports deviations:
resource "aws_config_configuration_recorder" "main" {
name = "main-recorder"
role_arn = aws_iam_role.config.arn
recording_group {
all_supported = true
include_global_resource_types = true # IAM, CloudFront, etc.
}
}
resource "aws_config_delivery_channel" "main" {
name = "main-channel"
s3_bucket_name = aws_s3_bucket.config_logs.bucket
snapshot_delivery_properties {
delivery_frequency = "TwentyFour_Hours"
}
depends_on = [aws_config_configuration_recorder.main]
}
resource "aws_config_configuration_recorder_status" "main" {
name = aws_config_configuration_recorder.main.name
is_enabled = true
depends_on = [aws_config_delivery_channel.main]
}
# Managed rule: MFA for root account
resource "aws_config_rule" "root_mfa" {
name = "root-account-mfa-enabled"
source {
owner = "AWS"
source_identifier = "ROOT_ACCOUNT_MFA_ENABLED"
}
depends_on = [aws_config_configuration_recorder_status.main]
}
# Managed rule: VPC flow logs enabled
resource "aws_config_rule" "vpc_flow_logs" {
name = "vpc-flow-logs-enabled"
source {
owner = "AWS"
source_identifier = "VPC_FLOW_LOGS_ENABLED"
}
}
# Managed rule: CloudTrail enabled
resource "aws_config_rule" "cloudtrail_enabled" {
name = "cloud-trail-enabled"
source {
owner = "AWS"
source_identifier = "CLOUD_TRAIL_ENABLED"
}
}
# Custom rule via Lambda: WAF-SEC-specific check
resource "aws_config_rule" "custom_encryption_check" {
name = "custom-waf-sec-030-encryption-check"
source {
owner = "CUSTOM_LAMBDA"
source_identifier = aws_lambda_function.config_encryption_check.arn
source_detail {
message_type = "ConfigurationItemChangeNotification"
event_source = "aws.config"
}
}
scope {
compliance_resource_types = ["AWS::RDS::DBInstance", "AWS::S3::Bucket"]
}
}
Policy Versioning
Policies evolve – new requirements emerge, old ones become stricter. Without a versioning strategy, this leads to breaking changes:
Semantic Versioning for Policies
# wafpass-policies/security/WAF-SEC-010.policy.yml
version: "2.1.0"
# 2.x.x = Breaking change (new mandatory assertion)
# x.2.x = Non-breaking enhancement (new optional check)
# x.x.1 = Bugfix (corrected assertion)
effective_date: "2025-01-01"
deprecation_warning: null
breaking_changes_from_previous:
- "Minimum password length increased from 12 to 14 characters (BSI C5:2020 IAM-01 update)"
Graceful Migration: Warn Mode Before Enforce Mode
New policies should initially run in warn mode:
# Phase 1 (month 1): warn only
enforcement_level: warn
# Phase 2 (month 2): non-compliance in reports
enforcement_level: report
# Phase 3 (month 3): CI gate blocks on new deployments
enforcement_level: fail-on-new
# Phase 4 (month 4): CI gate blocks all deployments
enforcement_level: fail
Monitoring and Measurement
-
Config Compliance Dashboard: Percentage of compliant resources per rule
-
Config Aggregator: Multi-account compliance overview for the entire organization
-
Security Hub Findings: Config findings aggregated with other security sources
-
Policy Coverage Metric: Share of resources covered by at least one policy
-
Mean Time to Remediation (MTTR): How long does it take to fix a Config finding?
-
wafpass check --pillar security --controls WAF-SEC-090 --output json | jq '.summary'