Skip to main content
โšก Calmops

Security Compliance Automation: CIS Benchmarks, Terraform

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