mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
601 lines
17 KiB
Markdown
601 lines
17 KiB
Markdown
---
|
|
name: policy-as-code
|
|
description: Implement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.
|
|
license: MIT
|
|
metadata:
|
|
author: devops-skills
|
|
version: "1.0"
|
|
---
|
|
|
|
# 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
|
|
|
|
```rego
|
|
# 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]
|
|
)
|
|
}
|
|
```
|
|
|
|
```rego
|
|
# 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[_]
|
|
}
|
|
```
|
|
|
|
```rego
|
|
# 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]
|
|
)
|
|
}
|
|
```
|
|
|
|
```rego
|
|
# 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]
|
|
)
|
|
}
|
|
```
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```python
|
|
# 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()
|
|
```
|
|
|
|
```python
|
|
# 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()
|
|
```
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
# 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
|