Systematically secure your cloud infrastructure. Covers CIS benchmarks, identity management, network segmentation, encryption, and compliance automation.
Cloud Security Posture Management (CSPM) is the continuous process of assessing, monitoring, and improving the security configuration of your cloud infrastructure. It matters when you need to ensure that your cloud environment—across multiple accounts, regions, and services—adheres to a known security baseline, and when drift from that baseline leads to vulnerabilities, compliance gaps, or incident response delays.
To harden a cloud environment, start by defining a security baseline: the set of required and recommended configurations across compute, storage, networking, and identity. This baseline must be machine-readable, version-controlled, and enforceable.
Define baseline rules using a declarative format such as JSON or YAML. Each rule specifies a control, a target resource type, and a condition. For example:
{
"rule_id": "s3-bucket-encrypted",
"description": "All S3 buckets must use server-side encryption with AWS KMS",
"resource_type": "aws:s3:bucket",
"compliance_standard": "cis-aws-2.0",
"severity": "high",
"condition": {
"property": "encryption",
"operator": "equals",
"value": {
"ServerSideEncryptionConfiguration": {
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": ".*"
}
}
]
}
}
}
}
Store these rules in a rules/ directory within your repository. Use git to version-control the baseline:
git add rules/
git commit -m "Add S3 encryption rule and VPC flow logging"
git push origin main
The baseline is now the source of truth. Any change to it triggers a CSPM scan.
Run CSPM scans on a regular schedule using curl, jq, and aws CLI. Each scan collects the current state of all resources and evaluates them against the baseline.
Schedule a scan with cron:
0 */6 * * * /usr/local/bin/cspm-scan --account us-east-1 --region all --output /var/reports/cspm-$(date +\%Y-\%m-\%d-\%H).json
The scan script fetches current configurations from each cloud provider:
#!/bin/bash
# /usr/local/bin/cspm-scan
set -euo pipefail
ACCOUNT="$1"
REGION="$2"
OUTPUT_DIR="$3"
# List all S3 buckets
aws s3api list-buckets --output json --query 'Buckets[].Name' > $OUTPUT_DIR/s3-buckets.json
# List all EC2 instances
aws ec2 describe-instances --output json --query 'Reservations[].Instances[].{InstanceId:InstanceId, ImageId:ImageId, State:State.Name, PublicIpAddress:PublicIpAddress}' > $OUTPUT_DIR/ec2-instances.json
# List all security groups
aws ec2 describe-security-groups --output json --query 'SecurityGroups[].{GroupId:GroupId, GroupName:GroupName, VpcId:VpcId, Rules:IpPermissions}' > $OUTPUT_DIR/security-groups.json
# List all VPC flow logs
aws ec2 describe-flow-logs --output json --query 'FlowLogs[].{FlowLogId:FlowLogId, LogGroupName:LogGroupName, Delimiter:DelimitedKeys}' > $OUTPUT_DIR/flow-logs.json
# Generate metadata
cat << EOF > $OUTPUT_DIR/metadata.json
{
"scan_timestamp": "$(date -Iseconds)",
"account": "$ACCOUNT",
"region": "$REGION",
"scan_id": "$(uuidgen)"
}
EOF
The output is a set of JSON files, each representing a resource collection. These files become input to the evaluation engine.
Use a lightweight evaluator that processes the scan output and applies the rules. The evaluator is a script that reads the rules and scans, then produces a report of compliance status per rule.
#!/bin/bash
# /usr/local/bin/cspm-evaluate
set -euo pipefail
RULES_DIR="$1"
SCANS_DIR="$2"
OUTPUT_DIR="$3"
mkdir -p "$OUTPUT_DIR"
for rule_file in $RULES_DIR/*.json; do
rule_id=$(jq -r '.rule_id' "$rule_file")
echo "Evaluating $rule_id..."
# Load rule
rule=$(cat "$rule_file")
# Find applicable scans
scan_files=$(find "$SCANS_DIR" -name "*.json" | grep "$(jq -r '.resource_type' <<< "$rule")")
# Evaluate each scan
for scan_file in $scan_files; do
scan_name=$(basename "$scan_file" .json)
scan_data=$(cat "$scan_file")
# Apply rule condition to scan data
result=$(jq --argjson rule "$rule" '
.rule_id as $rule_id |
.description as $description |
.condition as $condition |
.severity as $severity |
{
rule_id: $rule_id,
description: $description,
severity: $severity,
resource_type: .resource_type,
scan_id: $scan_name,
status: (
$condition.property as $prop |
$condition.operator as $op |
$condition.value as $val |
($scan_data | getpath($prop)) as $actual |
case $op
when "equals" then $actual == $val
when "contains" then $actual | index($val) != null
when "regex" then ($actual | test($val))
else false
),
details: {
actual_value: $actual,
expected_value: $val
}
}
' <<< "$rule")
echo "$result" >> "$OUTPUT_DIR/results.json"
done
done
# Aggregate results
jq -s 'add | group_by(.rule_id) | map({
rule_id: .[0].rule_id,
description: .[0].description,
severity: .[0].severity,
total_scans: length,
passed: (map(select(.status == true)) | length),
failed: (map(select(.status == false)) | length),
issues: map(select(.status == false)) | sort_by(.scan_id)
})' "$OUTPUT_DIR/results.json" > "$OUTPUT_DIR/aggregated.json"
This script produces an aggregated report with pass/fail status per rule, including the actual and expected values for each resource.
When a scan reveals drift, initiate remediation. Use terraform to generate configuration changes that align the environment with the baseline.
For example, if the S3 encryption rule fails for a bucket named logs-prod, the evaluator outputs:
{
"rule_id": "s3-bucket-encrypted",
"scan_id": "s3-buckets.json",
"status": false,
"details": {
"actual_value": {
"ServerSideEncryptionConfiguration": {
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}
},
"expected_value": {
"ServerSideEncryptionConfiguration": {
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": ".*"
}
}
]
}
}
}
}
Generate a Terraform module to fix the bucket:
# terraform/enforce-s3-encryption.tf
resource "aws_s3_bucket" "logs_prod" {
bucket = "logs-prod"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = "arn:aws:kms:us-east-1:123456789012:key/abc123-xyz"
}
}
}
}
output "remediation_plan" {
value = "Apply KMS encryption to bucket logs-prod"
}
Apply the plan via terraform apply:
terraform init
terraform plan -var="bucket_name=logs-prod" -out=plan.tfplan
terraform apply -input=false plan.tfplan
The resulting state file is committed to git:
git add .
git commit -m "Enforce S3 encryption on logs-prod"
git push origin main
This creates a feedback loop: configuration drift → scan → evaluation → remediation → applied fix → verified state.
Drift is silent until it causes failure. The most common failure is that a scan runs but no alert is sent. The system assumes all is well, but a critical service has been misconfigured for weeks.
To detect this, build a drift detector that compares successive scan outputs.
#!/bin/bash
# /usr/local/bin/cspm-detect-drift
set -euo pipefail
SCAN_DIR="$1"
DRIFT_DIR="$2"
# Compare current scan to previous
current=$(find "$SCAN_DIR" -name "*.json" -newer "$DRIFT_DIR/last-scan.json" | sort)
previous=$(find "$DRIFT_DIR" -name "*.json" -newer "$DRIFT_DIR/last-scan.json" | sort)
# Compute diff between scan sets
for file in $current; do
base=$(basename "$file")
prev_file="$DRIFT_DIR/$base"
if [ ! -f "$prev_file" ]; then
echo "New resource: $base"
continue
fi
# Compare JSONs using jq
diff=$(jq --argjson prev "$(cat "$prev_file")" '
. as $current |
$prev as $prev |
$current | tojson | fromjson |
$prev | tojson | fromjson |
(map(select(.id != null)) | group_by(.id)) as $groups |
$groups | map({
id: .[0].id,
current: .[0],
previous: .[1] // null
}) | map(select(.previous == null)) | length
' <<< "$file")
if [ "$diff" -gt 0 ]; then
echo "Drift detected: $base"
# Send alert via curl to alerting system
curl -X POST https://alerting.example.com/api/v1/alert \
-H "Content-Type: application/json" \
-d '{
"title": "Cloud Drift Detected",
"message": "Resource $base has changed in configuration.",
"severity": "warning",
"details": {
"scan_id": "$(basename "$file" .json)",
"change_type": "new",
"resource": "$base"
}
}'
fi
done
# Update last-scan.json
find "$SCAN_DIR" -name "*.json" | sort > "$DRIFT_DIR/last-scan.json"
This script detects new, changed, and missing resources across scans. It triggers alerts on drift, even when no rule fails.
condition.property is "encryption.SSEAlgorithm" but the actual data has "SSEAlgorithm" at the top level. Use jq to normalize the input before evaluation.s3-buckets.json exists, but the scan script outputs s3-buckets.json.gz. Decompressing with gunzip is essential.aws:s3:bucket, but the scan outputs s3:bucket or s3_bucket. Use jq to normalize the resource type: jq 'map(.resource_type |= gsub(":|_"; "-"))'
regex operator with "^us-.*", but the actual value is "us-west-2". The test function in jq is case-sensitive. Use test("us.*", "i") for case-insensitive matching.terraform apply command must be wrapped in a try-catch block: terraform apply -input=false plan.tfplan || {
echo "Terraform apply failed"
exit 1
}
systemd timers to ensure the detector runs on a fixed schedule, regardless of scan timing.CSPM is not a one-time setup. It requires ongoing maintenance:
rules/ and re-run scans.jq to validate the structure of a rule before committing: jq '.rule_id | test("^[a-z0-9-]+$")' rules/new-rule.json
metadata.json file with scan_timestamp and scan_id. find /var/reports -name "*.json" -mtime +30 -delete
jinja2 or pandoc from the aggregated JSON results.Cloud Security Posture Management is the continuous discipline of ensuring your cloud infrastructure remains secure, compliant, and consistent. It is not about tools, but about processes: scan, evaluate, remediate, detect, and maintain. It is the foundation of a resilient, auditable, and self-healing cloud environment.
This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy