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¶
- Prowler Report: Screenshot or export of all FAILED findings
- Findings Summary: Table of top 5 findings with severity, description, remediation
- Remediation Evidence: Before/After screenshots for 3 findings
- IaC Scan Results: Checkov output with identified issues
Lab Questions¶
- How many CRITICAL findings did Prowler identify in your initial scan? List the top 3.
- Explain why an IAM user with AdministratorAccess is a security risk.
- What specific attack could a threat actor perform using an exposed S3 bucket?
- What is the difference between an AWS-managed policy and a customer-managed policy?
- If CloudTrail logging is disabled, what evidence would be missing from an IR investigation?