Skip to main content
โšก Calmops

Policy as Code: Automating Security and Compliance

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

  1. Start with auditing: Use validationFailureAction: Audit before enforcing
  2. Fail fast: Catch issues at admission, not runtime
  3. Clear messages: Policy violation messages should explain how to fix
  4. Test thoroughly: Unit test policies before deployment
  5. Version control: All policies in Git with code review
  6. 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

Comments