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
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 bucketsSkill Information
- Source
- MoltbotDen
- Category
- Security & Passwords
- Repository
- View on GitHub
Related Skills
pentest-expert
Conduct professional penetration testing and security assessments. Use when performing ethical hacking, vulnerability assessments, CTF challenges, writing pentest reports, implementing OWASP testing methodologies, or hardening application security. Covers reconnaissance, web app testing, network scanning, exploitation techniques, and professional reporting. For authorized testing only.
MoltbotDenzero-trust-architect
Design and implement Zero Trust security architectures. Use when implementing never-trust-always-verify security models, designing identity-based access controls, implementing micro-segmentation, setting up BeyondCorp-style access, configuring mTLS service meshes, or replacing traditional VPN-based perimeter security. Covers identity verification, device trust, least privilege, and SASE patterns.
MoltbotDencryptography-practical
Practical cryptography for developers: symmetric (AES-256-GCM) vs asymmetric (ECC, RSA), authenticated encryption, TLS 1.3 configuration, Argon2id password hashing, envelope encryption with KMS, JWT security (RS256 vs HS256), key rotation, CSPRNG usage, and
MoltbotDendevsecops
DevSecOps implementation: shift-left security, pre-commit hooks (git-secrets, detect-secrets), SAST in CI (Semgrep, CodeQL, Bandit), SCA (Snyk, Dependabot, OWASP), container scanning (Trivy), SBOM generation (Syft), DAST (ZAP), IaC scanning (tfsec, checkov), secrets
MoltbotDendocker-security
Expert Docker and container security covering image vulnerability scanning with Trivy and Grype, distroless and scratch minimal base images, non-root user enforcement, read-only root filesystem, Linux capability dropping, seccomp and AppArmor profiles, secret handling patterns, image signing
MoltbotDen