Cloud Security · lesson 1 of 8
Intermediate
8- 1 Scanning Terraform for Misconfiguration with Checkov
- 2 Policy as Code: Testing Terraform Plans with OPA and Conftest
- 3 CIS Docker Benchmark Checks with docker-bench-security
- 4 Kubernetes Pod Security Admission: Enforcing the Restricted Profile
- 5 CIS Kubernetes Benchmark Checks with kube-bench in kind
- 6 Linting AWS IAM Policies Before Deploy with Parliament
- 7 Auditing the AWS IAM Credential Report with Python
- 8 Hunting in AWS CloudTrail Logs with Python
Also part of Cloud Security
11- + IAM Concepts That Transfer: AWS vs Azure vs GCP from Multi-Cloud
- + IAM: Users, Roles, and Policies from AWS
- + GCP IAM: Roles and Service Accounts from Google Cloud
- + Entra ID: Users, Roles, and RBAC from Azure
- + IAM and Least Privilege from Cloud Interviews
- + Cloud Networking Basics: What's the Same Everywhere from Multi-Cloud
- + Docker Security from Docker
- + Security, RBAC, and Observability from Kubernetes
- + sensitive = true: What It Hides, and What It Doesn't from Terraform
- + Building an Incident Timeline from Auth, Web and IDS Logs from Defensive Security
- + Envelope Encryption and Key Rotation Without Re-encrypting Data from Cryptography
Also part of: DevSecOps (AppSec in the pipeline) Terraform
Scanning Terraform for Misconfiguration with Checkov
Catch a public S3 bucket and SSH open to the internet in Terraform before anything is deployed: run Checkov, gate on the checks that matter, fix, re-scan.
By SkillByExample · Published
Infrastructure code can be wrong in the same way application code can, and the cheapest place to
find out is before terraform apply. Checkov reads .tf files and flags settings that would be
insecure once deployed. It needs no cloud account, no credentials and no plan.
Tested with Checkov 3.3.29 in Docker (bridgecrew/checkov:3.3.29) on Docker Engine 29.8.2. To
type checkov like the commands below, define an alias first. On Git Bash for Windows, also put
MSYS_NO_PATHCONV=1 in front of docker so the /src path isn’t rewritten:
alias checkov='docker run --rm -v "$PWD:/src" -w /src bridgecrew/checkov:3.3.29'
pip install checkov==3.3.29 gives the same command without Docker.
Scan a file with two real problems
This Terraform has a bucket whose public access block is switched off, and a security group that allows SSH from anywhere:
# main.tf: a public S3 bucket and SSH open to the internet
resource "aws_s3_bucket" "reports" {
bucket = "acme-reports"
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_security_group" "admin" {
name = "admin"
description = "Admin access"
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check)"
Passed checks: 9, Failed checks: 13, Skipped checks: 0
Check: CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
Check: CKV2_AWS_61: "Ensure that an S3 bucket has a lifecycle configuration"
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_5: "Ensure that Security Groups are attached to another resource"
Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
Check: CKV2_AWS_6: "Ensure that S3 bucket has a Public Access block"
Check: CKV_AWS_145: "Ensure that S3 buckets are encrypted with KMS by default"
Thirteen failures from 25 lines, and only some of them are exposure. The four CKV_AWS_53 to
CKV_AWS_56 checks are the four switched-off flags, CKV_AWS_24 is the SSH rule, and
CKV2_AWS_6 fails even though a public access block is attached, because a block with every flag
set to false blocks nothing. The rest (versioning, logging, replication, KMS) are hardening
advice, not holes. The order of the list can change between runs, so compare by check id, not by
line.
Gate on the checks that matter
--check limits the scan to a list of ids. Failing the build on these six, and reviewing the rest,
keeps the gate meaningful:
$ checkov -f main.tf --compact --quiet --check CKV_AWS_24,CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56,CKV2_AWS_6
terraform scan results:
Passed checks: 0, Failed checks: 6, Skipped checks: 0
Check: CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
FAILED for resource: aws_s3_bucket_public_access_block.reports
File: /main.tf:6-12
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-22
Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
FAILED for resource: aws_s3_bucket_public_access_block.reports
File: /main.tf:6-12
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-21
Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
FAILED for resource: aws_s3_bucket_public_access_block.reports
File: /main.tf:6-12
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-20
Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
FAILED for resource: aws_s3_bucket_public_access_block.reports
File: /main.tf:6-12
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-19
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
FAILED for resource: aws_security_group.admin
File: /main.tf:14-25
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/networking-1-port-security
Check: CKV2_AWS_6: "Ensure that S3 bucket has a Public Access block"
FAILED for resource: aws_s3_bucket.reports
File: /main.tf:2-4
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/s3-bucket-should-have-public-access-blocks-defaults-to-false-if-the-public-access-block-is-not-attached
$ echo $?
1
Each finding names the resource and the line range in main.tf, so 6-12 is the access block and
14-25 is the security group. The exit code is 1, which is what a CI step needs to fail the
pipeline.
Fix and re-scan
Turn the four flags on and narrow SSH to the VPN range:
# main.tf: public access blocked, SSH only from the internal network
resource "aws_s3_bucket" "reports" {
bucket = "acme-reports"
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_security_group" "admin" {
name = "admin"
description = "Admin access"
ingress {
description = "SSH from the VPN range"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.8.0.0/16"]
}
}
$ checkov -f main.tf --compact --quiet --check CKV_AWS_24,CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56,CKV2_AWS_6
terraform scan results:
Passed checks: 6, Failed checks: 0, Skipped checks: 0
$ echo $?
0
All six pass and the exit code is 0. Nothing was deployed and no credentials were used: the bucket and the security group never existed outside this file.
The full scan still has opinions:
$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check)"
Passed checks: 15, Failed checks: 7, Skipped checks: 0
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
Check: CKV2_AWS_61: "Ensure that an S3 bucket has a lifecycle configuration"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_5: "Ensure that Security Groups are attached to another resource"
Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
Check: CKV_AWS_145: "Ensure that S3 buckets are encrypted with KMS by default"
Fixing the exposure took the count from 13 to 7 and the passes from 9 to 15. The remaining seven are
decisions for the team, not emergencies. CKV2_AWS_5 only fails because this file never attaches
the security group to an instance.
Accept a finding on purpose
When a check doesn’t apply, say so in the resource, with a reason a reviewer can read:
# main.tf: one finding accepted on purpose, with the reason in the code
resource "aws_s3_bucket" "reports" {
#checkov:skip=CKV_AWS_144:Reports are rebuilt nightly, so no second region
bucket = "acme-reports"
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_security_group" "admin" {
name = "admin"
description = "Admin access"
ingress {
description = "SSH from the VPN range"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.8.0.0/16"]
}
}
$ checkov -f main.tf --compact --check CKV_AWS_144 | sed -n '/scan results/,$p'
terraform scan results:
Passed checks: 0, Failed checks: 0, Skipped checks: 1
Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
SKIPPED for resource: aws_s3_bucket.reports
Suppress comment: Reports are rebuilt nightly, so no second region
File: /main.tf:2-5
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-s3-bucket-has-cross-region-replication-enabled
The check moves from failed to skipped, and the reason travels with it into every report. That
beats a global --skip-check, which hides the check everywhere and records no reason. The sed
only trims Checkov’s ASCII banner, which --quiet would hide along with the skipped check.
Practice
- In the fixed file, change the ingress rule to port 3389 from
0.0.0.0/0. Which check id fails now?
Solution
CKV_AWS_25, the RDP version of the SSH check:
$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check: CKV_AWS_2)"
Passed checks: 14, Failed checks: 8, Skipped checks: 0
Check: CKV_AWS_25: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 3389"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
Checkov has separate checks per well-known admin port, so a rule open to the internet on 22 or 3389 is caught by id, not by guessing.
- On the fixed file, drop
CKV2_AWS_5from the full scan without editing the Terraform. What does the summary line become?
Solution
$ checkov -f main.tf --compact --quiet --skip-check CKV2_AWS_5 | grep -E "^Passed"
Passed checks: 15, Failed checks: 6, Skipped checks: 0
--skip-check removes the check from the run entirely, so it isn’t even counted as skipped. That
is why an inline skip with a reason is the better tool for one-off exceptions.
- Run the original
main.tfwith--check CKV_AWS_24 --soft-fail. What exit code do you get, and when is that useful?
Solution
$ checkov -f main.tf --compact --quiet --check CKV_AWS_24 --soft-fail | grep -E "^Passed"
Passed checks: 0, Failed checks: 1, Skipped checks: 0
$ echo ${PIPESTATUS[0]}
0
The failure is still reported, but Checkov exits 0. Use it while introducing a scanner to an existing codebase: report first, then switch to failing builds once the backlog is fixed.
- Write a custom YAML policy
CKV2_ACME_1that requires anownertag on everyaws_s3_bucket, and make the fixed file pass it.
Solution
metadata:
id: "CKV2_ACME_1"
name: "S3 buckets must have an owner tag"
category: "GENERAL_SECURITY"
definition:
cond_type: "attribute"
resource_types:
- "aws_s3_bucket"
attribute: "tags.owner"
operator: "exists"
Save it as policies/owner_tag.yaml. Without a tag the bucket fails:
$ checkov -f main.tf --compact --quiet --external-checks-dir policies --check CKV2_ACME_1
terraform scan results:
Passed checks: 0, Failed checks: 1, Skipped checks: 0
Check: CKV2_ACME_1: "S3 buckets must have an owner tag"
FAILED for resource: aws_s3_bucket.reports
File: /main.tf:2-4
Add tags = { owner = "data-team" } under bucket = "acme-reports" and it passes:
$ checkov -f main.tf --compact --quiet --external-checks-dir policies --check CKV2_ACME_1
terraform scan results:
Passed checks: 1, Failed checks: 0, Skipped checks: 0