Implementing IaC Security Scanning

Implementing IaC Security Scanning

Static analysis of IaC templates identifies misconfigurations before deployment. Unlike application code scanning, IaC scanning must understand cloud provider APIs, resource relationships, and security best practices. Modern IaC scanners combine rule-based checking with cloud provider knowledge to identify both obvious misconfigurations and subtle security issues.

# Example Terraform with security considerations
# main.tf - Secure AWS infrastructure

# KMS key for encryption
resource "aws_kms_key" "main" {
  description             = "KMS key for ${var.environment} environment"
  deletion_window_in_days = 30
  enable_key_rotation     = true
  
  tags = merge(
    var.common_tags,
    {
      Name = "${var.project}-${var.environment}-kms"
      Security = "encryption-at-rest"
    }
  )
}

# S3 bucket with security best practices
resource "aws_s3_bucket" "secure_bucket" {
  bucket = "${var.project}-${var.environment}-data"
  
  tags = merge(
    var.common_tags,
    {
      Name = "${var.project}-${var.environment}-data"
      DataClassification = var.data_classification
    }
  )
}

# Block all public access
resource "aws_s3_bucket_public_access_block" "secure_bucket" {
  bucket = aws_s3_bucket.secure_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Enable versioning for data protection
resource "aws_s3_bucket_versioning" "secure_bucket" {
  bucket = aws_s3_bucket.secure_bucket.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

# Enable encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "secure_bucket" {
  bucket = aws_s3_bucket.secure_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.main.arn
      sse_algorithm     = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# Enable logging
resource "aws_s3_bucket_logging" "secure_bucket" {
  bucket = aws_s3_bucket.secure_bucket.id

  target_bucket = aws_s3_bucket.log_bucket.id
  target_prefix = "s3-access-logs/"
}

# Lifecycle policies for compliance
resource "aws_s3_bucket_lifecycle_configuration" "secure_bucket" {
  bucket = aws_s3_bucket.secure_bucket.id

  rule {
    id     = "compliance-retention"
    status = "Enabled"

    transition {
      days          = 90
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 180
      storage_class = "GLACIER"
    }

    expiration {
      days = var.retention_days
    }
  }
}

# RDS instance with security hardening
resource "aws_db_instance" "secure_database" {
  identifier = "${var.project}-${var.environment}-db"
  
  # Security configurations
  storage_encrypted               = true
  kms_key_id                     = aws_kms_key.main.arn
  backup_retention_period        = 30
  backup_window                  = "03:00-04:00"
  maintenance_window             = "sun:04:00-sun:05:00"
  deletion_protection            = var.environment == "production" ? true : false
  enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]
  
  # Network security
  db_subnet_group_name   = aws_db_subnet_group.private.name
  vpc_security_group_ids = [aws_security_group.database.id]
  publicly_accessible    = false
  
  # Authentication
  iam_database_authentication_enabled = true
  
  # Monitoring
  performance_insights_enabled = true
  performance_insights_retention_period = 7
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring.arn
  
  tags = merge(
    var.common_tags,
    {
      Name = "${var.project}-${var.environment}-db"
      Backup = "required"
    }
  )
}

# Security group with least privilege
resource "aws_security_group" "database" {
  name_prefix = "${var.project}-${var.environment}-db-"
  description = "Security group for RDS database"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "MySQL/Aurora from application"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.application.id]
  }

  egress {
    description = "Deny all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["127.0.0.1/32"]
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = merge(
    var.common_tags,
    {
      Name = "${var.project}-${var.environment}-db-sg"
    }
  )
}

Pipeline integration of IaC scanning requires balancing security thoroughness with deployment speed. Pre-commit hooks can catch basic issues before code commit. Pull request scanning provides comprehensive analysis with detailed feedback. Pre-deployment scanning offers final validation before resource creation. Each stage uses different scanning depths optimized for feedback speed.

# GitHub Actions workflow for IaC security
name: IaC Security Pipeline

on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'cloudformation/**'
      - 'kubernetes/**'

jobs:
  terraform-security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: TFSec Security Scan
        uses: aquasecurity/[email protected]
        with:
          working_directory: ./terraform
          format: json
          out: tfsec-results.json
          
      - name: Checkov Policy Scan
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./terraform
          framework: terraform
          output_format: json
          output_file_path: ./checkov-results.json
          
      - name: Terrascan
        run: |
          docker run --rm -v $(pwd):/src accurics/terrascan scan \
            -i terraform -d /src/terraform \
            -o json > terrascan-results.json
            
      - name: Open Policy Agent Validation
        run: |
          # Download OPA binary
          curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
          chmod +x opa
          
          # Validate against policies
          ./opa eval -d policies/ -i terraform/main.tf.json \
            "data.terraform.deny[_]" > opa-results.json
            
      - name: Aggregate Results
        run: |
          python3 scripts/aggregate_iac_scan_results.py \
            --tfsec tfsec-results.json \
            --checkov checkov-results.json \
            --terrascan terrascan-results.json \
            --opa opa-results.json \
            --output security-report.md
            
      - name: Comment PR
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('security-report.md', 'utf8');
            
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: report
            });
            
      - name: Fail on Critical Issues
        run: |
          if grep -q "CRITICAL" security-report.md; then
            echo "Critical security issues found!"
            exit 1
          fi