Introduction
Manual compliance is unsustainable. With 95% of compliance failures caused by misconfiguration, automation isn’t optionalโit’s essential. This guide covers building continuous compliance using CIS benchmarks, Terraform, and policy-as-code.
Key Statistics:
- 95% of compliance failures are configuration errors
- Automated compliance reduces audit prep time by 70%
- Self-healing infrastructure reduces remediation time by 90%
- Continuous monitoring catches 85% more issues than periodic audits
CIS Benchmarks Implementation
AWS CIS Benchmark Checklist
| Control ID | Description | Automated |
|---|---|---|
| 1.1 | CloudFront Distributions | โ |
| 1.2 | S3 Block Public Access | โ |
| 1.3 | MFA on Root Account | โ |
| 1.4 | MFA on IAM Users | โ |
| 1.5 | Password Policy | โ |
| 2.1 | CloudTrail Enabled | โ |
| 2.2 | CloudTrail Encryption | โ |
| 2.3 | CloudTrail IMDSv2 | โ |
| 3.1 | S3 Encryption | โ |
| 3.2 | S3 Logging | โ |
Automated CIS Compliance
# Terraform CIS Compliance Module
variable "enable_cis_compliance" {
description = "Enable CIS compliance controls"
type = bool
default = true
}
# Control 1.1 - CloudFront Distributions
resource "aws_cloudfront_distribution" "cis_compliant" {
count = var.enable_cis_compliance ? 1 : 0
origin {
domain_name = var.origin_domain
origin_id = "my-origin"
}
enabled = true
is_ipv6_enabled = true
comment = "CIS Compliant Distribution"
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "my-origin"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
# Control 1.1 - Restrict viewer access
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = var.allowed_countries
}
}
# Control 1.1 - Use latest TLS
viewer_certificate {
cloudfront_default_certificate = true
minimum_protocol_version = "TLSv1.2_2021"
}
}
# Control 1.2 - S3 Block Public Access
resource "aws_s3_bucket_public_access_block" "cis_compliant" {
count = var.enable_cis_compliance ? 1 : 0
bucket = aws_s3_bucket.main.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Control 1.3 - MFA for Root Account (requires manual, but enforce with SCP)
resource "aws_organizations_policy" "enforce_mfa" {
count = var.enable_cis_compliance ? 1 : 0
name = "Enforce MFA for Root Account"
description = "SCP to enforce MFA for root account"
content = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyRootAccountActionsWithoutMFA",
"Effect": "Deny",
"Action": [
"iam:*AccessKey*",
"iam:*LoginProfile*",
"iam:Update*",
"iam:Delete*",
"ec2:*",
"s3:*"
],
"Resource": ["arn:aws:iam::*:root"],
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
})
}
AWS Config Rules
CIS-Compliant Rules
# Terraform AWS Config Rules for CIS
resource "aws_config_config_rule" "cis_s3_public_access" {
name = "cis-s3-public-access-blocked"
source {
owner = "AWS"
source_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
}
scope {
resources = ["AWS::S3::Bucket"]
}
tags = {
benchmark = "cis"
severity = "critical"
}
}
resource "aws_config_config_rule" "cis_mfa_enabled" {
name = "cis-iam-mfa-enabled"
source {
owner = "AWS"
source_identifier = "IAM_USER_MFA_ENABLED"
}
scope {
resource_type = "AWS::IAM::User"
}
}
resource "aws_config_config_rule" "cis_cloudtrail_enabled" {
name = "cis-cloudtrail-enabled"
source {
owner = "AWS"
source_identifier = "CLOUDTRAIL_ENABLED"
}
}
resource "aws_config_config_rule" "cis_rds_encryption" {
name = "cis-rds-storage-encrypted"
source {
owner = "AWS"
source_identifier = "RDS_STORAGE_ENCODED"
}
scope {
resource_type = "AWS::RDS::DBInstance"
}
}
Custom Remediation
# Auto-remediation with Lambda
resource "aws_config_config_rule" "cis_remediation" {
name = "cis-s3-encryption-remediation"
source {
owner = "CUSTOM_LAMBDA"
source_identifier = aws_lambda_function.config_remediation.arn
}
scope {
resources = ["AWS::S3::Bucket"]
}
}
resource "aws_lambda_permission" "allow_config" {
statement_id = "AllowExecutionFromConfig"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.config_remediation.function_name
principal = "config.amazonaws.com"
}
#!/usr/bin/env python3
"""AWS Config auto-remediation Lambda."""
import boto3
import json
s3 = boto3.client('s3')
def handler(event, context):
"""Handle Config remediation."""
# Get the bucket name from the event
invoking_event = json.loads(event['invokingEvent'])
bucket_name = invoking_event['configurationItem']['resourceName']
# Check if already encrypted
try:
encryption = s3.get_bucket_encryption(Bucket=bucket_name)
print(f"Bucket {bucket_name} is already encrypted")
return {'statusCode': 200}
except s3.exceptions.ClientError as e:
if 'ServerSideEncryptionConfigurationNotFoundError' not in str(e):
raise
# Enable encryption
s3.put_bucket_encryption(
Bucket=bucket_name,
ServerSideEncryptionConfiguration={
'Rules': [
{
'ApplyServerSideEncryptionByDefault': {
'SSEAlgorithm': 'AES256'
}
}
]
}
)
print(f"Enabled encryption on bucket {bucket_name}")
return {'statusCode': 200}
Policy as Code with Sentinel
Terraform Cloud Policies
# Sentinel policy for AWS resources
import "tfplan/v2" as tfplan
# Deny resources without required tags
main = rule {
all tfplan.resource_changes as _, rc {
all rc.change.after as key, value {
key in ["tags", "Name"] implies value is not null
}
}
}
# Specific rules for S3
s3_rules = rule {
all tfplan.resource_changes as _, rc {
rc.type is "aws_s3_bucket" implies {
all rc.change.after as key, value {
key is "acl" implies value is not "public-read"
}
}
}
}
# Deny specific instance types
ec2_rules = rule {
all tfplan.resource_changes as _, rc {
rc.type is "aws_instance" implies {
rc.change.after.instance_type in [
"t2.micro",
"t2.small",
"t3.micro",
"t3.small"
]
}
}
}
Kubernetes Security Policies
OPA Gatekeeper
# CIS Benchmark for Kubernetes
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredsecuritycontext
spec:
crd:
spec:
names:
kind: K8sRequiredSecurityContext
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredsecuritycontext
violation[{"msg": msg}] {
container := input.request.object.spec.containers[_]
not container.securityContext
msg := sprintf("Container %s must define securityContext", [container.name])
}
violation[{"msg": msg}] {
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container %s must runAsNonRoot", [container.name])
}
violation[{"msg": msg}] {
container := input.request.object.spec.containers[_]
not container.securityContext.readOnlyRootFilesystem
msg := sprintf("Container %s should use readOnlyRootFilesystem", [container.name])
}
# CIS: Require privileged flag to be false
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8snoprivilegedcontainers
spec:
crd:
spec:
names:
kind: K8sNoPrivilegedContainers
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snoprivilegedcontainers
violation[{"msg": msg}] {
container := input.request.object.spec.containers[_]
container.securityContext
container.securityContext.privileged == true
msg := sprintf("Container %s cannot run in privileged mode", [container.name])
}
Compliance Dashboard
# Grafana Compliance Dashboard
{
"dashboard": {
"title": "CIS Compliance Dashboard",
"tags": ["compliance", "cis", "security"],
"panels": [
{
"title": "Compliance Score",
"type": "stat",
"gridPos": {"h": 6, "w": 6},
"targets": [
{
"expr": "sum(cis_compliant_rules) / sum(cis_total_rules) * 100",
"legendFormat": "Compliance %"
}
]
},
{
"title": "Non-Compliant Resources",
"type": "table",
"gridPos": {"h": 12, "w": 12},
"targets": [
{
"expr": "topk(20, cis_non_compliant_resources)",
"format": "table"
}
]
},
{
"title": "Compliance by Control",
"type": "bargauge",
"gridPos": {"h": 8, "w": 12},
"targets": [
{
"expr": "sum by (control) (cis_compliance_status)",
"legendFormat": "{{control}}"
}
]
}
]
}
}
Continuous Compliance Pipeline
# GitLab CI Compliance Pipeline
stages:
- scan
- remediate
- verify
- report
compliance_scan:
stage: scan
image: checkov/checkov:latest
script:
- checkov -d . --framework terraform --output json --output-file gltf-report.json
- checkov -d . --framework kubernetes --output json --output-file k8s-report.json
artifacts:
reports:
dotenv: tfplan.env
paths:
- "*-report.json"
rules:
- if: $CI_COMMIT_BRANCH == "main"
auto_remediate:
stage: remediate
image: alpine:latest
script:
- apk add --no-cache jq
- |
# Parse scan results and create PR/issue for non-compliance
cat k8s-report.json | jq -r '.results.failed_checks[] | "\(.check_id) \(.check_name)"'
needs:
- compliance_scan
compliance_report:
stage: report
image: python:3.9
script:
- pip install jinja2
- python generate_report.py
artifacts:
paths:
- compliance-report.html
Comments