Skip to content
alyn
BagelHole

Policy As Code skill

Implement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.

$npx skills add BagelHole/DevOps-Security-Agent-Skills --skill policy-as-code
GitHub stars
1,198
Pack installs
62k
Alyn Score
52/99
Updated
6 months ago
License
MIT

BagelHole

Policy As Code

Install Policy As Code

Pick your agent. Every command is ready to paste.

$npx skills add BagelHole/DevOps-Security-Agent-Skills --skill policy-as-code

The open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.

01

Install it

Run the command above, or upload the folder in Claude.

02

Ask naturally

Describe the task. Your agent loads policy-as-code when the request matches its description.

03

Name it if needed

If it does not trigger, say “use the policy-as-code skill”. In Codex, type $policy-as-code.

The skill itself

SKILL.md

GitHub

Policy as Code

Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production.

When to Use

  • Enforcing security and compliance policies on infrastructure-as-code changes
  • Preventing misconfigured Kubernetes workloads from deploying
  • Automating guardrails in CI/CD pipelines for Terraform, CloudFormation, or Helm
  • Implementing organizational standards that must be consistently applied
  • Replacing manual approval gates with automated policy checks

Open Policy Agent (OPA) Rego Policies

# deny_public_s3.rego - Deny S3 buckets with public access
package terraform.aws.s3

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 '%s' has public-read ACL. All buckets must be private. [Policy: no-public-s3]",
        [resource.address]
    )
}

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    resource.change.after.acl == "public-read-write"
    msg := sprintf(
        "S3 bucket '%s' has public-read-write ACL. This is strictly prohibited. [Policy: no-public-s3]",
        [resource.address]
    )
}
# require_encryption.rego - Require encryption on data stores
package terraform.aws.encryption

import rego.v1

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_db_instance"
    not resource.change.after.storage_encrypted
    msg := sprintf(
        "RDS instance '%s' does not have storage encryption enabled. [Policy: require-rds-encryption]",
        [resource.address]
    )
}

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_ebs_volume"
    not resource.change.after.encrypted
    msg := sprintf(
        "EBS volume '%s' is not encrypted. [Policy: require-ebs-encryption]",
        [resource.address]
    )
}

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    not has_encryption(resource)
    msg := sprintf(
        "S3 bucket '%s' does not have default encryption configured. [Policy: require-s3-encryption]",
        [resource.address]
    )
}

has_encryption(resource) if {
    resource.change.after.server_side_encryption_configuration[_]
}
# require_tags.rego - Enforce mandatory tagging
package terraform.aws.tags

import rego.v1

required_tags := {"Environment", "Owner", "CostCenter", "DataClassification"}

deny contains msg if {
    resource := input.resource_changes[_]
    tags := object.get(resource.change.after, "tags", {})
    missing := required_tags - {key | tags[key]}
    count(missing) > 0
    msg := sprintf(
        "Resource '%s' is missing required tags: %v. [Policy: required-tags]",
        [resource.address, missing]
    )
}
# restrict_regions.rego - Limit resource deployment to approved regions
package terraform.aws.regions

import rego.v1

approved_regions := {"us-east-1", "us-west-2", "eu-west-1"}

deny contains msg if {
    resource := input.resource_changes[_]
    provider_config := input.configuration.provider_config.aws
    region := provider_config.expressions.region.constant_value
    not region in approved_regions
    msg := sprintf(
        "Resource '%s' is in region '%s'. Approved regions: %v. [Policy: approved-regions]",
        [resource.address, region, approved_regions]
    )
}
# Evaluate OPA policies against Terraform plan
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# Run OPA evaluation
opa eval \
  --data policies/ \
  --input tfplan.json \
  "data.terraform.aws.s3.deny" \
  --format pretty

# Use conftest for easier CI integration
conftest test tfplan.json --policy policies/ --output table

Kyverno Kubernetes Policies

# require-labels.yaml - Enforce required labels on all pods
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
  annotations:
    policies.kyverno.io/title: Require Labels
    policies.kyverno.io/category: Best Practices
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: check-required-labels
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: >-
        Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version',
        and 'team' are required on all Pods.
      pattern:
        metadata:
          labels:
            app.kubernetes.io/name: "?*"
            app.kubernetes.io/version: "?*"
            team: "?*"
---
# disallow-privileged.yaml - Block privileged containers
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
  annotations:
    policies.kyverno.io/title: Disallow Privileged Containers
    policies.kyverno.io/category: Pod Security
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: deny-privileged
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "Privileged containers are not allowed."
      pattern:
        spec:
          containers:
          - securityContext:
              privileged: "false"
          =(initContainers):
          - securityContext:
              privileged: "false"
---
# require-resource-limits.yaml - Enforce resource limits
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
  annotations:
    policies.kyverno.io/title: Require Resource Limits
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: check-resource-limits
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "All containers must have CPU and memory limits defined."
      pattern:
        spec:
          containers:
          - resources:
              limits:
                memory: "?*"
                cpu: "?*"
---
# disallow-latest-tag.yaml - Block usage of 'latest' image tag
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
  annotations:
    policies.kyverno.io/title: Disallow Latest Tag
    policies.kyverno.io/severity: medium
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: validate-image-tag
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "Images must use a specific tag, not 'latest'."
      pattern:
        spec:
          containers:
          - image: "!*:latest & *:*"
---
# restrict-image-registries.yaml - Allow only approved registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
  annotations:
    policies.kyverno.io/title: Restrict Image Registries
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Enforce
  background: true
  rules:
  - name: validate-registries
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: >-
        Images must come from approved registries:
        123456789012.dkr.ecr.us-east-1.amazonaws.com or ghcr.io/your-org.
      pattern:
        spec:
          containers:
          - image: "123456789012.dkr.ecr.*.amazonaws.com/* | ghcr.io/your-org/*"
---
# require-networkpolicy.yaml - Ensure namespaces have NetworkPolicies
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-networkpolicy
  annotations:
    policies.kyverno.io/title: Require Network Policy
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Audit
  background: true
  rules:
  - name: check-networkpolicy
    match:
      any:
      - resources:
          kinds:
          - Deployment
    preconditions:
      all:
      - key: "{{request.object.metadata.namespace}}"
        operator: NotIn
        value: ["kube-system", "kube-public"]
    validate:
      message: "A NetworkPolicy must exist in namespace '{{request.object.metadata.namespace}}' before deploying workloads."
      deny:
        conditions:
          all:
          - key: "{{request.object.metadata.namespace}}"
            operator: AnyNotIn
            value: "{{request.object.metadata.namespace}}"

Checkov Custom Checks

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


class S3Versioning(BaseResourceCheck):
    def __init__(self):
        name = "Ensure S3 bucket has versioning enabled"
        id = "CUSTOM_S3_001"
        supported_resources = ["aws_s3_bucket_versioning"]
        categories = [CheckCategories.BACKUP_AND_RECOVERY]
        super().__init__(name=name, id=id,
                         categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        versioning = conf.get("versioning_configuration", [{}])
        if isinstance(versioning, list):
            versioning = versioning[0] if versioning else {}
        status = versioning.get("status", ["Disabled"])
        if isinstance(status, list):
            status = status[0]
        return CheckResult.PASSED if status == "Enabled" else CheckResult.FAILED


check = S3Versioning()
# custom_checks/require_rds_backup.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories


class RDSBackupRetention(BaseResourceCheck):
    def __init__(self):
        name = "Ensure RDS has backup retention of at least 7 days"
        id = "CUSTOM_RDS_001"
        supported_resources = ["aws_db_instance"]
        categories = [CheckCategories.BACKUP_AND_RECOVERY]
        super().__init__(name=name, id=id,
                         categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        retention = conf.get("backup_retention_period", [0])
        if isinstance(retention, list):
            retention = retention[0]
        return CheckResult.PASSED if int(retention) >= 7 else CheckResult.FAILED


check = RDSBackupRetention()
# Run Checkov with custom checks
checkov -d ./terraform \
  --framework terraform \
  --external-checks-dir ./custom_checks \
  --output cli \
  --compact

# Run specific check IDs
checkov -d ./terraform \
  --check CUSTOM_S3_001,CUSTOM_RDS_001,CKV_AWS_18,CKV_AWS_19

# Generate SARIF output for GitHub Advanced Security integration
checkov -d ./terraform \
  --framework terraform \
  --output sarif \
  --output-file checkov-results.sarif

# Skip specific checks with documented justification
checkov -d ./terraform \
  --skip-check CKV_AWS_145 \
  --skip-check CKV_AWS_79

CI/CD Pipeline Integration

# GitHub Actions - Policy enforcement in PR workflow
name: Policy Checks
on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'kubernetes/**'

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

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init and Plan
        working-directory: terraform/
        run: |
          terraform init -backend=false
          terraform plan -out=tfplan
          terraform show -json tfplan > tfplan.json

      - name: OPA Policy Check
        uses: open-policy-agent/setup-opa@v2
        with:
          version: latest
      - run: |
          RESULTS=$(opa eval \
            --data policies/ \
            --input terraform/tfplan.json \
            --format json \
            "data.terraform" | jq '.result[0].expressions[0].value')
          DENY_COUNT=$(echo "$RESULTS" | jq '[.. | .deny? // empty | .[] ] | length')
          if [ "$DENY_COUNT" -gt 0 ]; then
            echo "::error::Policy violations found:"
            echo "$RESULTS" | jq '.. | .deny? // empty | .[]'
            exit 1
          fi

      - name: Checkov Scan
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          output_format: cli,sarif
          output_file_path: console,checkov-results.sarif
          soft_fail: false
          external_checks_dirs: custom_checks/

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov-results.sarif

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

      - name: Install Kyverno CLI
        run: |
          curl -LO https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_amd64.tar.gz
          tar -xzf kyverno-cli_linux_amd64.tar.gz
          sudo mv kyverno /usr/local/bin/

      - name: Test Kyverno Policies
        run: |
          kyverno apply policies/kyverno/ \
            --resource kubernetes/manifests/ \
            --detailed-results \
            --output-format table

      - name: Conftest Kubernetes Manifests
        uses: open-policy-agent/conftest-action@v2
        with:
          files: kubernetes/manifests/
          policy: policies/kubernetes/

Policy Exception Management

exception_workflow:
  request:
    fields:
      - policy_id: "Which policy needs an exception"
      - resource: "Specific resource requiring exception"
      - justification: "Business reason for the exception"
      - compensating_controls: "Alternative mitigations in place"
      - duration: "Temporary (with expiry) or permanent"
      - requestor: "Person requesting"
      - approver: "Security team member who approved"

  approval_process:
    1: "Requestor submits exception with justification"
    2: "Security team reviews and assesses risk"
    3: "Compensating controls verified"
    4: "Exception approved or denied with rationale"
    5: "Exception documented in registry"
    6: "Automated enforcement updated to allow exception"

  enforcement:
    opa: |
      # Exception list loaded as data
      # policies/exceptions.json
      # {"exceptions": [{"resource": "aws_s3_bucket.public_website", "policy": "no-public-s3", "expires": "2025-06-01"}]}
    kyverno: |
      # Use Kyverno PolicyException resource
      apiVersion: kyverno.io/v2beta1
      kind: PolicyException
      metadata:
        name: allow-public-website
        namespace: web
      spec:
        exceptions:
        - policyName: disallow-privileged-containers
          ruleNames:
          - deny-privileged
        match:
          any:
          - resources:
              kinds:
              - Pod
              names:
              - legacy-app-*

  review_schedule:
    - Review all active exceptions quarterly
    - Expire temporary exceptions automatically
    - Re-justify permanent exceptions annually
    - Track exception count trends as a security metric

Policy Testing

# Test OPA policies with mock input
mkdir -p policies/tests

# Create test input
cat > policies/tests/public_bucket_test.json <<'EOF'
{
  "resource_changes": [{
    "address": "aws_s3_bucket.test",
    "type": "aws_s3_bucket",
    "change": {
      "after": {"acl": "public-read"}
    }
  }]
}
EOF

# Run test
opa eval --data policies/ --input policies/tests/public_bucket_test.json \
  "data.terraform.aws.s3.deny" --format pretty
# Should output the deny message

# OPA unit tests
cat > policies/tests/s3_test.rego <<'EOF'
package terraform.aws.s3_test

import rego.v1
import data.terraform.aws.s3

test_deny_public_bucket if {
    result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "public-read"}}}]}
    count(result) > 0
}

test_allow_private_bucket if {
    result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "private"}}}]}
    count(result) == 0
}
EOF

opa test policies/ -v

Best Practices

  • Version control all policies alongside the infrastructure code they govern
  • Start in audit/warn mode and transition to enforce after verifying no false positives
  • Write unit tests for every policy to catch regressions and verify intended behavior
  • Implement a formal exception process: never disable policies to bypass legitimate checks
  • Use policy results as PR status checks to block non-compliant merges
  • Layer policies: Checkov for static analysis, OPA for Terraform plan evaluation, Kyverno for runtime
  • Tag policies with compliance framework references (e.g., SOC 2 CC6.1, PCI Req 2.2)
  • Monitor policy violation trends over time to identify systemic issues
  • Provide clear, actionable error messages that explain how to fix violations
  • Roll out new policies gradually: inform teams, give a remediation window, then enforce

Tips for Policy As Code

  • • Start a fresh session after installing so your agent picks up the new skill.
  • • It is part of a pack with 40 skills. Install only the ones you need with --skill to keep triggers precise.
  • • If two skills overlap, mention the one you want by name.

Questions about Policy As Code

What does the Policy As Code skill do?

Implement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.

How do I install Policy As Code?

Run "npx skills add BagelHole/DevOps-Security-Agent-Skills --skill policy-as-code" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.

Does Policy As Code work in Cursor and Codex?

Yes. Policy As Code uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.

Is Policy As Code free?

Policy As Code is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.

Who made Policy As Code?

Policy As Code is published by BagelHole in the BagelHole/DevOps-Security-Agent-Skills repository, together with 39 other skills.

Similar skills. From other creators.