RAPID CYBER AI
WEEK 2 OF 8
0 steps completed
WEEK 2

Terraform & IaC Security

Infrastructure as Code with security baked in from the start

8-10 hours

Lab 1: Terraform Fundamentals & State Security

~90 min // Remote state, encryption, locking

Install and verify Terraform

If not installed, refer to the Week 0 setup lab.

Verify
$ terraform version
# Should return v1.x.x

Initialize project structure

Create a new Terraform project with the AWS provider pinned to a stable version.

Project setup
$ mkdir -p ~/bootcamp-terraform && cd ~/bootcamp-terraform
$ cat > main.tf << 'EOF'
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}
EOF

$ terraform init

Create S3 backend for remote state

State files contain secrets in plaintext. Never store locally or in git.

Remote backend
$ ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
$ STATE_BUCKET="bootcamp-tf-state-${ACCOUNT_ID}"

$ aws s3api create-bucket --bucket $STATE_BUCKET --region us-east-1
$ aws s3api put-bucket-versioning --bucket $STATE_BUCKET \
    --versioning-configuration Status=Enabled
$ aws s3api put-bucket-encryption --bucket $STATE_BUCKET \
    --server-side-encryption-configuration \
    '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
$ aws s3api put-public-access-block --bucket $STATE_BUCKET \
    --public-access-block-configuration \
    "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Create DynamoDB table for state locking
$ aws dynamodb create-table \
    --table-name bootcamp-tf-locks \
    --attribute-definitions AttributeName=LockID,AttributeType=S \
    --key-schema AttributeName=LockID,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST
WHY THIS MATTERS

Terraform state contains every secret, every resource ID, every configuration. Encrypt it, lock it, restrict access to only the CI/CD pipeline role.

Configure backend in Terraform

Replace ACCOUNT_ID with your actual account ID. Run terraform init to migrate.

backend.tf
$ cat > backend.tf << 'EOF'
terraform {
  backend "s3" {
    bucket         = "bootcamp-tf-state-ACCOUNT_ID"
    key            = "bootcamp/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "bootcamp-tf-locks"
  }
}
EOF

# Migrate to remote backend
$ terraform init -migrate-state

Create variables with validation

Input validation blocks reject insecure inputs at plan time.

variables.tf
$ cat > variables.tf << 'EOF'
variable "environment" {
  type        = string
  description = "Environment name"
  validation {
    condition     = contains(["lab", "staging", "production"], var.environment)
    error_message = "Environment must be lab, staging, or production."
  }
}

variable "vpc_cidr" {
  type        = string
  default     = "10.0.0.0/16"
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid CIDR block."
  }
}
EOF

Lab 2: Secure Terraform Modules

~90 min // Opinionated modules with security defaults

Create module directory structure

Three modules with security baked in by default.

Structure
$ mkdir -p modules/{secure-s3-bucket,secure-vpc,secure-iam-role}
$ ls modules/

Build the secure-s3-bucket module

Encryption, versioning, and public access block are ON by default. Developers get a secure bucket without thinking about it.

modules/secure-s3-bucket/main.tf
$ cat > modules/secure-s3-bucket/main.tf << 'EOF'
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = var.tags
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket                  = aws_s3_bucket.this.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
EOF

Build the secure-vpc module

Default SG has zero rules (deny all). Flow logs enabled by default. Public subnets require explicit opt-in.

modules/secure-vpc/main.tf (excerpt)
$ cat > modules/secure-vpc/main.tf << 'EOF'
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = merge(var.tags, { Name = var.vpc_name })
}

resource "aws_default_security_group" "this" {
  vpc_id = aws_vpc.this.id
  # No rules = deny all on default SG
}

resource "aws_flow_log" "this" {
  vpc_id       = aws_vpc.this.id
  traffic_type = "REJECT"
  log_destination_type = "cloud-watch-logs"
  log_destination      = aws_cloudwatch_log_group.flow.arn
}
EOF

Test the modules

Verify the module creates resources with all security defaults applied.

Test
$ cat > test.tf << 'EOF'
module "test_bucket" {
  source      = "./modules/secure-s3-bucket"
  bucket_name = "test-secure-bucket-${random_id.suffix.hex}"
  tags        = { Environment = "lab" }
}

resource "random_id" "suffix" {
  byte_length = 4
}
EOF

$ terraform plan

Lab 3: IaC Security Scanning

~60 min // Checkov, tfsec, pre-commit hooks

Run Checkov against your code

Checkov scans for 750+ misconfigurations — unencrypted resources, public exposure, missing logging.

Checkov
$ pip3 install checkov --break-system-packages
$ checkov -d . --framework terraform

# Fix HIGH/CRITICAL findings, then re-run
$ checkov -d . --framework terraform --check HIGH,CRITICAL

Run tfsec

Second scanner catches issues Checkov misses, especially AWS-specific security group and IAM analysis.

tfsec
# Install
$ curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash

$ tfsec .

Set up pre-commit hooks

Insecure code is caught before it ever reaches your repository.

Pre-commit config
$ pip3 install pre-commit --break-system-packages

$ cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/antonbabenko/pre-commit-tf
    rev: v1.83.6
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
  - repo: https://github.com/bridgecrewio/checkov
    rev: 3.2.0
    hooks:
      - id: checkov
EOF

$ pre-commit install
$ pre-commit run --all-files

Lab 4: Terraform CI/CD Integration

~60 min // GitHub Actions pipeline with security gates

Create GitHub Actions workflow

Every PR is scanned before merge. Failures block the PR.

.github/workflows/terraform.yml
$ mkdir -p .github/workflows
$ cat > .github/workflows/terraform.yml << 'EOF'
name: Terraform Security Checks
on: [pull_request]

permissions:
  contents: read
  pull-requests: write

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
    - name: Terraform Init
      run: terraform init -backend=false
    - name: Terraform Validate
      run: terraform validate
    - name: Checkov Scan
      uses: bridgecrewio/checkov-action@v12
      with:
        directory: .
        framework: terraform
        soft_fail: false
    - name: tfsec Scan
      uses: aquasecurity/tfsec-action@v1
EOF

Configure OIDC federation

Replace long-lived access keys with short-lived OIDC tokens. No static credentials in GitHub Secrets.

OIDC trust
# Create OIDC provider for GitHub
$ aws iam create-open-id-connect-provider \
    --url https://token.actions.githubusercontent.com \
    --client-id-list sts.amazonaws.com \
    --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

# Create IAM role for GitHub Actions
# (trust policy scoped to your specific repo)

Lab 5: State Management & Drift Detection

~60 min // Drift detection, state ops, cleanup

Simulate and detect drift

When reality doesn't match state, Terraform tells you exactly what changed.

Drift detection
# Manually change a resource in console
# Then run:
$ terraform plan
# Terraform will show the discrepancy

# State inspection commands:
$ terraform state list
$ terraform state show aws_s3_bucket.test_bucket

Set up scheduled drift detection

Daily check alerts you when someone manually changes infrastructure.

Cron workflow
$ cat > .github/workflows/drift-check.yml << 'EOF'
name: Drift Detection
on:
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM
jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: hashicorp/setup-terraform@v3
    - run: terraform init
    - run: terraform plan -detailed-exitcode
EOF

Week 2 cleanup

Clean up lab resources. State bucket and DynamoDB table can remain for future weeks.

Cleanup
$ terraform destroy
# Review the plan, then type 'yes'

# Or destroy specific resources:
$ terraform destroy -target=module.test_bucket