Manual policy enforcement doesn’t scale. As infrastructure grows, ensuring security, compliance, and best practices becomes impossible without automation. Policy as Code (PaC) brings version control, testing, and automation to policy management.
Understanding Policy as Code
Policy as Code is the practice of defining policies as machine-readable code that can be versioned, tested, and automatically enforced. Benefits include:
- Consistency: Same policies everywhere
- Auditability: Full history of policy changes
- Automation: No manual enforcement needed
- Speed: Policies apply in milliseconds
- Scale: Works across thousands of resources
Open Policy Agent (OPA)
OPA is the industry-standard policy engine, now part of the Cloud Native Computing Foundation.
Rego Policy Language
# Example: Require labels on all resources
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Deployment"
not input.request.object.metadata.labels.app
msg = "Deployment must have an 'app' label"
}
# More complex: Require resources in allowed namespaces
allowed_namespaces = {"production", "staging", "development"}
deny[msg] {
input.request.kind.kind == "Pod"
namespace := input.request.object.metadata.namespace
not allowed_namespaces[namespace]
msg = sprintf("Pod must be in allowed namespace, got: %s", [namespace])
}
# Require resource limits
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits
msg = sprintf("Container %s must have resource limits", [container.name])
}
OPA Gatekeeper Installation
# Install Gatekeeper
apiVersion: v1
kind: Namespace
metadata:
name: gatekeeper-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: constrainttemplates.templates.gatekeeper.sh
spec:
group: templates.gatekeeper.sh
names:
kind: ConstraintTemplate
plural: constrainttemplates
scope: Cluster
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
enforcementAction:
type: string
targets:
type: array
items:
type: object
# ... (full CRD definition)
Constraint Templates
# Require labels constraint template
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.kubeaudit.com
rego: |
package k8srequiredlabels
deny[msg] {
provided := {label | label := input.request.object.metadata.labels[_]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
# Apply constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-and-owner
spec:
match:
kinds:
- apiGroups: ["*"]
kinds: ["*"]
parameters:
labels:
- "app"
- "owner"
- "environment"
Kubernetes API Validation
# Validate Ingress hostname
package kubernetes.ingress
deny[msg] {
input.request.kind.kind == "Ingress"
host := input.request.object.spec.rules[_].host
not ends_with(host, ".example.com")
not host == "example.com"
msg = sprintf("Ingress host must be in example.com domain: %s", [host])
}
# Validate service type
deny[msg] {
input.request.kind.kind == "Service"
input.request.object.spec.type == "LoadBalancer"
input.request.namespace == "production"
not input.request.object.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-type"] == "nlb"
msg = "Production LoadBalancer must be Network Load Balancer"
}
Kyverno
Kyverno is a Kubernetes-native policy engine that uses YAML for policies.
Kyverno Policies
# Require specific labels
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-for-app-label
match:
resources:
kinds:
- Deployment
- StatefulSet
- DaemonSet
validate:
message: "Label 'app' is required"
pattern:
metadata:
labels:
app: "?*"
# Require resource limits
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resources
spec:
validationFailureAction: Enforce
background: true
rules:
- name: validate-resources
match:
resources:
kinds:
- Pod
validate:
message: "CPU and memory limits are required"
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
Generate Resources
# Auto-generate ConfigMap for namespaces
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: namespace-config
spec:
rules:
- name: generate-namespace-config
match:
resources:
kinds:
- Namespace
generate:
kind: ConfigMap
name: "{{ request.object.metadata.name }}-config"
namespace: "{{ request.object.metadata.name }}"
data:
data:
log-level: "info"
retention-days: "30"
Mutate on Create
# Add default annotations
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: add-default-annotations
spec:
rules:
- name: add-annotations
match:
resources:
kinds:
- Deployment
mutate:
overlay:
metadata:
annotations:
kubernetes.io/change-cause: "{{ request.object.metadata.name }} was created"
monitoring.grafana.io/dashboard: "enabled"
Infrastructure Policy Examples
Terraform Policy Enforcement
# conftest policy check
import "tfplan/v2" as tfplan
# Require tags on all resources
allowed_tags = ["Environment", "Owner", "CostCenter", "Project"]
violation[resource] {
resource := tfplan.resource_changes[_]
resource.change.after.tags
missing_tags := [tag | tag := allowed_tags[_]; not resource.change.after.tags[tag]]
count(missing_tags) > 0
}
# Deny public S3 buckets
deny[msg] {
resource := tfplan.resource_changes[_]
resource.type == "aws_s3_bucket"
resource.change.after.acl == "public-read-write"
msg = sprintf("S3 bucket %s cannot be public", [resource.address])
}
# Run Conftest
conftest test terraform/plans -p policy/
AWS Config Rules
# CloudFormation template for Config Rule
AWSTemplateFormatVersion: '2010-09-09'
Resources:
S3BucketPublicReadProhibited:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: s3-bucket-public-read-prohibited
Description: "Checks that S3 buckets do not allow public read access"
Scope:
ComplianceResourceTypes:
- AWS::S3::Bucket
Source:
Owner: AWS
SourceIdentifier: S3_BUCKET_PUBLIC_READ_PROHIBITED
RequiredTags:
Type: AWS::Config::ConfigRule
Properties:
ConfigRuleName: required-tags
Description: "Checks that required tags are present"
InputParameters:
tag1Key: "Environment"
tag2Key: "Owner"
Scope:
ComplianceResourceTypes:
- AWS::EC2::Instance
- AWS::S3::Bucket
Source:
Owner: CUSTOM_LAMBDA
SourceIdentifier: !GetAtt ConfigFunction.Arn
CloudGuard and Prisma Cloud
# Prisma Cloud policy YAML
- name: AWS S3 bucket should block public access
cloudguardId: "AQAAAAA"
severity: high
category: storage
description: Checks that S3 buckets block public access
remediation: "Enable block public access in S3 bucket settings"
cloudTypes:
- aws
resourceTypes:
- s3
open: true
- name: RDS should have encryption enabled
cloudguardId: "AQAAAAA"
severity: high
category: encryption
description: Checks that RDS instances have encryption enabled
remediation: "Enable encryption at rest in RDS instance settings"
cloudTypes:
- aws
resourceTypes:
- rds
CI/CD Integration
Policy in GitHub Actions
name: Policy Check
on:
pull_request:
paths:
- '*.yaml'
- '*.yml'
- 'terraform/**'
- 'kustomization.yaml'
jobs:
conftest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Conftest
run: |
wget https://github.com/open-policy-agent/conftest/releases/download/v0.55.0/conftest_0.55.0_Linux_x86_64.tar.gz
tar -xzf conftest_0.55.0_Linux_x86_64.tar.gz
sudo mv conftest /usr/local/bin/
- name: Run Conftest
run: |
conftest test manifests/ -p policy/
kyverno:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Kyverno
run: |
kubectl apply -f https://github.com/kyverno/kyverno/releases/download/v1.12.0/install.yaml
- name: Apply Policies
run: |
kubectl apply -f policies/
- name: Check Policy Report
run: |
kubectl get polr -o json | jq -r '.items[] | select(.spec.summary.fail > 0) | .spec.summary'
Admission Controller Testing
#!/bin/bash
# Test admission controllers locally
# Install OPA Gatekeeper
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.15.0/deploy/gatekeeper.yaml
# Test policy with dry-run
kubectl apply -f deployment.yaml --dry-run=server
# Check policy violations
kubectl get constraint
# Test with OPA CLI locally
opa eval --format=pretty \
--data policy/kubernetes/admission.rego \
--input deployment.json \
"data.kubernetes.admission.deny"
Enterprise Policy Patterns
Policy Hierarchy
Organization Policies (Global)
โ
Platform Policies (Team/Project)
โ
Application Policies (Service-specific)
# Base policy - applies to all
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: org-base-policy
spec:
# Organization-wide rules
---
# Team policy - inherits from base
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: team-backend-policy
spec:
# Team-specific rules
Policy Exceptions
# Kyverno policy with exceptions
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
rules:
- name: check-labels
match:
resources:
kinds:
- Deployment
validate:
message: "Label 'app' is required"
pattern:
metadata:
labels:
app: "?*"
---
apiVersion: kyverno.io/v1
kind: PolicyException
metadata:
name: emergency-patch-exception
spec:
exceptions:
- policyName: require-labels
ruleNames:
- check-labels
matchedResources:
- kind: Deployment
name: "emergency-frontend"
authorizer: [email protected]
conditions:
- key: "{{ request.operation }}"
operator: Equal
value: UPDATE
Monitoring Policy Compliance
# Prometheus metrics from Gatekeeper
apiVersion: v1
kind: Service
metadata:
name: gatekeeper-metrics
namespace: gatekeeper-system
spec:
selector:
control-plane: controller-manager
ports:
- port: 8888
targetPort: 8888
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: gatekeeper
namespace: monitoring
spec:
selector:
matchLabels:
app: gatekeeper
endpoints:
- port: metrics
---
# Alert for policy violations
- name: policy-alerts
rules:
- alert: PolicyViolationsHigh
expr: gatekeeper_constraints_violations > 100
for: 1h
labels:
severity: warning
annotations:
summary: "High number of policy violations"
Best Practices
Policy Design Principles
- Start with auditing: Use
validationFailureAction: Auditbefore enforcing - Fail fast: Catch issues at admission, not runtime
- Clear messages: Policy violation messages should explain how to fix
- Test thoroughly: Unit test policies before deployment
- Version control: All policies in Git with code review
- Monitor effectiveness: Track policy violation trends
Policy Testing
# Test OPA policies with pytest
import pytest
import json
def test_deny_public_s3():
# Load policy
policy = load_policy("policy/s3.rego")
# Create test input
input_data = {
"resource": {
"type": "aws_s3_bucket",
"acl": "public-read-write"
}
}
# Evaluate
result = evaluate(policy, input_data)
# Assert violation
assert len(result) > 0
assert "public" in result[0].lower()
def test_allow_private_s3():
policy = load_policy("policy/s3.rego")
input_data = {
"resource": {
"type": "aws_s3_bucket",
"acl": "private"
}
}
result = evaluate(policy, input_data)
assert len(result) == 0
Common Policy Examples
Security Policies
# Require running as non-root
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-root-user
spec:
validationFailureAction: Enforce
rules:
- name: deny-root-user
match:
resources:
kinds:
- Pod
validate:
message: "Containers must not run as root"
pattern:
spec:
=(securityContext):
=(runAsNonRoot): "true"
# Require secret encryption
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-secret-encryption
spec:
validationFailureAction: Audit
rules:
- name: check-secrets-encrypted
match:
resources:
kinds:
- Secret
validate:
message: "Secrets must be encrypted"
pattern:
metadata:
annotations:
encryption: "encrypted"
Cost Policies
# Deny expensive instance types
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-expensive-instances
spec:
validationFailureAction: Enforce
rules:
- name: deny-expensive
match:
resources:
kinds:
- Pod
validate:
message: "Cannot use expensive instance types"
deny:
conditions:
- key: "{{ request.object.spec.nodeSelector.instance-type }}"
operator: In
value:
- "p3.2xlarge"
- "m5.16xlarge"
- "r5.24xlarge"
Conclusion
Policy as Code is essential for cloud-native security and compliance:
- Start with OPA/Gatekeeper or Kyverno for Kubernetes
- Use pre-built policy libraries and customize
- Move from Audit to Enforce gradually
- Integrate policies into CI/CD pipelines
- Monitor policy effectiveness continuously
- Test policies thoroughly before deployment
The key is starting simple - require labels, enforce resource limits, block dangerous configurations - then expand from there.
External Resources
Related Articles
- Container Security - Image scanning and security
- Secrets Management - Secret handling
- Zero Trust Security - Zero trust implementation
Comments