Skip to main content

Security Compliance Automation: CIS Benchmarks, Terraform

Published: February 18, 2026 • Updated: May 8, 2026 Larry Qu 6 min read

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

External Resources


Comments

👍 Was this article helpful?