Infrastructure as Code Security
Introduction
Infrastructure as Code (IaC) enables automated, repeatable infrastructure provisioning. However, IaC also codifies security misconfigurations — a mistake in a Terraform file can propagate to thousands of resources. Securing IaC means scanning for issues before deployment, enforcing policy as code, and preventing configuration drift.
Terraform Security Scanning
Scan Terraform configurations for security misconfigurations before applying them.
checkov
# Basic scan
checkov -d terraform/environments/production/
# Scan with specific framework
checkov -d . --framework terraform --skip-framework dockerfile
# Output in multiple formats
checkov -d . -o json | jq '.results.failed_checks[] | {resource: .resource, check: .check_id, severity: .severity}'
# Run in CI/CD with threshold
checkov -d . --soft-fail-on MEDIUM # Fail only on HIGH/CRITICAL
# Custom Checkov policy for Terraform
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult
class RDSEncryptionCheck(BaseResourceCheck):
def __init__(self):
name = "Ensure RDS instances have encryption enabled"
id = "CKV_CUSTOM_002"
supported_resources = ['aws_db_instance']
super().__init__(name=name, id=id, supported_resources=supported_resources)
def scan_resource_conf(self, conf):
# Check if storage_encrypted is set to true
if conf.get('storage_encrypted') == [True]:
return CheckResult.PASSED
return CheckResult.FAILED
check = RDSEncryptionCheck()
tfsec
# Basic scan
tfsec terraform/
# Scan with custom configuration
tfsec . --config-file tfsec.yaml
# Generate SARIF output for GitHub Code Scanning
tfsec . --format sarif --out tfsec-results.sarif
# Ignore specific checks
tfsec . --exclude aws-s3-enable-bucket-logging,aws-s3-specify-public-access-block
# tfsec.yaml
exclude:
- aws-s3-enable-bucket-logging
include:
- aws-*
severity_threshold: HIGH
custom_checks:
- name: custom-restrict-ssh
description: Security groups should not allow SSH from 0.0.0.0/0
impact: Open SSH access to the internet
severity: CRITICAL
match:
- resource_type: aws_security_group
attribute_path: ingress
condition: >
any(ingress,
i.from_port == 22 &&
any(i.cidr_blocks, c == "0.0.0.0/0")
)
Policy as Code with Open Policy Agent
OPA allows you to write policies that enforce infrastructure standards across your IaC.
# OPA policy for Terraform
package terraform.analysis
# All resources must have tags
deny[msg] {
resource := input.resource[name][_]
not resource.tags
msg = sprintf("%v %v must have tags", [name, resource.type])
}
# S3 buckets must be private
deny[msg] {
resource := input.resource.aws_s3_bucket[name]
resource.acl == "public-read"
msg = sprintf("S3 bucket %v must not be public", [name])
}
# Security groups must not have wide open ingress
deny[msg] {
sg := input.resource.aws_security_group[name]
rule := sg.ingress[_]
rule.cidr_blocks[_] == "0.0.0.0/0"
rule.from_port == 22
msg = sprintf("Security group %v allows SSH from anywhere", [name])
}
# Evaluate OPA policy against Terraform plan
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
opa eval --data policies/ --input plan.json "data.terraform.analysis.deny"
Sentinel (HashiCorp)
Sentinel is HashiCorp's policy-as-code framework for Terraform Enterprise/Cloud.
# sentinel.hcl
policy "restrict-instance-type" {
source = "./restrict-instance-type.sentinel"
enforcement_level = "hard-mandatory"
}
policy "require-encryption" {
source = "./require-encryption.sentinel"
enforcement_level = "soft-mandatory"
}
# restrict-instance-type.sentinel
import "tfplan/v2" as tfplan
allowed_instance_types = [
"t3.medium",
"t3.large",
"m5.large",
]
# Get all EC2 instances
instances = filter tfplan.resource_changes as _, rc {
rc.mode is "managed" and
rc.type is "aws_instance" and
rc.change.actions contains "create"
}
# Check each instance
for _, instance in instances {
instance_type = instance.change.after.instance_type
if instance_type not in allowed_instance_types {
print("Instance type", instance_type, "not allowed")
return false
}
}
return true
CI/CD Integration
# GitLab CI pipeline for IaC security
stages:
- validate
- security
- plan
- apply
terraform-validate:
stage: validate
script:
- terraform init
- terraform fmt -check
- terraform validate
iac-security-scan:
stage: security
script:
- checkov -d . --framework terraform --skip-check CKV_AWS_126
- tfsec . --no-color
- opa eval --data policies/ --input plan.json "data.terraform.analysis.deny"
artifacts:
reports:
checkov: checkov-report.json
terraform-plan:
stage: plan
script:
- terraform plan -out=plan.tfplan
artifacts:
paths:
- plan.tfplan
only:
- merge_requests
terraform-apply:
stage: apply
script:
- terraform apply plan.tfplan
when: manual
only:
- main
Drift Detection
# Terraform drift detection with AWS Config
resource "aws_config_config_rule" "required_tags" {
name = "required-tags"
source {
owner = "AWS"
source_identifier = "REQUIRED_TAGS"
}
input_parameters = jsonencode({
tag1Key = "Environment"
tag2Key = "Owner"
})
}
# Automated drift detection
#!/bin/bash
aws configservice get-compliance-details-by-config-rule \
--config-rule-name required-tags \
--compliance-types NON_COMPLIANT \
--query 'EvaluationResults[].EvaluationResultIdentifier.EvaluationResultQualifier[].ResourceId' \
--output json
Conclusion
IaC security shifts security left by catching misconfigurations before infrastructure is provisioned. Use tools like Checkov and tfsec for automated scanning, OPA or Sentinel for policy enforcement, integrate scans into CI/CD pipelines, and continuously monitor for drift in deployed infrastructure. Remember that IaC security is an ongoing practice — new resources, providers, and attack patterns require continuous policy updates.