Most compliance work is cleanup. A team ships a service, a scanner runs months later, and the findings turn into a backlog nobody planned for. The cheaper order is the reverse: start from infrastructure that already passes the scan, and let the scanner prove it stays that way.
That is the idea behind the latest release of the Terraform ECS Fargate starter. It was already a fork-and-deploy template: VPC, load balancer, Fargate service, ECR, logs, and optional CloudFront, EFS, and PostgreSQL. Version 2.2 connects it to the other thing I build, WardBee by ScanComb, which checks cloud and repository evidence against framework controls. The template now ships the controls WardBee looks for, runs the same scanner in CI, and documents where each control lands in ISO 27001, SOC 2, CIS, NIS2, and NIST 800-53.
A template decides the security posture of every service built from it. Fix the template once and every fork starts from a passing position.
Why the template is the right place to fix this
When I scan an AWS account, the same findings come back again and again: no VPC flow logs, logs that expire after a week, a database without deletion protection, backups set to zero "for dev", missing security headers. None of them are hard to fix. They exist because the first version of the infrastructure did not include them, and every later version copied the first.
A starter template is copied on purpose. If the defaults are weak, every team that
forks it inherits the gap and the remediation ticket. If the defaults are strong, the
team gets the control for free and spends its time on the application. That is the
jump start: not a faster terraform apply, but not having to come back
later.
Same scanner, two places
WardBee evaluates evidence at two layers. On connected GitHub and GitLab repositories it runs Checkov over the Terraform, which shows what the code declares. On a connected AWS account it runs Prowler through a read-only role, which shows what is actually deployed. Both results roll up into the same framework controls, and anything that cannot be confirmed stays out of the pass count.
The starter now runs the same Checkov scan, in the same mode, on every pull request and every release. Before this change the root configuration had 11 failed checks. It now has none:
checkov -d . --framework terraform --compact
Passed checks: 13, Failed checks: 0, Skipped checks: 12
The skips matter as much as the passes. Every one is an inline comment with a reason, next to the code it applies to:
module "vpc" {
# checkov:skip=CKV_TF_1:Registry module pinned to an exact version; Dependabot proposes reviewed updates.
source = "terraform-aws-modules/vpc/aws"
version = "6.7.3"
...
}
An auditor does not need zero exceptions. They need exceptions that are deliberate, explained, and visible in history. An inline skip travels with the code, so CI, WardBee, and anyone running Checkov locally all see the same decision and report it as skipped, never as passed.
The scanner also keeps moving. The day this shipped, a new Checkov release added a rule about unpinned Availability Zone lookups and the release pipeline stopped. That is the system working: a new rule surfaced, the decision was reviewed, and the answer was recorded next to the code in a patch release.
What changed in the infrastructure
The changes are small, and most of them cost nothing. That is the point: the gap between a demo stack and a reviewable one is usually a handful of arguments.
- VPC flow logs on by default, sent to CloudWatch Logs.
- One-year log retention for container, flow, RDS, and ECS Exec logs, instead of 30 days.
- Deletion protection on the load balancer and database, with a final snapshot when the database is removed.
- Backups cannot be switched off. Setting backup retention to zero now fails validation.
- PostgreSQL hardening: IAM authentication, log exports, a Secrets Manager password that RDS rotates, and an optional Multi-AZ switch.
- CloudFront security headers through the AWS managed policy, so HSTS and friends are set without writing a policy.
- A WAF hook: pass an existing web ACL ARN and CloudFront uses it.
Those sit on top of what the template already did: tasks and data in private subnets, a security-group chain that only lets each tier talk to the next, an internal load balancer reachable only through CloudFront, encryption at rest everywhere, immutable image tags with scan on push, and secrets passed as ARNs instead of plain environment variables. The upgrade also moved every module and the AWS provider to their current releases.
From controls to frameworks
A control that nobody can map to a requirement is hard to use in a review. The new compliance guide lists each capability with the file that implements it, the NIST 800-53 control it evidences, and the matching ISO 27001, SOC 2, CIS v8, and NIS2 Article 21 references. It also names the Prowler check that confirms the control at runtime:
VPC flow logs → AU-12, SI-4
→ ISO 27001 A.8.15, A.8.16 · SOC 2 CC7.2 · CIS 8.2, 13.6 · NIS2 21(2)(e)
→ Prowler: vpc_flow_logs_enabled
The mapping uses the same NIST 800-53 spine and crosswalk WardBee uses internally, so the control IDs in the document match the ones in the WardBee report. A crosswalked mapping is partial evidence: it shows the controls cover the same subject, not that one fully satisfies the other. The guide says so, because overstating coverage is how audits go wrong.
What the template does not do
A workload template cannot make an organization compliant. CloudTrail, GuardDuty, Security Hub, AWS Config, identity with MFA, backup plans with tested restores, and alarms with someone on call belong to the account baseline, not to each service. The compliance guide lists them explicitly, and a Prowler run through WardBee will flag them if they are missing. That is useful rather than noisy: once the workload controls pass, the remaining gaps are the account-level work that actually needs a decision.
Some trade-offs are also left to the team. The default CloudFront certificate cannot enforce a minimum TLS version, so a custom domain is the fix. ECR stays on AES-256 encryption, because switching an existing repository to KMS replaces it and deletes its images. Both are written down, with the reason, rather than hidden.
The fast path
- Create a repository from the template and run the first plan and apply. The default nginx container proves the platform before your image exists.
- Connect the repository and the AWS account to WardBee with read-only access.
- Read the gap list. It should contain the account-level items and your application's own controls, not the infrastructure basics.
- Keep CI green. A pull request that weakens a control fails Checkov before it merges, and the next Prowler run confirms what is deployed.
That is the connection between the two projects. The template makes the secure version the default. WardBee keeps checking that it stays that way and turns the result into evidence a reviewer can follow. Your team still signs off, but it signs off on a short list instead of a spreadsheet.