mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -9,61 +9,592 @@ metadata:
|
||||
|
||||
# Policy as Code
|
||||
|
||||
Automate policy enforcement through code.
|
||||
Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production.
|
||||
|
||||
## Open Policy Agent (OPA)
|
||||
## 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_buckets.rego
|
||||
package terraform.s3
|
||||
# deny_public_s3.rego - Deny S3 buckets with public access
|
||||
package terraform.aws.s3
|
||||
|
||||
deny[msg] {
|
||||
resource := input.resource.aws_s3_bucket[name]
|
||||
resource.acl == "public-read"
|
||||
msg := sprintf("S3 bucket '%s' has public ACL", [name])
|
||||
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]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Kyverno (Kubernetes)
|
||||
```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
|
||||
validationFailureAction: Enforce
|
||||
background: true
|
||||
rules:
|
||||
- name: check-labels
|
||||
- name: check-required-labels
|
||||
match:
|
||||
resources:
|
||||
kinds:
|
||||
- Pod
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Label 'app' is required"
|
||||
message: >-
|
||||
Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version',
|
||||
and 'team' are required on all Pods.
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
app: "?*"
|
||||
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
|
||||
## 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
|
||||
# Scan Terraform
|
||||
checkov -d . --framework terraform
|
||||
# Run Checkov with custom checks
|
||||
checkov -d ./terraform \
|
||||
--framework terraform \
|
||||
--external-checks-dir ./custom_checks \
|
||||
--output cli \
|
||||
--compact
|
||||
|
||||
# Custom check
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
# Run specific check IDs
|
||||
checkov -d ./terraform \
|
||||
--check CUSTOM_S3_001,CUSTOM_RDS_001,CKV_AWS_18,CKV_AWS_19
|
||||
|
||||
class S3Encryption(BaseResourceCheck):
|
||||
def scan_resource_conf(self, conf):
|
||||
return CheckResult.PASSED if 'encryption' in conf else CheckResult.FAILED
|
||||
# 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 policies
|
||||
- Test policies in CI
|
||||
- Gradual rollout (warn → enforce)
|
||||
- Exception management
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user