Opening Framing
Infrastructure as Code (IaC) transforms cloud infrastructure from manually configured resources into version-controlled, reviewable, and repeatable definitions. This shift fundamentally changes security: instead of auditing running infrastructure, you can analyze the code that creates it. Misconfigurations can be caught before deployment, security policies can be enforced through automated checks, and consistent security baselines can be guaranteed across all environments.
But IaC also introduces new risks. A single misconfigured template can create hundreds of insecure resources. Secrets accidentally committed to repositories are exposed in version history forever. And the speed of IaC deployment means insecure infrastructure can be created faster than ever. Security must be built into the IaC pipeline, not bolted on afterward.
This week covers IaC security fundamentals, static analysis tools, policy as code with OPA and Sentinel, secrets management in IaC, and secure CI/CD pipelines for infrastructure deployment. You'll learn to shift security left and catch issues before they reach production.
Key insight: IaC enables "security as code"—automated, consistent, and reviewable security controls.
1) Infrastructure as Code Fundamentals
Understanding IaC tools and their security implications is essential for securing cloud infrastructure:
IaC Tools Landscape:
DECLARATIVE vs IMPERATIVE:
┌─────────────────────────────────────────────────────────────┐
│ DECLARATIVE (What you want): │
│ - Terraform, CloudFormation, Pulumi │
│ - Define desired end state │
│ - Tool figures out how to get there │
│ - Easier to audit and review │
│ │
│ IMPERATIVE (How to do it): │
│ - AWS CDK, Ansible, Scripts │
│ - Define steps to execute │
│ - More flexibility, more complexity │
│ - Harder to predict final state │
└─────────────────────────────────────────────────────────────┘
MAJOR IaC TOOLS:
┌─────────────────────────────────────────────────────────────┐
│ Terraform: │
│ - Multi-cloud support │
│ - HCL (HashiCorp Configuration Language) │
│ - State file tracks resources │
│ - Large provider ecosystem │
│ │
│ AWS CloudFormation: │
│ - AWS-native │
│ - YAML/JSON templates │
│ - Integrated with AWS services │
│ - Stack-based resource management │
│ │
│ AWS CDK: │
│ - Infrastructure in programming languages │
│ - TypeScript, Python, Java, etc. │
│ - Synthesizes to CloudFormation │
│ - Higher-level constructs │
│ │
│ Pulumi: │
│ - Multi-cloud │
│ - Real programming languages │
│ - State management options │
└─────────────────────────────────────────────────────────────┘
IaC Security Benefits and Risks:
IaC Security Implications:
SECURITY BENEFITS:
┌─────────────────────────────────────────────────────────────┐
│ ✓ Version Control │
│ - Track all infrastructure changes │
│ - Audit trail of who changed what │
│ - Rollback capability │
│ │
│ ✓ Code Review │
│ - Security review before deployment │
│ - Multiple eyes on changes │
│ - Catch misconfigurations early │
│ │
│ ✓ Consistency │
│ - Same code = same infrastructure │
│ - Eliminate configuration drift │
│ - Reproducible environments │
│ │
│ ✓ Automated Testing │
│ - Static analysis in CI/CD │
│ - Policy enforcement │
│ - Compliance checking │
│ │
│ ✓ Documentation │
│ - Code IS documentation │
│ - Always current │
│ - Searchable and reviewable │
└─────────────────────────────────────────────────────────────┘
SECURITY RISKS:
┌─────────────────────────────────────────────────────────────┐
│ ✗ Secrets in Code │
│ - Credentials committed to repositories │
│ - Visible in version history forever │
│ - Exposed in state files │
│ │
│ ✗ Overprivileged Deployment │
│ - CI/CD needs broad permissions │
│ - Compromised pipeline = infrastructure compromise │
│ │
│ ✗ State File Exposure │
│ - Terraform state contains sensitive data │
│ - Must be secured and encrypted │
│ │
│ ✗ Rapid Misconfiguration Deployment │
│ - Bad template = many bad resources fast │
│ - Automation amplifies mistakes │
│ │
│ ✗ Third-Party Module Risks │
│ - Community modules may be insecure │
│ - Supply chain attacks possible │
└─────────────────────────────────────────────────────────────┘
COMMON IaC MISCONFIGURATIONS:
┌─────────────────────────────────────────────────────────────┐
│ - S3 buckets with public access │
│ - Security groups with 0.0.0.0/0 │
│ - Unencrypted storage (EBS, RDS, S3) │
│ - Overprivileged IAM roles │
│ - Disabled logging │
│ - Missing tags │
│ - Default VPC usage │
│ - Hardcoded secrets │
│ - Missing resource limits │
│ - Exposed management ports │
└─────────────────────────────────────────────────────────────┘
Key insight: IaC transforms security from reactive (fixing running infrastructure) to proactive (preventing bad deployments).
2) IaC Static Analysis Tools
Static analysis tools scan IaC templates for security issues before deployment:
IaC Scanning Tools:
CHECKOV (Open Source):
┌─────────────────────────────────────────────────────────────┐
│ Supports: Terraform, CloudFormation, Kubernetes, ARM, CDK │
│ │
│ Installation: │
│ pip install checkov │
│ │
│ Usage: │
│ checkov -d . # Scan directory │
│ checkov -f main.tf # Scan file │
│ checkov --framework terraform # Specific framework │
│ checkov --check CKV_AWS_1 # Run specific check │
│ checkov --skip-check CKV_AWS_2 # Skip check │
│ │
│ Output: │
│ Passed checks: 45 │
│ Failed checks: 3 │
│ Skipped checks: 2 │
│ │
│ Check: CKV_AWS_19: "Ensure S3 bucket has encryption" │
│ FAILED for resource: aws_s3_bucket.data │
│ File: /main.tf:15-20 │
└─────────────────────────────────────────────────────────────┘
TFSEC (Terraform Security):
┌─────────────────────────────────────────────────────────────┐
│ Installation: │
│ brew install tfsec │
│ │
│ Usage: │
│ tfsec . # Scan directory │
│ tfsec --format json # JSON output │
│ tfsec --minimum-severity HIGH # Filter by severity │
│ │
│ Output: │
│ Result #1 HIGH S3 bucket has encryption disabled │
│ ───────────────────────────────────────────────────────── │
│ main.tf:15-20 │
│ 15 │ resource "aws_s3_bucket" "data" { │
│ 16 │ bucket = "my-data-bucket" │
│ 17 │ } │
│ │
│ Impact: The bucket objects could be read if accessed │
│ Resolution: Enable server-side encryption │
└─────────────────────────────────────────────────────────────┘
TRIVY (Multi-Purpose):
┌─────────────────────────────────────────────────────────────┐
│ Supports: Terraform, CloudFormation, Kubernetes, Dockerfile │
│ │
│ Usage: │
│ trivy config . # Scan IaC │
│ trivy config --severity HIGH,CRITICAL . │
│ │
│ Also scans: │
│ - Container images │
│ - Filesystems │
│ - Git repositories │
└─────────────────────────────────────────────────────────────┘
AWS CLOUDFORMATION GUARD:
┌─────────────────────────────────────────────────────────────┐
│ Policy-as-code for CloudFormation │
│ │
│ Installation: │
│ cargo install cfn-guard │
│ │
│ Usage: │
│ cfn-guard validate -d template.yaml -r rules.guard │
│ │
│ Example Rule (rules.guard): │
│ let s3_buckets = Resources.*[Type == 'AWS::S3::Bucket'] │
│ │
│ rule s3_bucket_encryption when %s3_buckets !empty { │
│ %s3_buckets.Properties.BucketEncryption exists │
│ %s3_buckets.Properties.BucketEncryption. │
│ ServerSideEncryptionConfiguration[*]. │
│ ServerSideEncryptionByDefault.SSEAlgorithm in │
│ ['AES256', 'aws:kms'] │
│ } │
└─────────────────────────────────────────────────────────────┘
CI/CD Integration:
CI/CD Pipeline Integration:
GITHUB ACTIONS EXAMPLE:
name: IaC Security Scan
on:
pull_request:
paths:
- 'terraform/**'
- 'cloudformation/**'
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
framework: terraform
soft_fail: false
output_format: sarif
output_file_path: checkov-results.sarif
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: checkov-results.sarif
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: false
- name: Run Trivy
uses: aquasecurity/trivy-action@master
with:
scan-type: 'config'
scan-ref: 'terraform/'
severity: 'HIGH,CRITICAL'
exit-code: '1'
GITLAB CI EXAMPLE:
stages:
- validate
- security
- plan
- apply
security-scan:
stage: security
image: bridgecrew/checkov:latest
script:
- checkov -d terraform/ --output junitxml > checkov-results.xml
artifacts:
reports:
junit: checkov-results.xml
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
terraform-plan:
stage: plan
needs: [security-scan]
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths:
- tfplan
FAIL ON CRITICAL FINDINGS:
┌─────────────────────────────────────────────────────────────┐
│ Policy: Block PR merge if critical findings │
│ │
│ checkov --hard-fail-on HIGH,CRITICAL │
│ tfsec --minimum-severity HIGH --soft-fail=false │
│ trivy config --exit-code 1 --severity HIGH,CRITICAL │
│ │
│ This prevents insecure code from reaching main branch │
└─────────────────────────────────────────────────────────────┘
Key insight: Scan on every PR. Finding a misconfiguration in code review is infinitely better than finding it in production.
3) Policy as Code
Policy as Code enables defining and enforcing security policies programmatically:
Policy as Code Concepts:
WHY POLICY AS CODE:
┌─────────────────────────────────────────────────────────────┐
│ Traditional Policy: │
│ - Written in documents │
│ - Manually enforced │
│ - Inconsistently applied │
│ - Hard to verify compliance │
│ │
│ Policy as Code: │
│ - Written in code │
│ - Automatically enforced │
│ - Consistently applied │
│ - Compliance verified automatically │
│ │
│ Example Policy: │
│ "All S3 buckets must be encrypted" │
│ │
│ Document: Security team reviews each bucket manually │
│ Code: Every deployment automatically checked and blocked │
└─────────────────────────────────────────────────────────────┘
OPEN POLICY AGENT (OPA):
┌─────────────────────────────────────────────────────────────┐
│ General-purpose policy engine │
│ Uses Rego policy language │
│ Works with Terraform, Kubernetes, APIs │
│ │
│ Installation: │
│ brew install opa │
│ │
│ Rego Policy Example: │
│ package terraform.s3 │
│ │
│ # Deny S3 buckets without encryption │
│ deny[msg] { │
│ resource := input.resource_changes[_] │
│ resource.type == "aws_s3_bucket" │
│ not bucket_encrypted(resource) │
│ msg := sprintf( │
│ "S3 bucket %s must have encryption enabled", │
│ [resource.name] │
│ ) │
│ } │
│ │
│ bucket_encrypted(resource) { │
│ resource.change.after.server_side_encryption_configuration│
│ } │
│ │
│ # Deny public S3 buckets │
│ deny[msg] { │
│ resource := input.resource_changes[_] │
│ resource.type == "aws_s3_bucket_public_access_block" │
│ resource.change.after.block_public_acls == false │
│ msg := sprintf( │
│ "S3 bucket %s must block public ACLs", │
│ [resource.name] │
│ ) │
│ } │
└─────────────────────────────────────────────────────────────┘
USING OPA WITH TERRAFORM:
┌─────────────────────────────────────────────────────────────┐
│ # Generate Terraform plan as JSON │
│ terraform plan -out=tfplan │
│ terraform show -json tfplan > tfplan.json │
│ │
│ # Evaluate against policies │
│ opa eval --input tfplan.json \ │
│ --data policies/ \ │
│ "data.terraform.deny" │
│ │
│ # Conftest (OPA wrapper for config files) │
│ conftest test tfplan.json --policy policies/ │
│ │
│ FAIL - tfplan.json - S3 bucket data must have encryption │
│ FAIL - tfplan.json - Security group allows 0.0.0.0/0 │
│ │
│ 2 tests, 0 passed, 2 warnings, 2 failures │
└─────────────────────────────────────────────────────────────┘
HashiCorp Sentinel:
HashiCorp Sentinel (Terraform Cloud/Enterprise):
SENTINEL POLICY EXAMPLE:
┌─────────────────────────────────────────────────────────────┐
│ import "tfplan/v2" as tfplan │
│ │
│ # Get all S3 buckets │
│ s3_buckets = filter tfplan.resource_changes as _, rc { │
│ rc.type is "aws_s3_bucket" and │
│ rc.mode is "managed" and │
│ (rc.change.actions contains "create" or │
│ rc.change.actions contains "update") │
│ } │
│ │
│ # Check encryption │
│ bucket_encryption = rule { │
│ all s3_buckets as _, bucket { │
│ bucket.change.after.server_side_encryption_configuration│
│ is not null │
│ } │
│ } │
│ │
│ # Check versioning │
│ bucket_versioning = rule { │
│ all s3_buckets as _, bucket { │
│ bucket.change.after.versioning[0].enabled is true │
│ } │
│ } │
│ │
│ # Main rule │
│ main = rule { │
│ bucket_encryption and bucket_versioning │
│ } │
└─────────────────────────────────────────────────────────────┘
POLICY ENFORCEMENT LEVELS:
┌─────────────────────────────────────────────────────────────┐
│ Advisory: │
│ - Logged but not enforced │
│ - For informational policies │
│ │
│ Soft Mandatory: │
│ - Can be overridden by authorized users │
│ - For policies with exceptions │
│ │
│ Hard Mandatory: │
│ - Cannot be overridden │
│ - For critical security policies │
│ - Blocks non-compliant deployments │
└─────────────────────────────────────────────────────────────┘
AWS SERVICE CONTROL POLICIES (SCPs):
┌─────────────────────────────────────────────────────────────┐
│ Organization-level guardrails │
│ Enforced regardless of IaC tool │
│ │
│ Example: Deny unencrypted S3 │
│ { │
│ "Version": "2012-10-17", │
│ "Statement": [{ │
│ "Effect": "Deny", │
│ "Action": "s3:PutObject", │
│ "Resource": "*", │
│ "Condition": { │
│ "StringNotEquals": { │
│ "s3:x-amz-server-side-encryption": "AES256" │
│ } │
│ } │
│ }] │
│ } │
│ │
│ Note: SCPs are a backstop, not a replacement for IaC checks │
└─────────────────────────────────────────────────────────────┘
Key insight: Policies as code enable consistent enforcement across all deployments without human intervention.
4) Secrets Management in IaC
Secrets in IaC require special handling to prevent exposure:
Secrets in IaC Risks:
COMMON MISTAKES:
┌─────────────────────────────────────────────────────────────┐
│ BAD: Hardcoded secrets in Terraform │
│ │
│ resource "aws_db_instance" "default" { │
│ engine = "mysql" │
│ username = "admin" │
│ password = "SuperSecret123!" # EXPOSED! │
│ } │
│ │
│ Problems: │
│ - Visible in version control │
│ - Visible in Terraform state │
│ - Visible in CI/CD logs │
│ - Cannot be rotated without code change │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BAD: Secrets in environment variables │
│ │
│ export TF_VAR_db_password="SuperSecret123!" │
│ terraform apply │
│ │
│ Problems: │
│ - Still in shell history │
│ - May be logged in CI/CD │
│ - Still ends up in state file │
└─────────────────────────────────────────────────────────────┘
SECRETS IN STATE FILES:
┌─────────────────────────────────────────────────────────────┐
│ Terraform state contains ALL resource attributes │
│ Including sensitive values in plaintext! │
│ │
│ "resources": [{ │
│ "type": "aws_db_instance", │
│ "instances": [{ │
│ "attributes": { │
│ "password": "SuperSecret123!" # In state! │
│ } │
│ }] │
│ }] │
│ │
│ Must protect state file: │
│ - Encrypt at rest │
│ - Restrict access │
│ - Use remote backend (not local file) │
└─────────────────────────────────────────────────────────────┘
Secure Secrets Handling:
Secure Patterns:
PATTERN 1: Reference Secrets Manager
┌─────────────────────────────────────────────────────────────┐
│ # Secret created outside Terraform (or in separate state) │
│ │
│ data "aws_secretsmanager_secret_version" "db_password" { │
│ secret_id = "prod/database/password" │
│ } │
│ │
│ resource "aws_db_instance" "default" { │
│ engine = "mysql" │
│ username = "admin" │
│ password = data.aws_secretsmanager_secret_version. │
│ db_password.secret_string │
│ } │
│ │
│ Benefits: │
│ - Secret not in code │
│ - Secret still in state (limitation) │
│ - Centralized secret management │
│ - Rotation possible │
└─────────────────────────────────────────────────────────────┘
PATTERN 2: Generate Random Secrets
┌─────────────────────────────────────────────────────────────┐
│ resource "random_password" "db_password" { │
│ length = 32 │
│ special = true │
│ } │
│ │
│ resource "aws_secretsmanager_secret" "db_password" { │
│ name = "prod/database/password" │
│ } │
│ │
│ resource "aws_secretsmanager_secret_version" "db_password" {│
│ secret_id = aws_secretsmanager_secret.db_password.id │
│ secret_string = random_password.db_password.result │
│ } │
│ │
│ resource "aws_db_instance" "default" { │
│ password = random_password.db_password.result │
│ } │
│ │
│ Benefits: │
│ - No secret in code │
│ - Unique per environment │
│ - Stored in Secrets Manager for apps │
│ - Still in state (limitation) │
└─────────────────────────────────────────────────────────────┘
PATTERN 3: Use SOPS for Encrypted Files
┌─────────────────────────────────────────────────────────────┐
│ # secrets.yaml (encrypted with SOPS) │
│ db_password: ENC[AES256_GCM,data:...,type:str] │
│ │
│ # In Terraform │
│ data "sops_file" "secrets" { │
│ source_file = "secrets.yaml" │
│ } │
│ │
│ resource "aws_db_instance" "default" { │
│ password = data.sops_file.secrets.data["db_password"] │
│ } │
│ │
│ Benefits: │
│ - Encrypted in repository │
│ - Key managed separately (KMS) │
│ - Can be version controlled │
└─────────────────────────────────────────────────────────────┘
TERRAFORM STATE PROTECTION:
┌─────────────────────────────────────────────────────────────┐
│ # S3 backend with encryption │
│ terraform { │
│ backend "s3" { │
│ bucket = "my-terraform-state" │
│ key = "prod/terraform.tfstate" │
│ region = "us-east-1" │
│ encrypt = true │
│ kms_key_id = "alias/terraform-state" │
│ dynamodb_table = "terraform-locks" │
│ } │
│ } │
│ │
│ S3 Bucket Policy: │
│ - Deny unencrypted uploads │
│ - Restrict to specific IAM roles │
│ - Enable versioning for recovery │
│ - Enable access logging │
└─────────────────────────────────────────────────────────────┘
Secret Scanning:
Pre-Commit Secret Scanning:
GIT-SECRETS:
┌─────────────────────────────────────────────────────────────┐
│ # Install │
│ brew install git-secrets │
│ │
│ # Configure for AWS patterns │
│ git secrets --register-aws │
│ │
│ # Install hook │
│ git secrets --install │
│ │
│ # Scan existing repo │
│ git secrets --scan │
│ │
│ Detects: │
│ - AWS access key IDs (AKIA...) │
│ - AWS secret access keys │
│ - Custom patterns you define │
└─────────────────────────────────────────────────────────────┘
GITLEAKS:
┌─────────────────────────────────────────────────────────────┐
│ # Scan repository │
│ gitleaks detect --source . │
│ │
│ # Scan in CI/CD │
│ gitleaks detect --source . --exit-code 1 │
│ │
│ Detects: │
│ - AWS, GCP, Azure credentials │
│ - API keys │
│ - Private keys │
│ - Passwords │
│ - Generic secrets │
└─────────────────────────────────────────────────────────────┘
GITHUB SECRET SCANNING:
┌─────────────────────────────────────────────────────────────┐
│ Automatic scanning for: │
│ - Supported secret types (100+) │
│ - Push protection (blocks commits) │
│ - Alerts on detected secrets │
│ │
│ Enable in repository settings: │
│ Settings > Security > Secret scanning │
└─────────────────────────────────────────────────────────────┘
Key insight: Treat IaC state files as sensitive as the secrets they contain. Encrypt, restrict access, and audit.
5) Secure IaC Pipelines
The CI/CD pipeline for IaC requires its own security controls:
Secure Pipeline Design:
PIPELINE STAGES:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Lint │──►│ Scan │──►│ Plan │──►│ Approve │ │
│ │ │ │Security │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ ┌───────────────────────────┘ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Apply │──►│ Verify │ │
│ │ │ │ │ │
│ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
STAGE DETAILS:
┌─────────────────────────────────────────────────────────────┐
│ 1. LINT: │
│ - terraform fmt -check │
│ - terraform validate │
│ - tflint │
│ │
│ 2. SECURITY SCAN: │
│ - checkov / tfsec / trivy │
│ - Secret scanning │
│ - Policy evaluation (OPA/Sentinel) │
│ - FAIL on critical findings │
│ │
│ 3. PLAN: │
│ - terraform plan │
│ - Review changes │
│ - Cost estimation │
│ - Drift detection │
│ │
│ 4. APPROVE: │
│ - Manual approval for production │
│ - Review plan output │
│ - Require multiple approvers │
│ │
│ 5. APPLY: │
│ - terraform apply │
│ - Only from main branch │
│ - Audit logging │
│ │
│ 6. VERIFY: │
│ - Post-deployment validation │
│ - Compliance check │
│ - Functional testing │
└─────────────────────────────────────────────────────────────┘
Pipeline Security Controls:
Pipeline Security:
CREDENTIAL MANAGEMENT:
┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions with OIDC (no stored credentials): │
│ │
│ jobs: │
│ deploy: │
│ permissions: │
│ id-token: write │
│ contents: read │
│ steps: │
│ - uses: aws-actions/configure-aws-credentials@v2 │
│ with: │
│ role-to-assume: arn:aws:iam::123456789:role/... │
│ aws-region: us-east-1 │
│ │
│ Benefits: │
│ - No long-lived credentials │
│ - Automatic rotation │
│ - Auditable access │
└─────────────────────────────────────────────────────────────┘
LEAST PRIVILEGE FOR PIPELINE:
┌─────────────────────────────────────────────────────────────┐
│ Plan Stage IAM Role: │
│ - Read-only access to state │
│ - Read-only access to resources │
│ - No create/update/delete │
│ │
│ Apply Stage IAM Role: │
│ - Full access to managed resources │
│ - Write access to state │
│ - Scoped to specific resource types │
│ - No IAM admin permissions │
│ │
│ Separate roles for: │
│ - Different environments (dev, staging, prod) │
│ - Different resource types │
│ - Plan vs Apply stages │
└─────────────────────────────────────────────────────────────┘
BRANCH PROTECTION:
┌─────────────────────────────────────────────────────────────┐
│ GitHub Branch Protection Rules: │
│ │
│ ✓ Require pull request reviews │
│ ✓ Require status checks to pass │
│ - security-scan │
│ - terraform-plan │
│ ✓ Require branches to be up to date │
│ ✓ Require signed commits │
│ ✓ Do not allow bypassing settings │
│ │
│ Only allow terraform apply from main branch │
│ Only allow merges after security scan passes │
└─────────────────────────────────────────────────────────────┘
COMPLETE GITHUB ACTIONS EXAMPLE:
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Secret Scan
uses: gitleaks/gitleaks-action@v2
- name: Checkov Scan
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
soft_fail: false
- name: tfsec Scan
uses: aquasecurity/tfsec-action@v1.0.0
plan:
needs: security
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_PLAN_ROLE }}
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v2
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Post Plan to PR
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
with:
script: |
// Post plan output as PR comment
apply:
needs: plan
if: github.ref == 'refs/heads/main'
environment: production # Requires approval
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: ${{ secrets.AWS_APPLY_ROLE }}
- name: Terraform Apply
run: terraform apply -auto-approve tfplan
Key insight: The pipeline itself is a security boundary. Protect it as carefully as production infrastructure.
Real-World Context
Case Study: Codecov Supply Chain Attack
In 2021, attackers compromised Codecov's bash uploader script, which was used in thousands of CI/CD pipelines. The malicious script exfiltrated environment variables—including cloud credentials—from customer pipelines. Organizations using long-lived AWS credentials in their CI/CD were compromised. Those using OIDC-based authentication (short-lived credentials) were largely unaffected because credentials weren't stored in environment variables. This incident highlighted the importance of minimizing credentials in pipelines and using federated authentication.
Case Study: Terraform State Exposure
A developer accidentally committed their terraform.tfstate file to a public GitHub repository. The state file contained database passwords, API keys, and other sensitive values in plaintext. Even after removing the file, it remained in Git history. The organization had to rotate all credentials and implement state file encryption, remote backends, and .gitignore enforcement. Prevention would have been far cheaper than remediation.
IaC Security Checklist:
IaC Security Checklist:
CODE SECURITY:
□ No hardcoded secrets
□ Secrets from Secrets Manager / Vault
□ Variables marked sensitive
□ No default passwords
□ Secret scanning in pre-commit hooks
STATIC ANALYSIS:
□ Checkov/tfsec/trivy in CI/CD
□ Fail on HIGH/CRITICAL findings
□ Policy as code (OPA/Sentinel)
□ Regular rule updates
STATE FILE SECURITY:
□ Remote backend (S3, Terraform Cloud)
□ State file encryption (KMS)
□ State file access restricted
□ State file versioning enabled
□ No local state files committed
PIPELINE SECURITY:
□ OIDC authentication (no stored credentials)
□ Least privilege IAM roles
□ Separate plan/apply roles
□ Branch protection enabled
□ Required status checks
□ Manual approval for production
□ Audit logging enabled
MODULES:
□ Pin module versions
□ Use trusted sources
□ Review module code
□ Scan module dependencies
DRIFT DETECTION:
□ Regular drift checks
□ Alert on manual changes
□ Enforce IaC-only changes
IaC security is about preventing misconfigurations at the source. Every security check in the pipeline is a vulnerability prevented in production.
Guided Lab: Secure IaC Pipeline
In this lab, you'll build a secure Terraform pipeline with scanning and policy enforcement.
Lab Environment:
- GitHub repository
- AWS account with OIDC configured
- Terraform installed locally
- Checkov and tfsec installed
Exercise Steps:
- Create Terraform configuration with intentional issues
- Run local security scans and review findings
- Fix security issues in Terraform code
- Configure S3 backend with encryption
- Create GitHub Actions workflow with security stages
- Configure OIDC authentication for AWS
- Add OPA/Conftest policy checks
- Enable branch protection
- Test pipeline blocks insecure code
Reflection Questions:
- How does shifting security left change the development workflow?
- What security issues can't be caught by static analysis?
- How would you handle exceptions to security policies?
Week Outcome Check
By the end of this week, you should be able to:
- Explain the security benefits and risks of Infrastructure as Code
- Use Checkov, tfsec, and Trivy for IaC security scanning
- Write OPA/Rego policies for Terraform validation
- Implement secure secrets management in IaC
- Configure secure Terraform state backend
- Design secure CI/CD pipelines for IaC deployment
- Configure OIDC authentication for pipeline AWS access
- Implement branch protection and approval workflows
🎯 Hands-On Labs (Free & Essential)
Secure infrastructure-as-code with static analysis, policy enforcement, and secrets scanning.
📝 TryHackMe: Terraform Security
What you'll do: Analyze Terraform code for misconfigurations—find exposed
secrets, overpermissive policies, and hardening opportunities.
Why it matters: IaC misconfigurations multiply at deployment—catch them
before production.
Time estimate: 2-3 hours
🔍 Open Source: Checkov & tfsec Practice
What you'll do: Run Checkov and tfsec against sample IaC code—analyze findings,
fix vulnerabilities, integrate into CI/CD.
Why it matters: Automated scanning prevents 80% of common IaC
misconfigurations.
Time estimate: 2-3 hours
🛡️ HackTheBox: CI/CD Pipeline Security
What you'll do: Exploit and secure CI/CD pipelines—secrets exposure, supply
chain attacks, and policy enforcement (free modules).
Why it matters: Compromised pipelines deploy compromised infrastructure at
scale.
Time estimate: 3-4 hours
💡 Lab Strategy: Never commit secrets to IaC—use secret managers and environment variables. Git history never forgets.
Resources
Lab
Complete the following lab exercises to practice IaC security.