DevSecOps for Terraform in 1 hour
In this comprehensive chapter of the DevSecOps Zero to Hero series, we shift our focus to Infrastructure as Code (IaC) Security, specifically examining Terraform. We explore how to establish robust security baselines, automate static security analysis using Checkov, and eliminate long-lived cloud credentials by integrating HashiCorp Vault with GitHub Actions via OIDC.
1. Why Do We Need Terraform Security?
The Status Quo and Its Perils
Infrastructure as Code (IaC) allows organizations to define cloud resources using declarative configuration files (e.g., HashiCorp Configuration Language or HCL). While this drastically improves scalability and reproducibility, it also introduces unique security risks:
- Human Error & Defaults: Junior engineers or rushed developers often rely on tutorials, AI tools (like ChatGPT), or copy-pasted snippets that may create insecure infrastructure (e.g., public S3 buckets, wide-open security groups).
- Hardcoded Secrets: Developers frequently embed sensitive credentials (
AWS_ACCESS_KEY_ID, database passwords, private keys) directly into Terraform files or environment variable files. - Long-Lived Credentials: Traditional workflows rely on static, long-lived AWS IAM user credentials stored indefinitely within CI/CD secret repositories. If compromised, these credentials grant persistent access to cloud resources.
2. Core Mental Model & Real-Life Analogies
The Visitor Badge Analogy (Short-Lived vs. Permanent Keys)
Real-Life Analogy: Imagine visiting a high-security corporate headquarters.
- Long-Lived Credentials (The Problem): Giving every visitor a master electronic key card that never expires. If a card is lost or stolen, anyone can walk into the building at any time.
- Short-Lived Credentials (Vault + OIDC): Giving visitors a temporary, color-coded day pass that automatically deactivates after 15 minutes. Even if someone steals it, the pass is worthless shortly after. This is exactly how HashiCorp Vault protects cloud environments.
3. Common Security Best Practices for Terraform
To prevent catastrophic leaks and infrastructure misconfigurations, DevSecOps engineers enforce a three-tier defensive strategy:
- Comprehensive
.gitignoreConfiguration:- Never commit sensitive configuration files or state files to version control.
- Every repository must ignore environment files (
.env), Terraform state files (*.tfstate,*.tfstate.backup), and private keys (*.pem,id_rsa).
- Local Pre-Commit Hooks (GitLeaks):
- Catch secrets before they touch remote repositories.
- Even if a developer forgets to update
.gitignore, a pre-commit hook runs automatically ongit commit, scanning files for regex patterns matching API keys, passwords, and tokens.
- Automated CI/CD Secret Scanning:
- Implement workflow checks in GitHub Actions to block pull requests or commits if any secret or misconfiguration slips past local validation.
4. Setting Up Pre-Commit Hooks with GitLeaks
Relying solely on developer discipline fails. Implementing an automated pre-commit framework ensures that accidental credential leaks are blocked locally prior to execution.
Installation Steps
- macOS:
brew install pre-commit - Linux/Pip:
pip install pre-commit
Configuration File (.pre-commit-config.yaml)
Create a pre-commit configuration file in your repository root to invoke GitLeaks:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Installation and Execution
Initialize the hook within your local git repository:
pre-commit install
pre-commit auto-update
If a developer accidentally creates a secrets.env file containing an AWS secret key and attempts a commit:
git commit -am "WIP: adding advanced terraform workflow"
Output / Prevention:
gitleaks........................................................Failed
- hook id: gitleaks
- exit code: 1
- secret detected: AWS Secret Access Key
Error: 1 leak found. Commit blocked.
5. Identifying Insecure Terraform Configurations with Checkov
Even if code is free of hardcoded secrets, the infrastructure definition itself may be dangerously misconfigured (e.g., an S3 bucket with public access enabled). Terraform syntax checkers and basic linters will pass valid HCL syntax, missing security vulnerabilities.
Naive vs. Secure Terraform Configuration
Below is a comparison between an insecure S3 bucket configuration (generated by accident or AI assistants) and a hardened production-ready configuration.
Running Checkov Scans
To scan a local directory containing Terraform files for security and compliance misconfigurations:
checkov -d .
Checkov Execution Output (Insecure State):
[HTTP GET]
Passed checks: 2, Failed checks: 4, Skipped checks: 0
Resource: aws_s3_bucket.example
- FAILED: CKV_AWS_19: "Ensure all data stored in the S3 bucket is securely encrypted at rest"
- FAILED: CKV_AWS_53: "Ensure S3 bucket has block public ACLs enabled"
- FAILED: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
- FAILED: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
- FAILED: CKV_AWS_56: "Ensure S3 bucket has restrict public buckets enabled"
Fixing the Insecure Configuration (main.tf)
Update your Terraform resource definition to explicitly enforce security controls:
resource "aws_s3_bucket" "secure_bucket" {
bucket = "devsecops-secure-bucket-demo"
}
resource "aws_s3_bucket_public_access_block" "public_block" {
bucket = aws_s3_bucket.secure_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Re-running checkov -d . will now result in Passed checks: 6, Failed checks: 0.
6. The Production Workflow: Moving Away from Local Execution
In hobby projects, developers execute Terraform locally using personal AWS credentials stored in .env files. In enterprise production environments, this model is insecure and non-compliant.
Enterprise vs. Hobbyist Workflow Comparison
Key Advantages of CI/CD-Driven Infrastructure:
- Peer Review & Accountability: Every infrastructure change requires pull request reviews by senior engineers.
- Auditability & Traceability: Git history explicitly records who proposed a change, when it was merged, and why.
- Version Control & Instant Rollbacks: If a faulty infrastructure commit breaks production, engineers can instantly revert the commit and trigger a CI deployment of the previous stable state.
7. The Problem with Long-Lived Credentials
When configuring GitHub Actions to deploy infrastructure, teams often create an AWS IAM service account (bot account) and store its static Access Key ID and Secret Access Key inside GitHub Repository Secrets.
Why Static Repository Secrets Fail at Scale:
- Insider Threat & Proliferation: Dozens of engineers, developers, and managers often have access to the repository settings or CI configuration. If anyone leaves or leaks the credentials, the keys remain active indefinitely.
- Lack of Rotation: Organizations rarely rotate bot credentials every 10–30 days, leaving a massive window of vulnerability if keys are intercepted.
8. HashiCorp Vault & Short-Lived Credentials
HashiCorp Vault solves the long-lived credential problem by acting as a dynamic secrets broker. Instead of storing permanent AWS keys in GitHub, GitHub Actions uses OIDC (OpenID Connect) and JSON Web Tokens (JWT) to authenticate directly with Vault and request temporary, short-lived credentials (valid for only 10 to 15 minutes).
High-Level OIDC Authentication Flow
9. Setting Up HashiCorp Vault on AWS EC2
Follow these steps to deploy a development Vault server on an EC2 instance and configure dynamic AWS secrets.
Step 1: Install Vault Binary
SSH into your Ubuntu EC2 instance and install essential utilities:
sudo apt-get update && sudo apt-get install -y unzip wget
wget https://releases.hashicorp.com/vault/1.15.5/vault_1.15.5_linux_amd64.zip
unzip vault_1.15.5_linux_amd64.zip
sudo mv vault /usr/local/bin/
vault version
Step 2: Run Vault in Development Mode
Start the Vault server binding to localhost on port 8200:
vault server -dev -dev-listen-address="0.0.0.0:8200"
(Keep this terminal running. Note the generated root token, e.g., root). Ensure inbound security group rules on AWS allow port 8200 for testing.
Step 3: Configure AWS Secret Engine in Vault
Open a second terminal session connected to your EC2 instance and configure the AWS secrets engine:
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'
# Enable the AWS secret engine
vault secrets enable aws
# Write root AWS credentials into Vault (replace with your active AWS keys)
vault write aws/config/root \
access_key="YOUR_AWS_ACCESS_KEY_ID" \
secret_key="YOUR_AWS_SECRET_ACCESS_KEY" \
region="us-east-1"
Step 4: Configure OIDC Authentication for GitHub Actions
Enable JWT authentication and configure OIDC discovery so GitHub Actions can authenticate trustlessly:
# Enable JWT auth method
vault auth enable jwt
# Configure OIDC discovery URL for GitHub Actions
vault write auth/jwt/config \
bound_issuer="https://token.actions.githubusercontent.com" \
default_role="terraform-role"
Step 5: Create Vault Policy and Role for GitHub
Define a read policy for the AWS secrets engine:
vault policy write terraform-policy - <<EOF
path "aws/creds/terraform-role" {
capabilities = ["read"]
}
EOF
Bind the policy to your specific GitHub repository (e.g., your-username/devsecops-zero-to-hero):
vault write auth/jwt/role/terraform-role \
role="oidc" \
allowed_oidc_scopes="openid" \
user_claim="sub" \
bound_audiences="https://github.com/your-username" \
bound_claims_type="string" \
bound_claims="repo:your-username/devsecops-zero-to-hero:ref:refs/heads/main" \
policies="terraform-policy" \
ttl="15m"
10. Integrating Terraform with GitHub Actions & Vault
Create a GitHub Actions workflow file in .github/workflows/infra-deploy.yml that fetches dynamic credentials from Vault and executes Terraform.
name: Terraform Deployment with Vault
on:
push:
branches: [ "main" ]
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Authenticate with Vault & Fetch AWS Credentials
uses: hashicorp/vault-action@v2
with:
url: http://<YOUR_VAULT_EC2_PUBLIC_IP>:8200
method: jwt
role: terraform-role
jwtGithubString: ${{ steps.mint-token.outputs.result }}
exportEnvironment: true
secrets: |
aws/creds/terraform-role access_key AWS_ACCESS_KEY_ID |
aws/creds/terraform-role secret_key AWS_SECRET_ACCESS_KEY
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Init
run: terraform init
working-directory: ./terraform
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: ./terraform
Verifying Short-Lived IAM Users in AWS
- Navigate to the AWS IAM Console -> Users.
- Trigger the GitHub Actions workflow.
- Observe that a temporary IAM user (managed dynamically by HashiCorp Vault) is automatically created with a lifespan of strictly 10 to 15 minutes, after which it is automatically destroyed.
Summary and Key Takeaways
- Infrastructure as Code (IaC) Security: Automating cloud resource creation with Terraform requires rigorous security controls to prevent public data exposure and misconfigurations.
.gitignoreDiscipline: Always exclude sensitive environment files (.env), Terraform state files (*.tfstate), and private keys (*.pem) from version control.- Pre-Commit Hooks with GitLeaks: Utilize local pre-commit hooks to block commits containing hardcoded API keys and secrets before code is pushed upstream.
- Static Analysis with Checkov: Run Checkov (
checkov -d .) to automatically scan Terraform configuration files for security compliance violations (e.g., public S3 buckets, unencrypted storage). - The Problem with Long-Lived Credentials: Storing static AWS access keys in CI/CD secret repositories creates persistent attack surfaces vulnerable to insider threats and leaks.
- HashiCorp Vault & OIDC Integration: Eliminate permanent keys by using HashiCorp Vault to dynamically provision short-lived (10–15 minute) AWS credentials via GitHub Actions and OpenID Connect (OIDC).
- Enterprise CI/CD Pipelines: Enforce code reviews, audit trails, and version-controlled rollbacks by ensuring all infrastructure changes are deployed exclusively through automated CI/CD pipelines.