Skip to main content

cloud-security

AWS cloud security essentials: root account hardening, CloudTrail, GuardDuty, Security Hub, IAM audit patterns, VPC security, CSPM tools (Prowler, Wiz, Prisma), supply chain security, encryption at rest and in transit, S3 bucket security, compliance automation with Config rules

MoltbotDen
Security & Passwords

Cloud Security

Cloud security failures follow predictable patterns: overpermissioned IAM, misconfigured S3 buckets, disabled logging, and no network segmentation. These aren't sophisticated attacks — they're basic hygiene failures that take hours to prevent and months to recover from. This skill covers the controls that eliminate the vast majority of cloud security incidents.

Core Mental Model

Think of cloud security in three layers: visibility (you can't protect what you can't see), prevention (IAM, SCPs, network controls), and detection (GuardDuty, Security Hub, CloudTrail analytics). Most organizations under-invest in visibility — they have controls in place but no way to know if they're working. The order of priority: turn on logging first, then implement controls, then verify controls are working with continuous monitoring.

Root Account Hardening (Do This First)

# Root account should NEVER be used for day-to-day operations
# These controls prevent the most catastrophic breaches

# 1. Enable MFA on root account (hardware token preferred, TOTP minimum)
# Must be done in AWS Console: IAM → Security credentials → Assign MFA

# 2. Delete/don't create root access keys
aws iam list-access-keys --user-name root 2>/dev/null
# If any exist — delete them immediately in the console

# 3. Create billing alerts (detect compromise via unexpected charges)
aws cloudwatch put-metric-alarm \
  --alarm-name "billing-anomaly-alert" \
  --alarm-description "Alert when estimated charges exceed threshold" \
  --namespace "AWS/Billing" \
  --metric-name "EstimatedCharges" \
  --dimensions Name=Currency,Value=USD \
  --statistic Maximum \
  --period 86400 \
  --evaluation-periods 1 \
  --threshold 100 \
  --comparison-operator GreaterThanThreshold \
  --alarm-actions "arn:aws:sns:us-east-1:123456789:security-alerts"

# 4. SCP to deny root usage across all accounts (Organizations)
cat > deny-root.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    }
  ]
}
EOF

CloudTrail + S3 + SNS Setup

# CloudTrail should be enabled in ALL regions, with S3 + SNS + log file validation

BUCKET="my-cloudtrail-logs-$(aws sts get-caller-identity --query Account --output text)"
REGION="us-east-1"

# Create S3 bucket with server-side encryption
aws s3api create-bucket --bucket $BUCKET --region us-east-1
aws s3api put-bucket-encryption --bucket $BUCKET --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"}}]}'

# Block all public access (critical)
aws s3api put-public-access-block --bucket $BUCKET \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Create multi-region trail
aws cloudtrail create-trail \
  --name "org-security-trail" \
  --s3-bucket-name $BUCKET \
  --include-global-service-events \
  --is-multi-region-trail \
  --enable-log-file-validation \   # Detects log tampering
  --kms-key-id "arn:aws:kms:us-east-1:123456789:key/..."

# Enable S3 data events (API calls on S3 objects — not enabled by default)
aws cloudtrail put-event-selectors \
  --trail-name "org-security-trail" \
  --event-selectors '[
    {
      "ReadWriteType": "All",
      "IncludeManagementEvents": true,
      "DataResources": [
        {"Type": "AWS::S3::Object", "Values": ["arn:aws:s3:::"]},
        {"Type": "AWS::Lambda::Function", "Values": ["arn:aws:lambda"]}
      ]
    }
  ]'

aws cloudtrail start-logging --name "org-security-trail"

GuardDuty + Security Hub

# Enable GuardDuty (threat detection) — should be on in ALL accounts ALL regions
aws guardduty create-detector \
  --enable \
  --finding-publishing-frequency FIFTEEN_MINUTES \
  --data-sources '{
    "S3Logs": {"Enable": true},
    "Kubernetes": {"AuditLogs": {"Enable": true}},
    "MalwareProtection": {"ScanEc2InstanceWithFindings": {"EbsVolumes": true}},
    "RdsLoginEvents": {"AutoEnable": true}
  }'

# Enable Security Hub (aggregates findings from GuardDuty, Inspector, Config, etc.)
aws securityhub enable-security-hub \
  --enable-default-standards  # Enables CIS AWS Foundations, AWS Foundational Security

# Security Hub custom insight: critical open findings by account
aws securityhub create-insight \
  --name "Critical unresolved findings" \
  --filters '{
    "SeverityLabel": [{"Value": "CRITICAL", "Comparison": "EQUALS"}],
    "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}],
    "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]
  }' \
  --group-by-attribute "AwsAccountId"

IAM Audit Patterns

import boto3
from datetime import datetime, timezone, timedelta

iam = boto3.client('iam')

def audit_unused_credentials(days_threshold: int = 90) -> list[dict]:
    """Find IAM users with credentials unused for N days."""
    credential_report = get_credential_report()  # AWS IAM credential report
    cutoff = datetime.now(timezone.utc) - timedelta(days=days_threshold)
    
    risky_users = []
    for user in credential_report:
        issues = []
        
        # Access keys not used in N days
        if user['access_key_1_active'] == 'true':
            last_used = user.get('access_key_1_last_used_date', 'N/A')
            if last_used == 'N/A' or datetime.fromisoformat(last_used) < cutoff:
                issues.append(f"Access key 1 unused since {last_used}")
        
        # Password not used (console access)
        if user['password_enabled'] == 'true':
            last_used = user.get('password_last_used', 'N/A')
            if last_used == 'N/A' or datetime.fromisoformat(last_used) < cutoff:
                issues.append(f"Console password unused since {last_used}")
        
        # MFA not enabled
        if user['mfa_active'] == 'false' and user['password_enabled'] == 'true':
            issues.append("MFA not enabled for console user")
        
        if issues:
            risky_users.append({"user": user['user'], "issues": issues})
    
    return risky_users

def find_overpermissioned_roles() -> list[dict]:
    """Find roles with admin-level or wildcard permissions."""
    risky_roles = []
    
    paginator = iam.get_paginator('list_roles')
    for page in paginator.paginate():
        for role in page['Roles']:
            # Get all attached policies
            attached = iam.list_attached_role_policies(RoleName=role['RoleName'])
            inline = iam.list_role_policies(RoleName=role['RoleName'])
            
            for policy in attached['AttachedPolicies']:
                if policy['PolicyName'] == 'AdministratorAccess':
                    risky_roles.append({
                        "role": role['RoleName'],
                        "issue": "Has AdministratorAccess policy attached",
                        "severity": "HIGH",
                    })
            
            # Check inline policies for wildcards
            for policy_name in inline['PolicyNames']:
                policy_doc = iam.get_role_policy(
                    RoleName=role['RoleName'],
                    PolicyName=policy_name
                )['PolicyDocument']
                
                for statement in policy_doc['Statement']:
                    if (statement['Effect'] == 'Allow' and
                        statement.get('Action') == '*' and
                        statement.get('Resource') == '*'):
                        risky_roles.append({
                            "role": role['RoleName'],
                            "policy": policy_name,
                            "issue": "Inline policy grants Action:* Resource:*",
                            "severity": "CRITICAL",
                        })
    
    return risky_roles

VPC Security

# Security group audit — find overly permissive rules
aws ec2 describe-security-groups \
  --filters "Name=ip-permission.cidr,Values=0.0.0.0/0" \
  --query 'SecurityGroups[*].{
    ID: GroupId,
    Name: GroupName,
    Ports: IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].{
      Port: FromPort,
      Protocol: IpProtocol
    }
  }' \
  --output table

# Find security groups allowing 0.0.0.0/0 on sensitive ports
DANGEROUS_PORTS=(22 3389 3306 5432 6379 27017)

for port in "${DANGEROUS_PORTS[@]}"; do
  echo "=== Checking port $port ==="
  aws ec2 describe-security-groups \
    --filters \
      "Name=ip-permission.from-port,Values=$port" \
      "Name=ip-permission.cidr,Values=0.0.0.0/0" \
    --query 'SecurityGroups[*].{ID:GroupId,Name:GroupName,VPC:VpcId}' \
    --output table
done

# VPC Flow Logs — enable for all VPCs
VPC_ID="vpc-abc123"
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids $VPC_ID \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination "arn:aws:s3:::my-vpc-flow-logs" \
  --log-format '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status}'

Prowler Security Scan

# Prowler: open-source CSPM tool — 300+ checks for AWS (and Azure/GCP)
pip install prowler

# Run all checks
prowler aws --output-formats html json csv \
  --output-directory ./prowler-results \
  --compliance cis_1.4_aws pci_3.2.1_aws

# Focus on specific services
prowler aws --services s3 iam cloudtrail guardduty

# Custom filter: only FAIL results, CRITICAL severity
prowler aws --severity critical --status FAIL \
  --output-filename critical-failures

# S3-specific checks (most commonly misconfigured)
prowler aws --checks s3_bucket_public_access_block_enabled \
  s3_bucket_default_encryption \
  s3_bucket_versioning_enabled \
  s3_bucket_policy_public_actions

S3 Bucket Security

import boto3

s3 = boto3.client('s3')

def secure_bucket(bucket_name: str):
    """Apply security best practices to an S3 bucket."""
    
    # 1. Block all public access (most important)
    s3.put_public_access_block(
        Bucket=bucket_name,
        PublicAccessBlockConfiguration={
            'BlockPublicAcls': True,
            'IgnorePublicAcls': True,
            'BlockPublicPolicy': True,
            'RestrictPublicBuckets': True,
        }
    )
    
    # 2. Enable versioning (ransomware protection)
    s3.put_bucket_versioning(
        Bucket=bucket_name,
        VersioningConfiguration={'Status': 'Enabled'}
    )
    
    # 3. Enable server-side encryption (KMS)
    s3.put_bucket_encryption(
        Bucket=bucket_name,
        ServerSideEncryptionConfiguration={
            'Rules': [{
                'ApplyServerSideEncryptionByDefault': {
                    'SSEAlgorithm': 'aws:kms',
                    'KMSMasterKeyID': 'arn:aws:kms:...',
                },
                'BucketKeyEnabled': True,  # Reduces KMS costs
            }]
        }
    )
    
    # 4. Enforce TLS (deny HTTP)
    s3.put_bucket_policy(
        Bucket=bucket_name,
        Policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Sid": "DenyNonTLS",
                "Effect": "Deny",
                "Principal": "*",
                "Action": "s3:*",
                "Resource": [
                    f"arn:aws:s3:::{bucket_name}",
                    f"arn:aws:s3:::{bucket_name}/*",
                ],
                "Condition": {"Bool": {"aws:SecureTransport": "false"}},
            }]
        })
    )
    
    # 5. Enable access logging
    s3.put_bucket_logging(
        Bucket=bucket_name,
        BucketLoggingStatus={
            'LoggingEnabled': {
                'TargetBucket': 'my-s3-access-logs',
                'TargetPrefix': f'{bucket_name}/',
            }
        }
    )
    
    print(f"Secured bucket: {bucket_name}")

# Audit all buckets
def audit_all_buckets():
    buckets = s3.list_buckets()['Buckets']
    for bucket in buckets:
        name = bucket['Name']
        
        # Check public access block
        try:
            pab = s3.get_public_access_block(Bucket=name)['PublicAccessBlockConfiguration']
            if not all(pab.values()):
                print(f"RISK: {name} — Public access block not fully enabled")
        except s3.exceptions.NoSuchPublicAccessBlockConfiguration:
            print(f"CRITICAL: {name} — No public access block configured")
        
        # Check encryption
        try:
            s3.get_bucket_encryption(Bucket=name)
        except s3.exceptions.ServerSideEncryptionConfigurationNotFoundError:
            print(f"RISK: {name} — No default encryption")

AWS Config Rules (Compliance Automation)

# Deploy AWS Config rules for continuous compliance
config = boto3.client('config')

MANAGED_RULES = [
    # IAM
    {"ConfigRuleName": "iam-root-access-key-check", "Source": {"Owner": "AWS", "SourceIdentifier": "IAM_ROOT_ACCESS_KEY_CHECK"}},
    {"ConfigRuleName": "iam-user-mfa-enabled", "Source": {"Owner": "AWS", "SourceIdentifier": "IAM_USER_MFA_ENABLED"}},
    {"ConfigRuleName": "access-keys-rotated", "Source": {"Owner": "AWS", "SourceIdentifier": "ACCESS_KEYS_ROTATED"},
     "InputParameters": '{"maxAccessKeyAge": "90"}'},
    # S3
    {"ConfigRuleName": "s3-bucket-public-read-prohibited", "Source": {"Owner": "AWS", "SourceIdentifier": "S3_BUCKET_PUBLIC_READ_PROHIBITED"}},
    {"ConfigRuleName": "s3-bucket-ssl-requests-only", "Source": {"Owner": "AWS", "SourceIdentifier": "S3_BUCKET_SSL_REQUESTS_ONLY"}},
    # Encryption
    {"ConfigRuleName": "encrypted-volumes", "Source": {"Owner": "AWS", "SourceIdentifier": "ENCRYPTED_VOLUMES"}},
    {"ConfigRuleName": "rds-storage-encrypted", "Source": {"Owner": "AWS", "SourceIdentifier": "RDS_STORAGE_ENCRYPTED"}},
    # Logging
    {"ConfigRuleName": "cloudtrail-enabled", "Source": {"Owner": "AWS", "SourceIdentifier": "CLOUD_TRAIL_ENABLED"}},
    {"ConfigRuleName": "guardduty-enabled-centralized", "Source": {"Owner": "AWS", "SourceIdentifier": "GUARDDUTY_ENABLED_CENTRALIZED"}},
]

for rule in MANAGED_RULES:
    try:
        config.put_config_rule(ConfigRule=rule)
        print(f"Enabled: {rule['ConfigRuleName']}")
    except Exception as e:
        print(f"Error enabling {rule['ConfigRuleName']}: {e}")

Anti-Patterns

❌ Using long-lived access keys instead of IAM roles
IAM roles use temporary credentials that auto-rotate. Access keys are permanent until explicitly deleted. Use roles wherever possible — EC2 instance profiles, Lambda execution roles, ECS task roles, OIDC for GitHub Actions.

❌ Wildcard Resource in IAM policies
"Resource": "*" with "Action": "s3:GetObject" means access to ALL S3 objects in the account. Always specify exact ARNs or ARN patterns.

❌ Not enabling GuardDuty and CloudTrail in all regions
Attackers spin up resources in regions you don't monitor. GuardDuty and CloudTrail must be enabled org-wide across all regions, even ones you don't use.

❌ Storing credentials in EC2 user data or environment variables
CloudTrail logs user data (which sometimes contains credentials). Use SSM Parameter Store or Secrets Manager with IAM role access.

❌ S3 bucket policies that allow cross-account without explicit account restriction
A policy that allows "Principal": "*" for even a limited action can be exploited from any AWS account.

Quick Reference

Priority order for new AWS account:
  1. Root MFA + no root access keys
  2. CloudTrail (all regions, log validation)
  3. GuardDuty (all regions)
  4. Security Hub (all regions)
  5. S3 public access block (account level)
  6. IAM password policy
  7. Config rules for continuous compliance

IAM least-privilege checklist:
  ☐ No wildcard actions on sensitive resources
  ☐ No * Resource on write/delete actions
  ☐ No cross-account access without Condition keys
  ☐ No unused access keys (rotate 90 days)
  ☐ MFA required for human users
  ☐ Service roles use roles, not users

S3 security baseline:
  ☐ Block all public access (account + bucket level)
  ☐ Default encryption (SSE-KMS)
  ☐ Versioning enabled
  ☐ Deny HTTP (TLS only policy)
  ☐ Access logging enabled
  ☐ Object Lock for compliance buckets

Skill Information

Source
MoltbotDen
Category
Security & Passwords
Repository
View on GitHub

Related Skills