Skip to content

Infrastructure as Code Security

Alex Stojcic edited this page Apr 3, 2025 · 1 revision

Infrastructure as Code Security

Infrastructure as Code (IaC) enables teams to define and provision infrastructure through machine-readable definition files rather than manual processes. While IaC offers numerous benefits for consistency and scalability, it also introduces unique security challenges. This guide covers comprehensive security practices for IaC implementations.

IaC Security Fundamentals

Security Benefits of IaC

  1. Consistency - Every deployment follows the same secure configuration
  2. Version control - Track changes and maintain audit history
  3. Automated testing - Verify security before deployment
  4. Reduced human error - Eliminate manual misconfigurations
  5. Compliance as Code - Embed compliance requirements in templates

Common IaC Security Risks

  1. Insecure defaults - Templates with weak security configurations
  2. Secrets in code - Hardcoded credentials and sensitive data
  3. Excessive permissions - Overprivileged resources and service accounts
  4. Misconfiguration - Security options disabled or improperly set
  5. Unpatched components - Outdated base images or software versions
  6. Insufficient monitoring - Lack of security logging and alerting

Securing Terraform Deployments

Terraform is a widely-used IaC tool that supports multiple cloud providers and services.

Terraform Security Best Practices

  1. Structure and Organization
# Modular structure for better security management
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── staging/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── security/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── shared/
    └── iam/
        ├── roles.tf
        └── policies.tf
  1. Secure State Management
# Use encrypted remote state with access controls
terraform {
  backend "s3" {
    bucket         = "terraform-state-secure"
    key            = "prod/terraform.tfstate"
    region         = "us-west-2"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    
    # Use IAM roles rather than static credentials
    role_arn       = "arn:aws:iam::123456789012:role/TerraformStateManager"
  }
}
  1. Secure Variable Handling
# Define sensitive variables
variable "database_password" {
  description = "Password for database access"
  type        = string
  sensitive   = true # Marks as sensitive in logs and outputs
}

# Use environment variables for secrets (don't hardcode)
# TF_VAR_database_password=securepassword terraform apply
  1. Provider Configuration Security
# Provider configuration with secure settings
provider "aws" {
  region = "us-west-2"

  # Enforce minimum TLS version
  default_tags {
    tags = {
      Environment = "Production"
      ManagedBy   = "Terraform"
    }
  }

  # Use assume role instead of static credentials
  assume_role {
    role_arn = "arn:aws:iam::123456789012:role/TerraformDeploymentRole"
  }
}

# Version constraints for providers
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"  # Only accept 4.x versions
    }
  }
}

Secure AWS Resources with Terraform

  1. Secure S3 Bucket Configuration
resource "aws_s3_bucket" "data_bucket" {
  bucket = "my-secure-data-bucket"

  # Explicitly disable public access
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

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

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# Enable versioning
resource "aws_s3_bucket_versioning" "bucket_versioning" {
  bucket = aws_s3_bucket.data_bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

# Enable access logging
resource "aws_s3_bucket_logging" "bucket_logging" {
  bucket = aws_s3_bucket.data_bucket.id

  target_bucket = aws_s3_bucket.log_bucket.id
  target_prefix = "log/data-bucket/"
}
  1. Secure EC2 Instance Configuration
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0" # Use vetted, up-to-date AMIs
  instance_type = "t3.micro"
  
  # Use secure networking
  subnet_id = aws_subnet.private_subnet.id
  
  # Enable detailed monitoring
  monitoring = true
  
  # Secure IAM instance profile (least privilege)
  iam_instance_profile = aws_iam_instance_profile.web_server_profile.name

  root_block_device {
    encrypted = true  # Enable EBS encryption
    volume_size = 20
  }

  metadata_options {
    http_tokens = "required"  # Require IMDSv2
    http_endpoint = "enabled"
  }

  # Use security groups with least privilege
  vpc_security_group_ids = [aws_security_group.web_server_sg.id]
  
  # User data for hardening
  user_data = <<-EOF
              #!/bin/bash
              # Update system
              yum update -y
              # Set secure SSH configuration
              sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
              systemctl restart sshd
              EOF

  tags = {
    Name = "SecureWebServer"
  }
}

# Restrictive security group
resource "aws_security_group" "web_server_sg" {
  name        = "web-server-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id

  # Allow HTTP/HTTPS only
  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Restrict SSH access to VPN IP range
  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"] # Corporate VPN range
  }

  # Allow all outbound traffic
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
  1. Secure IAM Configuration
# Create role with least privilege
resource "aws_iam_role" "app_role" {
  name = "app-service-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      },
    ]
  })
}

# Custom policy with minimal permissions
resource "aws_iam_policy" "app_policy" {
  name        = "app-service-policy"
  description = "Minimal permissions for application"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetObject",
          "s3:ListBucket",
        ]
        Effect = "Allow"
        Resource = [
          "${aws_s3_bucket.data_bucket.arn}",
          "${aws_s3_bucket.data_bucket.arn}/*",
        ]
      },
      {
        Action = [
          "cloudwatch:PutMetricData",
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect = "Allow"
        Resource = "*"
      }
    ]
  })
}

# Attach policy to role
resource "aws_iam_role_policy_attachment" "app_attach" {
  role       = aws_iam_role.app_role.name
  policy_arn = aws_iam_policy.app_policy.arn
}

# Create instance profile
resource "aws_iam_instance_profile" "web_server_profile" {
  name = "web-server-profile"
  role = aws_iam_role.app_role.name
}

Securing CloudFormation Templates

AWS CloudFormation allows you to define your AWS infrastructure as code.

CloudFormation Security Best Practices

  1. Template Structure and Organization
# secure-web-app.yaml - Example of well-structured template
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Secure web application stack with VPC, security groups, and web servers'

# Parameter section with proper constraints
Parameters:
  EnvironmentType:
    Description: Environment type
    Type: String
    Default: Development
    AllowedValues:
      - Development
      - Production
    ConstraintDescription: Must be Development or Production
  
  DBPassword:
    Description: Database admin password
    Type: String
    NoEcho: true  # Hide sensitive parameter
    MinLength: 12
    MaxLength: 41
    ConstraintDescription: Password must be between 12 and 41 characters

# Organize resources by type for clarity and management
Resources:
  # Network resources
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      # ...

  # Security resources
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      # ...
  
  # Instance resources
  WebServer:
    Type: AWS::EC2::Instance
    Properties:
      # ...

# Output only necessary information
Outputs:
  WebsiteURL:
    Description: URL for the web application
    Value: !GetAtt [LoadBalancer, DNSName]
  1. Secure VPC and Network Configuration
# Secure VPC configuration
VPC:
  Type: AWS::EC2::VPC
  Properties:
    CidrBlock: 10.0.0.0/16
    EnableDnsSupport: true
    EnableDnsHostnames: true
    Tags:
      - Key: Name
        Value: SecureVPC

# Public and private subnets
PublicSubnet1:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    CidrBlock: 10.0.1.0/24
    AvailabilityZone: !Select [0, !GetAZs '']
    MapPublicIpOnLaunch: false # Don't auto-assign public IPs
    Tags:
      - Key: Name
        Value: PublicSubnet1

PrivateSubnet1:
  Type: AWS::EC2::Subnet
  Properties:
    VpcId: !Ref VPC
    CidrBlock: 10.0.2.0/24
    AvailabilityZone: !Select [0, !GetAZs '']
    Tags:
      - Key: Name
        Value: PrivateSubnet1

# Network ACL with restrictive rules
PrivateSubnetNetworkAcl:
  Type: AWS::EC2::NetworkAcl
  Properties:
    VpcId: !Ref VPC
    Tags:
      - Key: Name
        Value: PrivateSubnetNetworkAcl

# Inbound rule - Allow only HTTP/HTTPS from public subnet
InboundHTTPSNetworkAclEntry:
  Type: AWS::EC2::NetworkAclEntry
  Properties:
    NetworkAclId: !Ref PrivateSubnetNetworkAcl
    RuleNumber: 100
    Protocol: 6  # TCP
    RuleAction: allow
    Egress: false
    CidrBlock: 10.0.1.0/24
    PortRange:
      From: 443
      To: 443
  1. Secure Parameter Handling
# Use SecureString parameter store for sensitive data
DBPasswordParameter:
  Type: AWS::SSM::Parameter
  Properties:
    Name: /app/database/password
    Type: SecureString
    Value: !Ref DBPassword
    Description: Database password stored securely
    Tier: Standard
    
# Reference stored parameter instead of using direct value
RDSDBInstance:
  Type: AWS::RDS::DBInstance
  Properties:
    DBName: !Ref DatabaseName
    Engine: mysql
    MasterUsername: admin
    MasterUserPassword: !Sub '{{resolve:ssm-secure:/app/database/password}}'
    StorageEncrypted: true # Enable encryption
    BackupRetentionPeriod: 7
    MultiAZ: true

CloudFormation Guard Rules

CloudFormation Guard lets you define policy rules for CloudFormation templates.

# cfn-guard rule file (security-rules.guard)

# Ensure S3 buckets have encryption enabled
rule s3_bucket_encryption_enabled {
  AWS::S3::Bucket {
    # Check for encryption configuration
    BucketEncryption exists
    BucketEncryption is_struct
    BucketEncryption {
      ServerSideEncryptionConfiguration exists
      ServerSideEncryptionConfiguration is_list
      ServerSideEncryptionConfiguration[*] {
        ServerSideEncryptionByDefault exists
      }
    }
  }
}

# Ensure RDS instances are encrypted
rule rds_storage_encrypted {
  AWS::RDS::DBInstance {
    StorageEncrypted exists
    StorageEncrypted == true
  }
}

# Ensure security groups don't allow unrestricted access (0.0.0.0/0) to risky ports
rule no_unrestricted_ssh_access {
  AWS::EC2::SecurityGroup {
    SecurityGroupIngress[*] {
      CidrIp != "0.0.0.0/0" or FromPort != 22
    }
  }
}

# Ensure IAM policies don't grant admin (*) permissions
rule no_iam_wildcards {
  AWS::IAM::Policy {
    PolicyDocument {
      Statement[*] {
        Effect == "Allow" {
          Action != "*"
          Resource != "*"
        }
      }
    }
  }
}

Securing Kubernetes Manifests

Kubernetes manifests are YAML or JSON files that describe the desired state of Kubernetes resources.

Kubernetes Security Best Practices

  1. Secure Pod Configuration
# secure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
  labels:
    app: secure-app
spec:
  # Run as non-root user
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  
  containers:
  - name: secure-container
    image: myapp:1.0.0  # Specify exact version, not 'latest'
    
    # Set container security context
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
          - ALL
        add:
          - NET_BIND_SERVICE
    
    # Set resource limits
    resources:
      limits:
        cpu: "500m"
        memory: "512Mi"
      requests:
        cpu: "250m"
        memory: "256Mi"
    
    # Liveness and readiness probes
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 20
    
    # Secure environment variables
    env:
    - name: APP_ENV
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: environment
    # Use secrets for sensitive data
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: app-secrets
          key: db-password
    
    # Mount volumes securely
    volumeMounts:
    - name: config-volume
      mountPath: /config
      readOnly: true
  
  # Volumes
  volumes:
  - name: config-volume
    configMap:
      name: app-config
  
  # Restrict which nodes can run this pod
  nodeSelector:
    environment: production
  1. Network Policies
# Restrictive network policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-policy
  namespace: production
spec:
  # Select pods this policy applies to
  podSelector:
    matchLabels:
      app: api-service
  # Default deny all ingress and egress
  policyTypes:
  - Ingress
  - Egress
  # Allow specific ingress
  ingress:
  - from:
    # Only allow traffic from frontend pods
    - podSelector:
        matchLabels:
          app: frontend
    # On specific ports
    ports:
    - protocol: TCP
      port: 443
  # Allow specific egress
  egress:
  - to:
    # Only allow traffic to database pods
    - podSelector:
        matchLabels:
          app: database
    # On specific ports
    ports:
    - protocol: TCP
      port: 5432
  # Allow DNS resolution
  - to:
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  1. Role-Based Access Control (RBAC)
# Create a limited role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: production
  name: app-reader
rules:
- apiGroups: [""]
  resources: ["pods", "services"]
  verbs: ["get", "list", "watch"]

---
# Create a service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service
  namespace: production

---
# Bind the role to the service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-service-binding
  namespace: production
subjects:
- kind: ServiceAccount
  name: app-service
  namespace: production
roleRef:
  kind: Role
  name: app-reader
  apiGroup: rbac.authorization.k8s.io
  1. Pod Security Standards
# Pod Security Policy (used via admission controllers)
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
    - ALL
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    rule: 'MustRunAsNonRoot'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      - min: 1
        max: 65535
  readOnlyRootFilesystem: true

Security Scanning for IaC

Terraform Scanning with Checkov

Checkov is an open-source static code analysis tool for scanning IaC files.

# Install Checkov
pip install checkov

# Scan a Terraform directory
checkov -d /path/to/terraform/files

# Scan a specific file
checkov -f main.tf

# Generate report in various formats
checkov -d /path/to/terraform/files --output json --output-file report.json

# Scan with specific checks enabled/disabled
checkov -d /path/to/terraform/files --check CKV_AWS_1,CKV_AWS_2

# Integrate with CI/CD (GitHub Actions example)

GitHub Actions workflow for Checkov scanning:

name: Terraform Security Scan

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  
jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: 3.8
    
    - name: Install Checkov
      run: pip install checkov
    
    - name: Run Checkov
      run: checkov -d . --output github_actions
    
    - name: Upload scan results
      if: success() || failure()
      uses: actions/upload-artifact@v2
      with:
        name: checkov-scan-results
        path: checkov-report.json

CloudFormation Scanning with cfn-nag

cfn-nag is a linting tool for CloudFormation templates.

# Install cfn-nag
gem install cfn-nag

# Scan a template
cfn_nag_scan --input-path template.yaml

# Scan a directory of templates
cfn_nag_scan --input-path /path/to/templates

# Output results in various formats
cfn_nag_scan --input-path template.yaml --output-format json --output-file results.json

# Create custom rules
# rules/my_rule.rb
require 'cfn-nag/violation'
require 'cfn-nag/custom_rules/base'

class MySecurityGroupRule < BaseRule
  def rule_text
    'Security group should not allow unrestricted access to ports'
  end

  def rule_type
    Violation::WARNING
  end

  def rule_id
    'W999'
  end

  def audit_impl(cfn_model)
    violations = []
    
    cfn_model.resources_by_type('AWS::EC2::SecurityGroup').each do |sg|
      sg.ingress.each do |ingress|
        if ingress.cidrIp == '0.0.0.0/0' && (ingress.fromPort == 22 || ingress.fromPort == 3389)
          violations << Violation.new(id: rule_id,
                                      type: rule_type,
                                      message: rule_text,
                                      logical_resource_ids: [sg.logical_resource_id])
        end
      end
    end
    
    violations
  end
end

Kubernetes Manifest Scanning with Kubesec

Kubesec is a security risk analysis tool for Kubernetes resources.

# Install Kubesec
docker pull kubesec/kubesec

# Scan a manifest
kubesec scan deployment.yaml

# Scan multiple manifests
kubesec scan pod.yaml deployment.yaml

# Integration with CI (example pipeline command)
docker run -i kubesec/kubesec:v2 scan /dev/stdin < deployment.yaml > results.json

Integrating IaC Security into CI/CD

GitHub Actions Workflow Example

name: IaC Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  terraform-security:
    name: Terraform Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.0.0

      - name: Terraform Init
        run: terraform init -backend=false
        working-directory: ./terraform

      - name: Terraform Validate
        run: terraform validate
        working-directory: ./terraform

      - name: Run tfsec
        uses: tfsec/tfsec-action@v1.0.0
        with:
          working_directory: ./terraform

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./terraform
          framework: terraform
          output_format: github_failed_only

  kubernetes-security:
    name: Kubernetes Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Lint Kubernetes manifests
        uses: azure/k8s-lint@v1
        with:
          manifests: |
            kubernetes/*.yaml

      - name: Kubesec Scan
        uses: controlplaneio/kubesec-action@master
        with:
          input: kubernetes/deployment.yaml
          format: json
          output: kubesec-results.json

      - name: Run Kube-Score
        run: |
          curl -L -o kube-score https://github.com/zegl/kube-score/releases/download/v1.10.0/kube-score_1.10.0_linux_amd64
          chmod +x kube-score
          ./kube-score score kubernetes/*.yaml > kube-score-results.txt

      - name: Upload scan results
        uses: actions/upload-artifact@v2
        with:
          name: security-scan-results
          path: |
            kubesec-results.json
            kube-score-results.txt

  cloudformation-security:
    name: CloudFormation Security Scan
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.7

      - name: Install cfn-nag
        run: gem install cfn-nag

      - name: Run cfn-nag
        run: |
          cfn_nag_scan --input-path cloudformation/ --template-pattern '.*\.yaml' --output-format json > cfn-nag-results.json

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Install CloudFormation Guard
        run: pip install cloudformation-guard

      - name: Run CloudFormation Guard
        run: |
          cfn-guard check -r guard-rules/*.guard -t cloudformation/*.yaml > cfn-guard-results.txt

      - name: Upload scan results
        uses: actions/upload-artifact@v2
        with:
          name: cfn-security-results
          path: |
            cfn-nag-results.json
            cfn-guard-results.txt

GitLab CI Pipeline Example

stages:
  - validate
  - scan
  - deploy

variables:
  TERRAFORM_VERSION: "1.0.0"

terraform-validate:
  stage: validate
  image: hashicorp/terraform:$TERRAFORM_VERSION
  script:
    - cd terraform/
    - terraform init -backend=false
    - terraform validate
  only:
    changes:
      - terraform/**/*

terraform-scan:
  stage: scan
  image: bridgecrew/checkov:latest
  script:
    - cd terraform/
    - checkov -d . --output cli --quiet
    - checkov -d . --output json > /tmp/checkov-report.json
  artifacts:
    paths:
      - /tmp/checkov-report.json
    when: always
  only:
    changes:
      - terraform/**/*

kubernetes-scan:
  stage: scan
  image: kubesec/kubesec:v2
  script:
    - mkdir -p reports
    - for file in kubernetes/*.yaml; do
        kubesec scan $file > reports/$(basename $file).json;
      done
  artifacts:
    paths:
      - reports/
    when: always
  only:
    changes:
      - kubernetes/**/*

cloudformation-scan:
  stage: scan
  image: ruby:2.7
  before_script:
    - gem install cfn-nag
  script:
    - mkdir -p reports
    - cfn_nag_scan --input-path cloudformation/ --output-format json > reports/cfn-nag-report.json
  artifacts:
    paths:
      - reports/
    when: always
  only:
    changes:
      - cloudformation/**/*

security-report:
  stage: deploy
  image: python:3.8
  script:
    - pip install jinja2 markdown
    - python scripts/generate_security_report.py
    - echo "Security report generated"
  artifacts:
    paths:
      - security-report.html
  needs:
    - terraform-scan
    - kubernetes-scan
    - cloudformation-scan
  only:
    - main
    - merge_requests

IaC Security Policies as Code

OPA (Open Policy Agent) Rego Policies

Rego is a policy language used by OPA to define policies for IaC validation.

# terraform_aws_security.rego
package terraform.aws.security

# Deny S3 buckets without encryption
deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    not resource.server_side_encryption_configuration
    msg := sprintf("S3 bucket '%v' does not have encryption enabled", [name])
}

# Deny security groups with unrestricted SSH access
deny[msg] {
    sg := input.resource.aws_security_group[name]
    ingress := sg.ingress[_]
    ingress.from_port <= 22
    ingress.to_port >= 22
    ingress.cidr_blocks[_] == "0.0.0.0/0"
    msg := sprintf("Security group '%v' allows SSH from the internet", [name])
}

# Deny IAM policies with full admin access
deny[msg] {
    policy := input.resource.aws_iam_policy[name]
    statement := policy.policy[_].Statement[_]
    statement.Effect == "Allow"
    statement.Action == "*"
    statement.Resource == "*"
    msg := sprintf("IAM policy '%v' grants full admin privileges", [name])
}

# Require encryption for EBS volumes
deny[msg] {
    volume := input.resource.aws_ebs_volume[name]
    not volume.encrypted
    msg := sprintf("EBS volume '%v' is not encrypted", [name])
}

# Ensure RDS instances are encrypted
deny[msg] {
    rds := input.resource.aws_db_instance[name]
    not rds.storage_encrypted
    msg := sprintf("RDS instance '%v' is not encrypted", [name])
}

Conftest for Policy Testing

Conftest is a utility for writing tests against structured configuration data.

# Install Conftest
brew install conftest

# Run policies against Terraform plan
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json -p policies/

# Run policies against Kubernetes manifests
conftest test kubernetes/ -p policies/

# Run policies against CloudFormation templates
conftest test cloudformation/ -p policies/

Example Conftest policy for Kubernetes:

# kubernetes_security.rego
package main

# Deny privileged containers
deny[msg] {
    input.kind == "Pod"
    container := input.spec.containers[_]
    container.securityContext.privileged == true
    msg := sprintf("Container '%v' is running as privileged", [container.name])
}

# Require resource limits
deny[msg] {
    input.kind == "Deployment"
    container := input.spec.template.spec.containers[_]
    not container.resources.limits
    msg := sprintf("Container '%v' does not have resource limits", [container.name])
}

# Require network policies in namespaces
warn[msg] {
    input.kind == "Namespace"
    name := input.metadata.name
    not data.network_policies[name]
    msg := sprintf("Namespace '%v' does not have a NetworkPolicy", [name])
}

# Deny use of latest tag
deny[msg] {
    input.kind == "Deployment"
    container := input.spec.template.spec.containers[_]
    endswith(container.image, ":latest")
    msg := sprintf("Container '%v' uses the 'latest' tag which is prohibited", [container.name])
}

Secure IaC Development Workflow

Git Hooks for Pre-commit Scanning

#!/bin/bash
# .git/hooks/pre-commit
# Make this file executable with: chmod +x .git/hooks/pre-commit

echo "Running security checks on staged IaC files..."

# Check for Terraform files
if git diff --cached --name-only | grep -q "\.tf$"; then
  echo "Checking Terraform files..."
  # Run terraform validate
  terraform validate || { echo "Terraform validation failed!"; exit 1; }
  
  # Run tfsec
  tfsec . || { echo "tfsec found security issues!"; exit 1; }
fi

# Check for Kubernetes manifests
if git diff --cached --name-only | grep -q "\.yaml$"; then
  echo "Checking Kubernetes manifests..."
  
  # Run kubeval
  for file in $(git diff --cached --name-only | grep "\.yaml$"); do
    kubeval "$file" || { echo "Kubernetes validation failed for $file!"; exit 1; }
  done
  
  # Run kubesec
  for file in $(git diff --cached --name-only | grep "\.yaml$"); do
    kubesec scan "$file" > /dev/null || { echo "Kubesec found security issues in $file!"; exit 1; }
  done
fi

# Check for CloudFormation templates
if git diff --cached --name-only | grep -q "\.(yaml|json)$"; then
  echo "Checking CloudFormation templates..."
  
  for file in $(git diff --cached --name-only | grep "\.(yaml|json)$"); do
    if grep -q "AWSTemplateFormatVersion" "$file"; then
      cfn-lint "$file" || { echo "CloudFormation linting failed for $file!"; exit 1; }
      cfn_nag_scan --input-path "$file" || { echo "cfn-nag found security issues in $file!"; exit 1; }
    fi
  done
fi

# Check for secrets
if git diff --cached --name-only | xargs grep -l ""; then
  echo "Checking for hardcoded secrets..."
  
  git diff --cached --name-only | xargs grep -l "AWS_SECRET_ACCESS_KEY\|password\|token\|secret" > /dev/null && {
    echo "Possible hardcoded secrets found in changes! Please review."
    exit 1
  }
fi

echo "All security checks passed!"
exit 0

Code Review Checklist for IaC

# IaC Security Code Review Checklist

## General Security
- [ ] No hardcoded secrets/credentials
- [ ] Resources follow naming conventions
- [ ] Tags/labels applied appropriately
- [ ] Documentation is up-to-date

## Access Control
- [ ] IAM roles/policies follow least privilege
- [ ] Authentication mechanisms are secure
- [ ] Authorization controls properly implemented
- [ ] No overly permissive policies (wildcards)

## Data Protection
- [ ] Encryption enabled for sensitive data
- [ ] Proper key management for secrets
- [ ] Sensitive data is not logged or exposed
- [ ] Backup and retention policies configured

## Network Security
- [ ] No unnecessary public exposure
- [ ] Network ACLs/security groups properly configured
- [ ] Traffic is appropriately restricted
- [ ] TLS/HTTPS enforced for communications

## Logging & Monitoring
- [ ] Appropriate audit logging enabled
- [ ] Monitoring and alerting configured
- [ ] Log storage and retention defined
- [ ] Access to logs appropriately restricted

## Compliance
- [ ] Required compliance controls implemented
- [ ] Necessary documentation for compliance included
- [ ] Regulatory requirements addressed

Additional Resources