JustAppSec

Infrastructure as Code

Terraform, Pulumi, CloudFormation — and the security foot-guns in declarative infra.

0:00

Infrastructure as Code (IaC) lets you define servers, networks, databases, and security controls in version-controlled configuration files. This is a security advantage — infrastructure becomes auditable, repeatable, and reviewable. It is also a risk — a misconfiguration in a template can expose your entire cloud environment.

Why IaC matters for security

Before IaC, infrastructure was configured manually through cloud consoles. This meant:

  • No audit trail of who changed what
  • No code review before changes went live
  • Inconsistency between environments (staging and production drifted)
  • Security configurations that were forgotten or overridden

IaC solves these problems by treating infrastructure like application code: written, reviewed, tested, and deployed through the same CI/CD pipeline.

Common IaC tools

ToolLanguageCloud support
Terraform / OpenTofuHCLMulti-cloud (AWS, GCP, Azure, etc.)
AWS CloudFormationJSON/YAMLAWS only
PulumiTypeScript, Python, Go, C#Multi-cloud
AWS CDKTypeScript, Python, Java, C#AWS only (generates CloudFormation)
Azure BicepBicep DSLAzure only
Google Cloud Deployment ManagerYAML/Jinja2GCP only

The security principles are the same regardless of the tool.

Common security misconfigurations

Public S3 buckets

The most common and most publicised IaC mistake. A single acl = "public-read" makes every object in the bucket accessible to the internet:

# VULNERABLE
resource "aws_s3_bucket_acl" "data" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read"
}

# SECURE — block all public access
resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id

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

Overly permissive security groups

A security group that allows 0.0.0.0/0 on all ports is essentially no firewall:

# VULNERABLE — open to the internet
resource "aws_security_group_rule" "allow_all" {
  type        = "ingress"
  from_port   = 0
  to_port     = 65535
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

# SECURE — restrict to specific ports and sources
resource "aws_security_group_rule" "allow_https" {
  type        = "ingress"
  from_port   = 443
  to_port     = 443
  protocol    = "tcp"
  cidr_blocks = ["10.0.0.0/8"]  # Internal network only
}

IAM wildcards

Wildcard permissions grant access to everything:

{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}

This is the cloud equivalent of running everything as root. Use specific actions and resources:

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::my-bucket/*"
}

Unencrypted resources

Databases, storage, and queues should be encrypted at rest:

resource "aws_db_instance" "main" {
  storage_encrypted = true
  # ...
}

resource "aws_sqs_queue" "orders" {
  kms_master_key_id = aws_kms_key.main.id
}

Logging disabled

CloudTrail, VPC Flow Logs, and access logging should be enabled by default:

resource "aws_cloudtrail" "main" {
  name           = "main-trail"
  s3_bucket_name = aws_s3_bucket.logs.id
  is_multi_region_trail = true
  enable_logging = true
}

Scanning IaC for misconfigurations

Static analysis tools catch security issues before deployment:

# Checkov (supports Terraform, CloudFormation, Kubernetes, Dockerfiles)
checkov -d .

# tfsec (Terraform-specific)
tfsec .

# KICS (Keeping Infrastructure as Code Secure)
kics scan -p .

Integrate these into your CI pipeline:

- name: Scan Terraform
  run: checkov -d infrastructure/ --framework terraform --soft-fail-on LOW

Policy as code

For custom rules, use tools like Open Policy Agent (OPA) or Sentinel (HashiCorp):

# OPA policy — deny public S3 buckets
deny[msg] {
  resource := input.resource.aws_s3_bucket[name]
  resource.acl == "public-read"
  msg := sprintf("S3 bucket '%s' must not be public", [name])
}

State file security

Terraform and similar tools maintain a state file that maps your configuration to real cloud resources. The state file contains:

  • Resource IDs and configurations
  • Output values (which may include secrets)
  • Metadata about your infrastructure

Protect the state file

  • Never commit state files to Git. They often contain secrets.
  • Use remote state backends with encryption and access control (S3 + DynamoDB locking, GCS, Terraform Cloud).
  • Enable state file encryption at rest.
  • Restrict access to the state backend. Only the CI pipeline and authorised operators should have access.
terraform {
  backend "s3" {
    bucket         = "myapp-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

Drift detection

Over time, manual changes in the cloud console cause the actual infrastructure to drift from the IaC definition. This drift can introduce security misconfigurations that your IaC scans will not catch (because the misconfiguration is not in the code).

Detect drift regularly:

terraform plan  # Shows differences between state and actual infrastructure

Automate drift detection and alert when changes are detected. Then fix the drift by either updating the IaC or reverting the manual change.

Modules and reuse

Encapsulate security best practices in reusable modules:

module "secure_bucket" {
  source = "./modules/secure-s3-bucket"
  bucket_name = "my-data"
}

The module enforces encryption, public access blocks, logging, and versioning. Teams use the module instead of configuring S3 from scratch, ensuring consistent security.

Summary

IaC makes infrastructure auditable and repeatable, but it also codifies misconfigurations. The most common mistakes — public storage, permissive security groups, wildcard IAM, unencrypted resources, and disabled logging — can all be caught by scanning tools like Checkov and tfsec. Run them in CI on every pull request. Protect state files with remote backends and encryption. Detect drift regularly. Encapsulate security best practices in reusable modules. The goal is infrastructure that is secure by default, not secure by vigilance.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].