HACKADEMICS

Cloud Security Configuration Best Practices for DevOps Teams

Your cloud infrastructure is probably misconfigured right now. I've reviewed enough breach postmortems to know that attackers don't need zero-days when you've left an S3 bucket public, over-privileged an IAM role, or forgotten to enable CloudTrail. Let's fix that before someone else finds it.

The Brutal Reality of Cloud Misconfigurations

DevOps teams move fast. Cloud providers make deployment easy. The deadly combination? They also make it easy to deploy things insecurely, and "we'll harden it later" never happens.

Capital One in 2019? Overprivileged IAM role plus SSRF. Pegasus Airlines leaked 23 million records because someone clicked "make public" on an S3 bucket. Tesla got cryptominers because their Kubernetes dashboard sat on the internet without auth.

None of these needed sophisticated exploits. They needed someone to try default credentials or run a bucket scanner.

Most cloud breaches are preventable with proper configuration. But you need to actually implement it, and that requires discipline your sprint velocity probably doesn't account for.

Storage: Lock It Down Before Someone Else Does

Public cloud storage is the easiest way to make headlines for the wrong reasons. Here's how to not become the next data leak cautionary tale.

S3 Bucket Hardening That Actually Works

Enable Block Public Access at the account level. Not per bucket. Not "just for production." Account-wide, immediately.

# Block public access across your entire AWS account
aws s3control put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Yeah, this breaks that static site someone deployed six months ago. Good. Static sites belong behind CloudFront with Origin Access Identity, not sitting as public buckets.

Audit existing buckets because someone definitely went rogue:

# Check all buckets for public access
aws s3api list-buckets --query 'Buckets[].Name' --output text | while read bucket; do
  echo "Checking: $bucket"
  aws s3api get-bucket-acl --bucket $bucket
  aws s3api get-bucket-policy-status --bucket $bucket 2>/dev/null || echo "No policy configured"
done

Enable versioning and object lock. When credentials leak, versioning prevents attackers from permanently deleting your evidence:

# Enable versioning
aws s3api put-bucket-versioning \
  --bucket critical-data \
  --versioning-configuration Status=Enabled

# Object lock for compliance and incident response
aws s3api put-object-lock-configuration \
  --bucket critical-data \
  --object-lock-configuration \
  'ObjectLockEnabled=Enabled,Rule={DefaultRetention={Mode=GOVERNANCE,Days=30}}'

Azure Storage: Same Story, Different Cloud

Azure makes public blob access way too easy. Shut it down at the account level:

# Disable anonymous blob access on the storage account
az storage account update \
  --name prodstorageacct \
  --resource-group production-rg \
  --allow-blob-public-access false

# Find containers that someone made public anyway
az storage container list \
  --account-name prodstorageacct \
  --query "[?properties.publicAccess != 'None'].name"

If that query returns results, someone ignored security. Fix it now.

IAM: Least Privilege Isn't a Suggestion

Identity and Access Management is where teams create their own nightmare scenarios. Least privilege isn't optional—it's the difference between a contained incident and total account compromise.

Kill the Managed Policy Habit

AWS managed policies like AdministratorAccess or PowerUserAccess are training wheels. Remove them. Build custom policies scoped to exactly what's needed:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::production-uploads/*"
    },
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/abc123def-456g-789h-012i-345jklmnopqr"
    }
  ]
}

Notice the specificity? Not all S3 actions. Not all buckets. Not all KMS keys. Just what's required.

Audit for Privilege Creep

IAM Access Analyzer tells you what permissions are actually used versus what's granted:

# Create analyzer
aws accessanalyzer create-analyzer \
  --analyzer-name prod-analyzer \
  --type ACCOUNT

# Review overly permissive access
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/prod-analyzer

Run this monthly. Delete unused permissions. If a role hasn't touched a permission in 90 days, it doesn't need it.

MFA is Non-Negotiable

Enforce multi-factor authentication for console access. No exceptions for "emergency" accounts—especially not those.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyAllWithoutMFA",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ListVirtualMFADevices",
        "iam:ResyncMFADevice",
        "sts:GetSessionToken"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

Attach this to all human users. They can enable MFA and nothing else until it's active.

Azure RBAC: Scope Properly or Suffer

Azure role assignments work at subscription, resource group, or resource scope. Always use the narrowest scope:

# Right: Assign at resource level
az role assignment create \
  --assignee dev-team-sp \
  --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/sub-id/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/prodstore"

# Wrong: Contributor at subscription level - lateral movement paradise

Network Security: Default Deny Everything

If your security groups allow 0.0.0.0/0 on ingress, we have a problem.

Security Groups Are Stateful, Use That

AWS security groups are stateful. Only allow inbound; responses are automatic. Audit for the overly permissive:

# Find security groups allowing internet traffic
aws ec2 describe-security-groups \
  --query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].{ID:GroupId,Name:GroupName}" \
  --output table

If this returns anything beyond load balancers on 443, investigate immediately.

Build with zero trust:

  1. Web tier: Allow 443 from CloudFront or ALB security group
  2. App tier: Allow specific ports from web tier security group
  3. Database tier: Allow DB port from app tier security group only
  4. Management: Use Systems Manager Session Manager, not SSH/RDP from the internet

VPC Flow Logs: Visibility is Security

Enable VPC Flow Logs for every VPC. Not just production. Staging will be the compromised one:

# Enable flow logs to CloudWatch
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids vpc-abc123 \
  --traffic-type ALL \
  --log-destination-type cloud-watch-logs \
  --log-group-name /aws/vpc/flowlogs/production

Ship these to your SIEM. When incident response starts, you'll want to know what connected where and when.

Private Subnets for Non-Public Resources

Public subnets should only contain:

  • Internet gateways
  • NAT gateways
  • Load balancers

Everything else—app servers, databases, Lambda—goes in private subnets with no internet gateway route.

Secrets Management: Git is Not a Vault

I've found AWS credentials in public GitHub repos more times than I care to count. Use proper secrets management or accept that your keys will leak.

Secrets Manager and Rotation

Store credentials, API keys, and secrets in dedicated services:

import boto3
import json

def get_database_creds():
    client = boto3.client('secretsmanager', region_name='us-east-1')
    response = client.get_secret_value(SecretId='prod/db/creds')
    secret = json.loads(response['SecretString'])
    return secret['username'], secret['password']

Enable automatic rotation:

aws secretsmanager rotate-secret \
  --secret-id prod/db/creds \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSRotation \
  --rotation-rules AutomaticallyAfterDays=30

Don't Do This With Secrets

Still seeing this in production:

# Wrong - avoid this pattern
export DB_PASSWORD="SuperSecret123"
docker run -e DB_PASSWORD=$DB_PASSWORD myapp

Use secrets injection, not environment variables. And never commit .env files.

Logging: Log Everything, Alert on Anomalies

CloudTrail is Mandatory

CloudTrail logs every API call in your AWS account. Without it, you won't know when compromised credentials are being used:

# Create multi-region trail with validation
aws cloudtrail create-trail \
  --name org-trail \
  --s3-bucket-name cloudtrail-logs \
  --is-multi-region-trail \
  --enable-log-file-validation

aws cloudtrail start-logging --name org-trail

Store logs in a separate security account. When an app account gets compromised, you don't want attackers deleting evidence.

Alert on Suspicious Activity

CloudWatch alarms for activities that should be rare:

aws cloudwatch put-metric-alarm \
  --alarm-name root-account-usage \
  --alarm-description "Root account should never be used" \
  --metric-name RootAccountUsage \
  --namespace CloudTrailMetrics \
  --statistic Sum \
  --period 60 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1

Monitor for:

  • Root account logins (should be zero)
  • Security group modifications
  • IAM policy changes
  • Authentication attempts exceeding threshold
  • Unusual API patterns by region or service

Encryption: At Rest and In Transit

Default Encryption for Storage

Enable encryption by default for all storage:

# S3 default encryption with KMS
aws s3api put-bucket-encryption \
  --bucket prod-data \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms","KMSMasterKeyID":"arn:aws:kms:us-east-1:123456789012:key/abc-123"}}]}'

# EBS encryption by default for entire region
aws ec2 enable-ebs-encryption-by-default --region us-east-1

Use customer-managed KMS keys, not AWS-managed. You want control over rotation and access policies.

TLS 1.2 Minimum Always

Configure load balancers to reject anything older:

aws elbv2 modify-listener \
  --listener-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/prod-alb/abc123/def456 \
  --ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01

TLS 1.0 and 1.1 are deprecated and vulnerable. No legitimate reason to support them.

Infrastructure as Code: Policy Enforcement

Manual configuration doesn't scale and creates drift. Use IaC with automated checks.

Terraform with Policy Validation

Define infrastructure in code and enforce security policies:

resource "aws_s3_bucket" "data" {
  bucket = "prod-data-bucket"
  
  versioning {
    enabled = true
  }
  
  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "aws:kms"
        kms_master_key_id = aws_kms_key.bucket_key.arn
      }
    }
  }
  
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Use Open Policy Agent to block bad configurations:

package terraform.analysis

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  not resource.change.after.block_public_acls
  msg := sprintf("S3 bucket %s must block public ACLs", [resource.address])
}

Implementation Checklist

Your sprint backlog for this week:

Storage:

  • Enable account-level Block Public Access
  • Audit existing buckets for public access
  • Enable versioning on all data buckets
  • Configure default encryption with customer-managed keys

IAM:

  • Replace managed policies with custom least-privilege policies
  • Enforce MFA for all console users
  • Enable IAM Access Analyzer
  • Remove unused credentials and roles

Network:

  • Audit security groups for unrestricted ingress
  • Enable VPC Flow Logs
  • Move workloads to private subnets
  • Implement security group chaining

Secrets:

  • Migrate hardcoded secrets to Secrets Manager
  • Enable automatic credential rotation
  • Scan repos for leaked credentials
  • Remove secrets from environment variables

Logging:

  • Enable CloudTrail with log validation
  • Configure alerts for suspicious activity
  • Ship logs to centralized SIEM
  • Set up compliance scanning

Encryption:

  • Enable default storage encryption
  • Enforce TLS 1.2 minimum on endpoints
  • Migrate to customer-managed KMS keys
  • Audit for unencrypted resources

The Bottom Line

Cloud security isn't difficult, but it requires discipline. The configurations that lead to breaches aren't exotic—they're basic settings teams skip because of velocity pressure.

Treat every environment like production from a security standpoint. Your staging environment has production credentials. Your dev environment connects to real services. Attackers know this.

Build security into your deployment pipeline. Make it impossible to deploy insecure configurations. When someone asks to "just quickly open up that security group," the answer is no.

Attackers are already scanning for your misconfigurations. Don't make it easy for them.