Skip to content

Lab 8: Cloud Security Audit

Lab Overview

Estimated Duration: 2-3 hours Difficulty: Intermediate Environment: AWS Free Tier account (or provided lab account) Learning Objectives: Identify and remediate common AWS misconfigurations using Prowler and manual assessment

Use a Dedicated Lab Account

NEVER run security scanners against production AWS accounts without explicit authorization. Use a dedicated lab/sandbox account. All resources created in this lab should be cleaned up to avoid charges.


Prerequisites

  • AWS account (free tier sufficient) or access to lab-provided account
  • AWS CLI installed and configured
  • Python 3.8+, pip
  • Completed Chapter 20 (Cloud Attack and Defense)

Lab Environment Setup

# Install required tools
pip3 install prowler boto3 checkov

# Configure AWS CLI
aws configure
# AWS Access Key ID: [YOUR_LAB_KEY]
# AWS Secret Access Key: [YOUR_LAB_SECRET]
# Default region name: us-east-1
# Default output format: json

# Verify access
aws sts get-caller-identity

Part 1: Intentionally Vulnerable Lab Infrastructure

Deploy intentionally vulnerable resources (in lab account only):

# Create vulnerable S3 bucket (public read)
BUCKET_NAME="lab-audit-$(date +%s)"
aws s3 mb s3://$BUCKET_NAME
aws s3api put-bucket-acl --bucket $BUCKET_NAME --acl public-read  # INTENTIONALLY INSECURE
echo "test data" | aws s3 cp - s3://$BUCKET_NAME/sensitive-data.txt

# Create IAM user with overly broad permissions
aws iam create-user --user-name lab-overprivileged-user
aws iam attach-user-policy --user-name lab-overprivileged-user \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess  # TOO BROAD

# Create access key without rotation
aws iam create-access-key --user-name lab-overprivileged-user

# Disable CloudTrail (INTENTIONALLY INSECURE for lab)
TRAIL_NAME=$(aws cloudtrail list-trails --query 'Trails[0].Name' --output text)
if [ "$TRAIL_NAME" != "None" ]; then
    aws cloudtrail stop-logging --name $TRAIL_NAME
fi

# Create security group with 0.0.0.0/0 to port 22 (SSH open to world)
VPC_ID=$(aws ec2 describe-vpcs --query 'Vpcs[0].VpcId' --output text)
SG_ID=$(aws ec2 create-security-group --group-name lab-open-sg \
  --description "Intentionally insecure lab" --vpc-id $VPC_ID \
  --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
  --protocol tcp --port 22 --cidr 0.0.0.0/0  # TOO OPEN

Part 2: Automated Assessment with Prowler

# Run Prowler against your lab account
# Full CIS AWS Foundations Benchmark Level 1
prowler aws --compliance cis_1.5_aws --output-formats html,csv --output-directory ./prowler-results/

# Quick check (faster — just critical/high):
prowler aws -S --severity critical high --output-formats json-ocsf --output-directory ./prowler-quick/

# Check specific services
prowler aws -c s3_bucket_public_access s3_bucket_default_encryption \
  iam_no_root_access_key iam_policy_no_star_star cloudtrail_multi_region_enabled

# Review HTML report
# Open: ./prowler-results/prowler-output-*.html

2.1 Analyze Prowler Findings

After running Prowler, categorize findings by:

import json, collections

# Parse Prowler OCSF output
with open("./prowler-quick/prowler-output-*.json") as f:
    findings = [json.loads(line) for line in f if line.strip()]

# Group by severity
severity_counts = collections.Counter(f['severity'] for f in findings)
print("Findings by severity:")
for sev, count in severity_counts.most_common():
    print(f"  {sev}: {count}")

# Top finding types
failed = [f for f in findings if f.get('status') == 'FAILED']
print(f"\nTotal FAILED checks: {len(failed)}")
for f in failed[:10]:
    print(f"  [{f['severity']}] {f['check_id']}: {f['check_title'][:80]}")

Part 3: Manual Investigation

3.1 S3 Bucket Assessment

# Find all S3 buckets
aws s3 ls

# Check public access status for each bucket
for bucket in $(aws s3 ls | awk '{print $3}'); do
    echo "=== $bucket ==="
    aws s3api get-bucket-public-access-block --bucket $bucket 2>/dev/null || echo "No block public access policy"
    aws s3api get-bucket-policy-status --bucket $bucket 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print('Policy: PUBLIC' if d['PolicyStatus']['IsPublic'] else 'Policy: private')" 2>/dev/null
    aws s3api get-bucket-acl --bucket $bucket | python3 -c "
import sys,json
d=json.load(sys.stdin)
for grant in d['Grants']:
    if 'AllUsers' in str(grant) or 'AllUsers' in str(grant.get('Grantee',{}).get('URI','')):
        print(f'PUBLIC ACL GRANT: {grant[\"Permission\"]}')
"
done

3.2 IAM Assessment

# Download IAM credential report
aws iam generate-credential-report
sleep 5  # Wait for generation
aws iam get-credential-report --query 'Content' --output text | base64 -d > iam-report.csv

# Analyze credential report
python3 << 'EOF'
import csv, datetime

with open('iam-report.csv') as f:
    reader = csv.DictReader(f)
    for row in reader:
        user = row['user']

        # Check: access keys older than 90 days
        for key_num in ['1', '2']:
            key_date = row.get(f'access_key_{key_num}_last_rotated', 'N/A')
            if key_date not in ('N/A', 'no_information', ''):
                try:
                    rotated = datetime.datetime.strptime(key_date[:19], '%Y-%m-%dT%H:%M:%S')
                    days = (datetime.datetime.utcnow() - rotated).days
                    if days > 90:
                        print(f"OLD KEY ({days}d): {user} - key {key_num}")
                except:
                    pass

        # Check: no MFA
        if row.get('mfa_active') == 'false' and row.get('password_enabled') == 'true':
            print(f"NO MFA: {user}")

        # Check: root account key exists
        if row.get('user') == '<root_account>' and row.get('access_key_1_active') == 'true':
            print(f"CRITICAL: Root access key exists!")
EOF

# Check for overly permissive policies (iam:* or AdministratorAccess)
aws iam list-users --query 'Users[].UserName' --output text | \
tr '\t' '\n' | while read user; do
    aws iam list-attached-user-policies --user-name "$user" \
      --query 'AttachedPolicies[?PolicyArn==`arn:aws:iam::aws:policy/AdministratorAccess`]' \
      --output text | grep -q "AdministratorAccess" && \
    echo "Admin policy attached to user: $user"
done

3.3 CloudTrail and GuardDuty Status

# Check CloudTrail status
echo "=== CloudTrail Status ==="
aws cloudtrail get-trail-status --name $(aws cloudtrail list-trails --query 'Trails[0].Name' --output text) \
  --query '{Logging:IsLogging,LatestDelivery:LatestDeliveryTime}' 2>/dev/null || echo "No trails configured"

# Check if multi-region trail exists
aws cloudtrail describe-trails --query 'trailList[?IsMultiRegionTrail==`true`].Name' --output text

# Check GuardDuty status
echo "=== GuardDuty Status ==="
aws guardduty list-detectors --query 'DetectorIds' --output text | while read id; do
    aws guardduty get-detector --detector-id "$id" --query '{Status:Status,FindingFrequency:FindingPublishingFrequency}'
done 2>/dev/null || echo "GuardDuty not enabled in this region"

# Check Config
echo "=== AWS Config Status ==="
aws configservice get-configuration-recorder-status \
  --query 'ConfigurationRecordersStatus[].recording' 2>/dev/null || echo "Config not configured"

3.4 VPC and Network Assessment

# Find security groups with 0.0.0.0/0 ingress (dangerous ports)
echo "=== Open Security Groups ==="
aws ec2 describe-security-groups \
  --query 'SecurityGroups[*].{Name:GroupName,Id:GroupId,Rules:IpPermissions[*].{Port:FromPort,CIDR:IpRanges[?CidrIp==`0.0.0.0/0`].CidrIp}}' \
  --output json | python3 -c "
import sys,json
for sg in json.load(sys.stdin):
    for rule in sg['Rules']:
        if rule['CIDR']:
            port = rule.get('Port','All')
            if port in [22, 3389, 5432, 3306, 27017, None] or str(port) in ['22','3389']:
                print(f'OPEN {port}: {sg[\"Name\"]} ({sg[\"Id\"]})')
"

# Check if default VPC exists (should be deleted in prod)
aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
  --query 'Vpcs[].VpcId' --output text

Part 4: Remediation

4.1 S3 Public Access Block

# Fix: Enable Account-Level S3 Public Access Block
aws s3control put-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text) \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Verify
aws s3control get-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text)

4.2 IAM Fixes

# Re-enable CloudTrail
aws cloudtrail start-logging --name $(aws cloudtrail list-trails --query 'Trails[0].Name' --output text)

# Detach AdministratorAccess from over-privileged user
aws iam detach-user-policy \
  --user-name lab-overprivileged-user \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Attach minimal policy instead (example: S3 read-only for specific bucket)
aws iam create-policy --policy-name LabMinimalPolicy \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::'"$BUCKET_NAME"'", "arn:aws:s3:::'"$BUCKET_NAME"'/*"]
    }]
  }'

4.3 Verify Remediation with Prowler

# Re-run specific checks after remediation
prowler aws -c s3_bucket_public_access iam_no_star_star cloudtrail_multi_region_enabled

# Compare with initial scan
diff prowler-results-initial.csv prowler-results-remediated.csv | grep "^[<>]"

Part 5: IaC Security (Bonus)

5.1 Scan CloudFormation Templates with Checkov

# Create a CloudFormation template with intentional issues
cat > vulnerable.yaml << 'TEMPLATE'
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  InsecureBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-insecure-bucket
      # Missing: PublicAccessBlockConfiguration, BucketEncryption

  InsecureSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Open security group
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0  # Bad practice
TEMPLATE

# Scan with Checkov
checkov -f vulnerable.yaml --framework cloudformation

# Expected: Multiple failures including:
# CKV_AWS_18: Ensure the S3 bucket has access logging enabled
# CKV_AWS_21: Ensure the S3 bucket has versioning enabled
# CKV2_AWS_6: Ensure that S3 Public Access Block is enabled

Part 6: Cleanup

# IMPORTANT: Clean up all lab resources to avoid charges
aws s3 rm s3://$BUCKET_NAME --recursive
aws s3 rb s3://$BUCKET_NAME

aws iam detach-user-policy --user-name lab-overprivileged-user \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam delete-access-key --user-name lab-overprivileged-user \
  --access-key-id $(aws iam list-access-keys --user-name lab-overprivileged-user \
  --query 'AccessKeyMetadata[0].AccessKeyId' --output text)
aws iam delete-user --user-name lab-overprivileged-user

aws ec2 delete-security-group --group-id $SG_ID

Lab Deliverables

  1. Prowler Report: Screenshot or export of all FAILED findings
  2. Findings Summary: Table of top 5 findings with severity, description, remediation
  3. Remediation Evidence: Before/After screenshots for 3 findings
  4. IaC Scan Results: Checkov output with identified issues

Lab Questions

  1. How many CRITICAL findings did Prowler identify in your initial scan? List the top 3.
  2. Explain why an IAM user with AdministratorAccess is a security risk.
  3. What specific attack could a threat actor perform using an exposed S3 bucket?
  4. What is the difference between an AWS-managed policy and a customer-managed policy?
  5. If CloudTrail logging is disabled, what evidence would be missing from an IR investigation?