Security of AI-Generated Infrastructure as Code (Terraform)
Security risks in AI-generated Terraform configurations including privilege escalation, network exposure, secret management failures, and compliance violations.
Overview
AI-generated Infrastructure as Code represents one of the highest-risk applications of code generation. Unlike application-level vulnerabilities that require exploitation through an attack chain, IaC misconfigurations take immediate effect when applied. An AI-generated Terraform configuration that opens a security group to 0.0.0.0/0 or creates an S3 bucket without encryption does not need to be exploited — it directly creates the vulnerable state.
LLMs generate insecure Terraform patterns for the same reasons they generate insecure application code: training data contains more insecure examples than secure ones, tutorials prioritize functionality over security, and the model lacks contextual understanding of the deployment environment. However, the consequences are amplified because IaC directly controls infrastructure access, network boundaries, and data protection.
Common AI-Generated Terraform Misconfigurations
Security Group and Network Exposure
The most frequent and dangerous pattern is overly permissive network configuration:
# Catalog of common AI-generated Terraform security issues
TERRAFORM_MISCONFIGURATIONS = {
"open_security_group": {
"severity": "critical",
"frequency": "very_common",
"description": "Security group allows all inbound traffic",
"insecure_terraform": '''
# AI generates this when asked to "create a web server"
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Security group for web server"
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # INSECURE: open to entire internet
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
''',
"secure_terraform": '''
# Secure: restrict to specific ports and source CIDRs
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Security group for web server"
vpc_id = var.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.allowed_cidrs
description = "HTTPS from approved networks"
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = var.allowed_cidrs
description = "HTTP from approved networks (redirect to HTTPS)"
}
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS outbound"
}
tags = {
ManagedBy = "terraform"
Security = "reviewed"
}
}
''',
"cwe": "CWE-284",
},
"public_s3_bucket": {
"severity": "critical",
"frequency": "common",
"description": "S3 bucket with public access enabled",
"insecure_terraform": '''
# AI generates this for "create an S3 bucket for file uploads"
resource "aws_s3_bucket" "uploads" {
bucket = "my-app-uploads"
}
# AI often omits these critical security configurations:
# - No public access block
# - No encryption
# - No versioning
# - No logging
''',
"secure_terraform": '''
# Secure S3 bucket configuration
resource "aws_s3_bucket" "uploads" {
bucket = "my-app-uploads-${data.aws_caller_identity.current.account_id}"
tags = {
ManagedBy = "terraform"
DataClass = "internal"
}
}
resource "aws_s3_bucket_public_access_block" "uploads" {
bucket = aws_s3_bucket.uploads.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = var.kms_key_id
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_versioning" "uploads" {
bucket = aws_s3_bucket.uploads.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_logging" "uploads" {
bucket = aws_s3_bucket.uploads.id
target_bucket = var.logging_bucket_id
target_prefix = "s3-access-logs/uploads/"
}
''',
"cwe": "CWE-732",
},
"hardcoded_secrets": {
"severity": "critical",
"frequency": "common",
"description": "Secrets hardcoded in Terraform configuration",
"insecure_terraform": '''
# AI generates hardcoded secrets when asked for "complete" examples
resource "aws_db_instance" "main" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
db_name = "myapp"
username = "admin"
password = "MyS3cur3P@ssw0rd!" # INSECURE: hardcoded secret
skip_final_snapshot = true # INSECURE: no final snapshot
publicly_accessible = true # INSECURE: public access
}
''',
"secure_terraform": '''
# Secure: use secrets manager and proper configuration
resource "aws_db_instance" "main" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
allocated_storage = 20
db_name = "myapp"
username = var.db_username
password = random_password.db_password.result
skip_final_snapshot = false
publicly_accessible = false
storage_encrypted = true
kms_key_id = var.kms_key_arn
vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.private.name
backup_retention_period = 7
multi_az = true
tags = {
ManagedBy = "terraform"
}
}
resource "random_password" "db_password" {
length = 32
special = true
}
resource "aws_secretsmanager_secret" "db_password" {
name = "myapp/db/password"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db_password.result
}
''',
"cwe": "CWE-798",
},
"overprivileged_iam": {
"severity": "critical",
"frequency": "very_common",
"description": "IAM role with overly broad permissions",
"insecure_terraform": '''
# AI generates this when asked for "IAM role for Lambda function"
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "*" # INSECURE: full admin access
Resource = "*"
}
]
})
}
''',
"secure_terraform": '''
# Secure: least-privilege IAM policy
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
]
Resource = [
aws_dynamodb_table.main.arn,
"${aws_dynamodb_table.main.arn}/index/*",
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
''',
"cwe": "CWE-250",
},
}Missing Encryption
LLMs frequently omit encryption configuration because it adds complexity without changing functionality:
# Resources where AI commonly omits encryption
ENCRYPTION_GAPS = [
{
"resource": "aws_ebs_volume",
"missing": "encrypted = true, kms_key_id",
"default_behavior": "Unencrypted unless account-level default is set",
"risk": "Data at rest exposure",
},
{
"resource": "aws_rds_instance",
"missing": "storage_encrypted = true, kms_key_id",
"default_behavior": "Unencrypted",
"risk": "Database data at rest exposure",
},
{
"resource": "aws_s3_bucket",
"missing": "aws_s3_bucket_server_side_encryption_configuration",
"default_behavior": "S3-SSE by default since Jan 2023, but KMS preferred",
"risk": "Insufficient encryption control",
},
{
"resource": "aws_sqs_queue",
"missing": "kms_master_key_id",
"default_behavior": "Unencrypted",
"risk": "Message data exposure",
},
{
"resource": "aws_sns_topic",
"missing": "kms_master_key_id",
"default_behavior": "Unencrypted",
"risk": "Notification data exposure",
},
{
"resource": "aws_elasticache_replication_group",
"missing": "at_rest_encryption_enabled, transit_encryption_enabled",
"default_behavior": "Unencrypted",
"risk": "Cache data exposure",
},
]Detection with IaC Security Tools
Checkov Analysis
#!/bin/bash
# Run Checkov to detect security issues in AI-generated Terraform
echo "=== Checkov IaC Security Scan ==="
TERRAFORM_DIR="${1:-.}"
if ! command -v checkov &>/dev/null; then
echo "Installing Checkov..."
pip install checkov
fi
# Run Checkov with all Terraform checks
checkov -d "$TERRAFORM_DIR" \
--framework terraform \
--output json \
--output-file /tmp/checkov-results.json \
2>/dev/null
# Analyze results focusing on AI-common patterns
python3 << 'PYTHON'
import json
with open("/tmp/checkov-results.json") as f:
data = json.load(f)
# Common AI-generated misconfigurations mapped to Checkov check IDs
AI_COMMON_CHECKS = {
"CKV_AWS_23": "Security group allows unrestricted ingress on port 22",
"CKV_AWS_24": "Security group allows unrestricted ingress on port 3389",
"CKV_AWS_25": "Security group allows unrestricted ingress",
"CKV_AWS_19": "S3 bucket not encrypted with KMS",
"CKV_AWS_18": "S3 bucket missing access logging",
"CKV_AWS_21": "S3 bucket missing versioning",
"CKV_AWS_41": "IAM policy allows full admin access",
"CKV_AWS_40": "IAM policy attached to users instead of groups",
"CKV_AWS_16": "RDS database not encrypted",
"CKV_AWS_17": "RDS has public access enabled",
"CKV_AWS_46": "KMS key rotation not enabled",
"CKV2_AWS_11": "VPC flow logs not enabled",
"CKV_AWS_145": "S3 bucket not encrypted with customer managed KMS",
}
failed = data.get("results", {}).get("failed_checks", [])
ai_related = [
f for f in failed
if f.get("check_id") in AI_COMMON_CHECKS
]
print(f"Total failed checks: {len(failed)}")
print(f"AI-common pattern failures: {len(ai_related)}")
print()
if ai_related:
print("AI-Generated Code Risk Findings:")
for f in ai_related:
check_id = f["check_id"]
print(f" {check_id}: {AI_COMMON_CHECKS.get(check_id, f['check_type'])}")
print(f" File: {f.get('file_path', 'unknown')}:{f.get('file_line_range', [])}")
print()
PYTHON
echo "=== Scan Complete ==="tfsec Analysis
#!/bin/bash
# tfsec scan focused on AI-generated Terraform patterns
echo "=== tfsec Security Scan ==="
TERRAFORM_DIR="${1:-.}"
if ! command -v tfsec &>/dev/null; then
echo "Install tfsec: https://github.com/aquasecurity/tfsec"
exit 1
fi
# Run tfsec
tfsec "$TERRAFORM_DIR" --format json > /tmp/tfsec-results.json 2>/dev/null
python3 << 'PYTHON'
import json
with open("/tmp/tfsec-results.json") as f:
data = json.load(f)
results = data.get("results", [])
if results is None:
results = []
# Categorize by severity
by_severity = {}
for r in results:
sev = r.get("severity", "unknown")
by_severity.setdefault(sev, []).append(r)
print(f"Total findings: {len(results)}")
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
items = by_severity.get(sev, [])
print(f" {sev}: {len(items)}")
# Highlight AI-common patterns
print()
print("Findings commonly introduced by AI code generation:")
ai_keywords = ["unrestricted", "public", "unencrypted", "wildcard", "hardcoded"]
for r in results:
desc = r.get("description", "").lower()
if any(kw in desc for kw in ai_keywords):
print(f" [{r.get('severity')}] {r.get('rule_description', '')}")
print(f" {r.get('location', {}).get('filename', 'unknown')}:{r.get('location', {}).get('start_line', '?')}")
PYTHONPolicy-as-Code Guardrails
OPA/Rego Policies
Open Policy Agent (OPA) can enforce policies that prevent AI-generated misconfigurations:
# OPA/Rego policies for AI-generated Terraform
OPA_POLICIES = """
# deny_public_security_groups.rego
package terraform.security
deny[msg] {
resource := input.resource.aws_security_group[name]
ingress := resource.ingress[_]
ingress.cidr_blocks[_] == "0.0.0.0/0"
msg := sprintf(
"Security group '%s' allows unrestricted ingress from 0.0.0.0/0. "
"This is a common AI-generated misconfiguration.",
[name]
)
}
# deny_unencrypted_storage.rego
deny[msg] {
resource := input.resource.aws_s3_bucket[name]
not has_encryption(name)
msg := sprintf(
"S3 bucket '%s' missing server-side encryption configuration. "
"AI tools frequently omit encryption for simplicity.",
[name]
)
}
has_encryption(bucket_name) {
input.resource.aws_s3_bucket_server_side_encryption_configuration[_].bucket == bucket_name
}
# deny_admin_iam.rego
deny[msg] {
resource := input.resource.aws_iam_role_policy[name]
policy := json.unmarshal(resource.policy)
statement := policy.Statement[_]
statement.Effect == "Allow"
statement.Action[_] == "*"
statement.Resource[_] == "*"
msg := sprintf(
"IAM policy '%s' grants full admin access (Action:* Resource:*). "
"AI assistants default to broad permissions.",
[name]
)
}
# deny_hardcoded_secrets.rego
deny[msg] {
resource := input.resource.aws_db_instance[name]
resource.password
not startswith(resource.password, "var.")
not startswith(resource.password, "random_password.")
msg := sprintf(
"RDS instance '%s' has a hardcoded password. "
"Use aws_secretsmanager_secret or random_password.",
[name]
)
}
"""Terraform Sentinel Policies
# Sentinel policy examples for AI-generated Terraform
SENTINEL_POLICIES = '''
# Prevent public S3 buckets
policy "no-public-s3" {
enforcement_level = "hard-mandatory"
description = "Prevents AI-generated public S3 bucket configurations"
}
# Require encryption on all storage
policy "require-encryption" {
enforcement_level = "hard-mandatory"
description = "Ensures AI-generated resources include encryption"
}
# Restrict security group CIDR blocks
policy "restrict-security-groups" {
enforcement_level = "hard-mandatory"
description = "Prevents 0.0.0.0/0 ingress rules from AI-generated configs"
}
'''Secure Terraform Generation Workflow
# Workflow for safely using AI-generated Terraform
SECURE_WORKFLOW = {
"step_1_generate": {
"action": "Generate Terraform with AI tool",
"guidance": [
"Include security requirements in your prompt",
"Specify encryption, access controls, and logging explicitly",
"Request least-privilege IAM policies",
],
},
"step_2_review": {
"action": "Human review of generated configuration",
"checklist": [
"No hardcoded secrets or credentials",
"No 0.0.0.0/0 CIDR blocks in security groups",
"Encryption enabled for all data stores",
"IAM follows least privilege",
"Logging and monitoring configured",
"Resources are in private subnets where appropriate",
],
},
"step_3_scan": {
"action": "Automated security scanning",
"tools": ["checkov", "tfsec", "terrascan", "OPA/conftest"],
},
"step_4_plan": {
"action": "terraform plan review",
"checks": [
"Review plan output for unexpected resources",
"Check for destructive changes",
"Verify no new public access is created",
],
},
"step_5_apply": {
"action": "terraform apply with approval",
"controls": [
"Require manual approval for production",
"Use CI/CD pipeline with policy gates",
"Log all apply operations",
],
},
}References
- Checkov — IaC static analysis — https://www.checkov.io/
- tfsec — Terraform security scanner — https://aquasecurity.github.io/tfsec/
- Open Policy Agent — Policy-as-code framework — https://www.openpolicyagent.org/
- CWE-250: Execution with Unnecessary Privileges — https://cwe.mitre.org/data/definitions/250.html
- CWE-732: Incorrect Permission Assignment for Critical Resource — https://cwe.mitre.org/data/definitions/732.html
- AWS Security Best Practices — https://docs.aws.amazon.com/security/
- OWASP Top 10 for LLM Applications 2025 — LLM02: Insecure Output Handling — https://genai.owasp.org/llmrisk/
- MITRE ATLAS — AML.T0048: Deploy Backdoor — https://atlas.mitre.org/