Cloud Security Posture Management: Hardening Your Cloud Environment

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.

Establishing a Baseline Configuration

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.

Automating Scans Across Cloud Accounts and Regions

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.

Evaluating Resources Against the Baseline

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.

Enforcing Configuration with Automated Remediation

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.

Detecting and Responding to Drift in Production

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.

Common Failure Modes and Gotchas

  jq 'map(.resource_type |= gsub(":|_"; "-"))'
  terraform apply -input=false plan.tfplan || {
    echo "Terraform apply failed"
    exit 1
  }

Maintaining the Posture Over Time

CSPM is not a one-time setup. It requires ongoing maintenance:

  jq '.rule_id | test("^[a-z0-9-]+$")' rules/new-rule.json
  find /var/reports -name "*.json" -mtime +30 -delete

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.