Policy-as-code enforcement and static analysis for Terraform infrastructure modules, aligned with the CIS AWS Foundations Benchmark. Companion project to container-hardening-lab.
No AWS credentials required. All checks are static analysis against HCL — nothing is deployed.
- Misconfigurations are caught before they reach a cloud account. tfsec, Trivy, and Conftest (OPA) all run against Terraform source files — no plan execution, no cloud API calls.
- Every hardening control maps to a CIS benchmark reference. Each module variable, resource, and policy rule cites the CIS AWS Foundations Benchmark section it enforces.
- Policy-as-code with OPA/Rego. Six custom Rego policies enforce controls that generic scanners miss: cross-resource joins (VPC → flow log), deep IAM JSON parsing (wildcard Action/Resource), multi-field checks (all four S3 public access block settings), CloudTrail log validation chains, RDS SSL/TLS parameter enforcement, and EKS Secrets encryption with Conftest HCL parsing normalization.
- Failure demo validates the pipeline itself. The
examples/insecure/directory contains deliberate violations. CI asserts that scanners must find them — a clean scan fails the job. - Three test layers. OPA unit tests (synthetic fixtures), native Terraform tests (variable validation, mock providers), and static analysis scans.
iac-security-lab/
├── modules/
│ ├── s3/ # CIS 2.1.x — encryption, public access block, versioning, logging
│ ├── iam/ # CIS 1.x — no wildcards, permission boundary, non-root path
│ ├── vpc/ # CIS 4.x/5.x — flow logs, restricted SGs, default SG locked
│ ├── cloudtrail/ # CIS 3.x — multi-region, log validation, CWL, KMS encryption
│ ├── rds/ # CIS 2.3.x — storage encryption, SSL/TLS, no public access
│ └── eks/ # CIS EKS Benchmark — Secrets encryption, private endpoint, IMDSv2
│
├── examples/
│ ├── hardened/ # Correct module consumption — make lint must pass cleanly
│ └── insecure/ # 10 deliberate violations — scanners must find them
│
├── policies/opa/ # Rego policies evaluated by Conftest against HCL
│ ├── s3.rego
│ ├── iam.rego
│ ├── vpc.rego
│ ├── cloudtrail.rego
│ ├── rds.rego
│ └── eks.rego
│
├── tests/
│ ├── opa/ # OPA unit tests — one file per policy
│ │ ├── s3_test.rego
│ │ ├── iam_test.rego
│ │ ├── vpc_test.rego
│ │ ├── cloudtrail_test.rego
│ │ ├── rds_test.rego
│ │ └── eks_test.rego
│ └── terraform/ # Native terraform test configs (TF >= 1.6)
│ ├── s3.tftest.hcl
│ ├── iam.tftest.hcl
│ ├── vpc.tftest.hcl
│ ├── cloudtrail.tftest.hcl
│ ├── rds.tftest.hcl
│ └── eks.tftest.hcl
│
├── docs/
│ ├── tool-decisions.md
│ ├── adding-a-module.md
│ └── conftest-hcl-parsing.md
│
├── .github/workflows/ci.yml
├── .tfsec.yml
├── Makefile
├── CONTRIBUTING.md
└── README.md
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| 2.1.1 — SSE enabled | Unencrypted data at rest | aws_s3_bucket_server_side_encryption_configuration always created; AES-256 default, KMS optional |
| 2.1.2 — Public access block | Accidental internet exposure via ACL or bucket policy | All four block settings hardcoded to true |
| 2.1.3 — Versioning | Ransomware overwrite, accidental permanent deletion | aws_s3_bucket_versioning status = "Enabled" |
| 2.1.5 — Access logging | Undetectable exfiltration, no GetObject audit trail | aws_s3_bucket_logging required; log bucket passed as variable |
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| 1.16 — No inline policies | Inline policies bypass IAM policy versioning and audit tools | Module only creates managed policies via aws_iam_policy |
| 1.16 — Permission boundary required | Privilege escalation via over-permissive policy attachment | permission_boundary_arn is a required variable with ARN validation |
| 1.16 — Non-root IAM path | Wildcard path-prefix matches in IAM conditions | role_path defaults to /app/; root / is rejected by variable validation |
| Principle of least privilege | Credential compromise → full account access | OPA iam.rego denies any wildcard Action or Resource |
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| 4.x — VPC flow logs | No network audit trail for lateral movement or exfiltration | aws_flow_log always created; CloudWatch Logs destination |
| 5.1 — No 0.0.0.0/0 on sensitive ports | Unrestricted SSH/RDP/DB access from the internet | OPA vpc.rego denies; module creates no such rules |
| 5.4 — Default SG restricted | Resources in default SG have implicit full access | aws_default_security_group removes all ingress/egress rules |
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| 3.1 — Multi-region trail | API calls in non-home regions leave no trail | is_multi_region_trail = true, include_global_service_events = true |
| 3.2 — Log file validation | Attacker with S3 write access deletes/modifies logs undetected | enable_log_file_validation = true — SHA-256 HMAC digest chain |
| 3.4 — CloudWatch Logs integration | No real-time alerting on DeleteTrail, StopLogging, ConsoleLogin without MFA |
cloud_watch_logs_group_arn and delivery role always configured |
| 3.6 — S3 access logging on trail bucket | Exfiltration of audit trail itself is unlogged | aws_s3_bucket_logging on the trail log bucket |
| 3.7 — KMS encryption of log files | S3-stored logs readable by anyone with bucket GetObject | kms_key_id is required; no default AWS-managed key permitted |
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| 2.3.1 — Storage encrypted (CMK) | Exfiltrated EBS snapshots are readable without a key | storage_encrypted = true; kms_key_id required |
| 2.3.2 — Automated backups ≥ 7 days | No recovery path after ransomware or insider deletion | backup_retention_period validated >= 7 |
| 2.3.3 — Not publicly accessible | Direct internet database access from leaked connection strings | publicly_accessible = false hardcoded |
| In-transit SSL/TLS | Cleartext credentials and queries on the wire | Parameter group enforces rds.force_ssl=1 (PostgreSQL) or require_secure_transport=ON (MySQL) |
| SG-to-SG only ingress | CIDR-based internet ingress structurally impossible | No CIDR ingress variable; only allowed_security_group_ids |
| CIS Control | What It Prevents | Implementation |
|---|---|---|
| EKS 2.1.1 — All control plane log types | Audit blind spots: RBAC decisions, auth failures, scheduler activity | All five types: api, audit, authenticator, controllerManager, scheduler |
| EKS 3.1.1 — Secrets encrypted at rest (CMK) | Compromised etcd backup exposes all Secrets in base64 | encryption_config with resources = ["secrets"] and CMK |
| EKS 5.4.1 — Private API endpoint | Control plane accessible from the internet | endpoint_private_access = true; endpoint_public_access = false by default |
| EKS 5.4.2 — Public access CIDRs restricted | 0.0.0.0/0 access to API server when public endpoint enabled |
public_access_cidrs required when enabling public endpoint |
| IMDSv2 enforced | SSRF in any container retrieves node IAM credentials | Launch template: http_tokens = "required", hop limit = 1 |
Fast, no tooling beyond opa. Each test mocks input directly as the Conftest-parsed JSON structure.
make test-opa
# or individually:
opa test policies/opa/s3.rego tests/opa/s3_test.rego --verboseCovers: deny triggers, pass cases, warn rules, multi-resource edge cases.
Validates variable validation logic and output structure without cloud credentials.
terraform -chdir=modules/s3 test
terraform -chdir=modules/iam test
terraform -chdir=modules/vpc testUses mock_provider "aws" {} — no API calls. Tests include expect_failures cases for invalid inputs.
tfsec and Trivy run against module HCL. Conftest runs custom OPA policies.
make lint # Conftest
make scan # tfsec + trivyexamples/insecure/main.tf contains ten deliberate violations across five services:
| Violation | CIS Control | Scanner |
|---|---|---|
| S3 bucket with no SSE | 2.1.1 | tfsec, Trivy |
| S3 public access block all false | 2.1.2 | tfsec, Trivy, Conftest |
IAM policy with Action: "*" |
1.16 | Conftest (iam.rego) |
IAM policy with Resource: "*" |
1.16 | Conftest (iam.rego) |
Security group 0.0.0.0/0 on port 22 |
5.1 | tfsec, Trivy, Conftest |
| VPC with no flow logs | 4.x | Conftest (vpc.rego) |
| RDS no encryption + publicly accessible | 2.3.1, 2.3.3 | tfsec, Trivy, Conftest |
| RDS parameter group without SSL | In-transit | Conftest (rds.rego) |
| EKS public endpoint + no Secrets encryption | EKS 5.4.1, 3.1.1 | Conftest (eks.rego) |
| Launch template without IMDSv2 | IMDSv2 | Conftest (eks.rego) |
CI job failure-demo asserts that both conftest and tfsec exit non-zero against this directory. A clean result fails the job — proving the pipeline itself is working.
See examples/insecure/README.md for the attack each violation enables.
graph LR
A[push / PR] --> B[policy-tests\nOPA unit tests]
B --> C[failure-demo\ninsecure configs must fail]
B --> D[module-pipeline\nvalidate · lint · tfsec · trivy]
D --> E[SARIF → GitHub Security tab]
Three jobs run on every push to main and every pull request:
- policy-tests — OPA unit tests for all six policies. No cloud, no Docker. Fast feedback.
- failure-demo — Conftest + tfsec against
examples/insecure/. Must produce findings. - module-pipeline — Matrix over [s3, iam, vpc, cloudtrail, rds, eks]. Validate, lint, tfsec SARIF, Trivy SARIF, artifact upload.
# Prerequisites: opa, conftest, tfsec, trivy, terraform
# Run OPA unit tests (no external tools except opa)
make test-opa
# Validate module HCL syntax
make validate
# Run Conftest policy check
make lint
# Run tfsec + trivy
make scan
# Full pipeline
make all- Terraform 1.9.x
- OPA 0.70.0
- Conftest 0.57.0
- tfsec 1.28.x
- Trivy 0.69.x
No AWS credentials required. See CONTRIBUTING.md for install instructions.
- container-hardening-lab — same philosophy applied to container images. The VPC module's private subnet design is where a hardened EKS cluster (from the container lab) would run.
- docs/tool-decisions.md — tfsec vs Checkov vs Trivy, native test vs Terratest, why no Sentinel
- docs/adding-a-module.md — checklist for adding a new module
- docs/conftest-hcl-parsing.md — Conftest HCL parsing gotchas: function calls and single-block-as-object