๐ง OWASP Top 10 โ A05:2021 Security Misconfiguration
The Silent Killer: How Default Settings, Forgotten Debug Modes, and Missing Headers Bring Down Entire Systems
"The most dangerous vulnerability is the one you didn't know you had โ because you never configured it."
>
This blog dissects A05:2021 โ Security Misconfiguration down to the molecular level. By the end, you'll understand why 90% of cloud breaches start with a single misconfigured setting, and how to never make that mistake again.
๐ Table of Contents
- Introduction โ Why Misconfiguration Is the #1 Cloud Breach Vector
- What Exactly Is Security Misconfiguration?
- The Anatomy of a Misconfiguration
- Real-World Breaches Caused by Misconfiguration
- Technical Deep-Dive: CWE Mapping
- The 10 Most Common Misconfigurations
- Cloud Misconfigurations: The Modern Frontier
- Container & Kubernetes Misconfigurations
- Security Headers: The Easiest Wins You're Missing
- Secure Configuration Patterns
- Hardening Guides by Technology
- Automation: Infrastructure as Code Security
- Code & Config Examples: Vulnerable vs Secure
- Tools for Detecting Misconfigurations
- The Ultimate Checklist
- Conclusion
๐ช Introduction โ Why Misconfiguration Is the #1 Cloud Breach Vector
If you look at the headlines of major data breaches over the past five years, you'll notice something terrifying: an overwhelming number of them did not involve sophisticated zero-day exploits, nation-state hackers, or genius-level code manipulation.
They involved one setting. One checkbox. One forgotten flag. One default password.
๐ด According to Gartner, by 2025, 99% of cloud security failures will be the customer's fault โ primarily due to misconfiguration.
The Shocking Statistics
| Statistic | Value |
|---|---|
| Incidence Rate (tested applications) | 4.5% |
| Mapped CWEs | 20 |
| Mapped CVEs | 208,387 |
| Average Weighted CVSS Score | 8.12 |
| % of cloud breaches caused by misconfiguration | ~65% |
| Average time to detect a misconfigured S3 bucket | ~90 days |
| Average cost of a misconfiguration breach | $4.14 million |
The weighted CVSS score of 8.12 is one of the highest in the entire OWASP Top 10. This means that when misconfigurations are exploited, the impact is devastating.
Why Misconfigurations Are So Dangerous
๐ฅ They're INVISIBLE:
You can't scan your code for them โ they live in configs, flags, and defaults.
๐ฅ They SCALE:
One Terraform module copied across 50 services = 50 vulnerable services.
๐ฅ They're PERSISTENT:
Once deployed, nobody revisits them. They sit there, for years, waiting.
๐ฅ They're EXPLOITED AUTOMATICALLY:
Bots scan the entire internet for misconfigured S3 buckets, exposed APIs,
default credentials, and open dashboards. 24/7. Non-stop.
๐ฅ They DEFEAT ALL OTHER CONTROLS:
You can have perfect code, encrypted databases, and MFA on everything.
One misconfigured firewall rule exposes it all.๐ง What Exactly Is Security Misconfiguration?
Security Misconfiguration occurs when security settings are defined, implemented, or maintained incorrectly at any layer of the application stack โ from the network and cloud infrastructure, through the operating system, web/application server, database, framework, and custom code.
The Official OWASP Definition
"Security misconfiguration is commonly a result of insecure default configurations, incomplete or ad hoc configurations, open cloud storage, misconfigured HTTP headers, and verbose error messages containing sensitive information."
In Plain English
Security Misconfiguration = Any security setting that is:
โข Not set when it should be
โข Set incorrectly
โข Left at an insecure default
โข Forgotten after initial deployment
โข Inconsistent across environments
โข Overly permissiveThe Six Layers Where Misconfigurations Live
Each layer is a potential disaster. And most organizations have misconfigurations across multiple layers simultaneously.
๐ฌ The Anatomy of a Misconfiguration
A misconfiguration doesn't just "happen" โ it follows a predictable lifecycle. Understanding this lifecycle is key to preventing it.
The Lifecycle Breakdown
๐ฆ Stage 1: Installation / Deployment
A new service, framework, or cloud resource is deployed. It comes with manufacturer defaults โ settings chosen for ease of setup, not security.
Example: MongoDB installed with default settings
โ Listens on 0.0.0.0 (all interfaces, including public internet)
โ No authentication required
โ All collections readable/writable to anyoneโ๏ธ Stage 2: Configuration (or Lack Thereof)
Two paths diverge here:
Path A โ The "It Works" Path (Most Common):
Developer: "The app connects to the database. It works. Done!"
โ Default settings remain
โ Debug mode left ON
โ Admin interface accessible publicly
โ Nobody runs a hardening checklistPath B โ The Hardening Path (Rare but Correct):
Developer: "Before I move on, let me run the hardening checklist."
โ Bind to localhost only
โ Enable authentication
โ Disable debug mode
โ Restrict admin interface by IP
โ Enable encryption
โ Review every setting๐ค Stage 3: Automated Discovery
Within hours of being deployed, automated scanners find it.
Shodan.io, Censys, ZoomEye continuously scan every IP on the internet.
They catalog:
- Open ports
- Service versions
- Default pages
- Exposed databases
- Admin panels
- Misconfigured S3 buckets
Attackers subscribe to these services and get real-time alerts
when new vulnerable targets appear.๐ฅ Stage 4: Exploitation
Average time from exposure to exploitation:
- Open database: < 1 hour
- Default credentials: < 24 hours
- Public S3 bucket: < 7 days (depending on content value)
- Exposed admin panel: < 48 hours๐ฐ Stage 5: The Headline
"Company X leaks 100 million customer records due to
misconfigured database"๐ Real-World Breaches Caused by Misconfiguration
Let's look at actual breaches โ not hypothetical scenarios โ that happened because of a single misconfiguration.
๐ Case 1: Capital One (2019) โ 106 Million Records
The Misconfiguration:
- WAF rule allowed Server-Side Request Forgery (SSRF)
- IAM role attached to EC2 had excessive permissions (* on S3)
- No alerting on unusual S3 access patterns
The Damage:
- ๐ฐ $190 million in fines and settlements
- ๐๏ธ Capital One's reputation severely damaged
- ๐ค 106 million customers affected
- โ๏ธ Federal consent order requiring years of oversight
๐ Case 2: Microsoft BlueBleed (2022) โ 2.4 TB Exposed
The Misconfiguration: A single misconfigured Azure Blob Storage endpoint.
Setting: "Allow public access to blob"
Default: โ
Enabled
Result: 2.4 TB of data exposed, including:
- Customer names, emails, phone numbers
- Company names
- Internal communications
- Proof of Concept documents
- Signed Statements of Work (SOWs)
Impact: 65,000+ companies affected across 111 countries๐ Case 3: Facebook (2019) โ 540 Million Records
The Misconfiguration: Third-party developers stored Facebook user data in publicly accessible Amazon S3 buckets.
Two buckets exposed:
1. Cultura Colectiva bucket: 146 GB โ 540 million records
2. "At the Pool" bucket: Fewer records but included passwords
All data was accessible WITHOUT any authentication.
Just a URL and a browser.Why it happened:
- S3 bucket set to "Public Read"
- No bucket policy restrictions
- No monitoring for public buckets
- Facebook had no visibility into third-party data handling
๐ Case 4: MongoDB Ransomware Wave (2017-ongoing)
The Misconfiguration: MongoDB installed with default settings โ no authentication, listening on 0.0.0.0.
Scale:
- 28,000+ MongoDB instances ransomed in the first wave
- Still happens today โ new victims every week
- Attackers automated the entire process
๐ Case 5: Equifax (2017) โ 147 Million Records
While often cited for the Apache Struts vulnerability (CVE-2017-5638), the breach was enabled by multiple misconfigurations:
โ Misconfiguration 1: Expired SSL certificate on monitoring tool
โ Attackers' encrypted exfiltration went UNDETECTED for 76 days
โ Monitoring tool couldn't inspect encrypted traffic without a valid cert
โ Misconfiguration 2: Overly broad network access
โ Once inside, attackers moved laterally to 48 databases
โ Misconfiguration 3: Credentials stored in plaintext files
โ Attackers found database credentials in a plaintext config
โ Misconfiguration 4: No multi-factor authentication on admin accounts
โ Lateral movement was trivial once initial access was gained
โ Misconfiguration 5: Unpatched Apache Struts (ironic โ a config issue)
โ Patch was available 2 months before the breachTotal damage: $1.4+ billion in costs, settlements, and remediation.
The Common Thread
Every single one of these breaches had the same root cause:
๐ด Someone made a configuration decision (or non-decision)
๐ด That decision was NEVER reviewed
๐ด No automated check caught it
๐ด It was exploited within days/weeks
๐ด By the time it was discovered, massive damage was done๐บ๏ธ Technical Deep-Dive: CWE Mapping
A05:2021 maps to 20 CWEs. Here are the most critical:
The Big Three CWEs
๐ธ CWE-732: Incorrect Permission Assignment for Critical Resource
๐ด EXAMPLES:
"text-slate-500 font-normal italic"># Overly permissive file
chmod 777 /etc/shadow "text-slate-500 font-normal italic"># Everyone can read password hashes
"text-slate-500 font-normal italic"># World-writable config
chmod 666 /app/config/database.yml "text-slate-500 font-normal italic"># Anyone can modify database config
"text-slate-500 font-normal italic"># S3 bucket with public write
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject" "text-slate-500 font-normal italic"># Anyone can upload files
"text-slate-500 font-normal italic"># Database with world access
GRANT ALL PRIVILEGES ON *.* TO 'app'@'%' "text-slate-500 font-normal italic"># App user can do EVERYTHING๐ธ CWE-16: Configuration
The umbrella CWE for all configuration-related weaknesses. Includes:
- Debug mode left enabled in production
- Admin interfaces exposed to the internet
- Default passwords unchanged
- Sample applications left installed
- Verbose error messages enabled
- Directory listing enabled
- Unnecessary HTTP methods allowed
- Missing security headers๐ธ CWE-798: Use of Hard-coded Credentials
# ๐ด VULNERABLE
DATABASE_URL = "postgresql://admin:[email protected]/users"
AWS_SECRET_KEY = "AKIAIOSFODNN7EXAMPLE"
JWT_SECRET = "my-secret-key-dont-tell"
# These end up in:
# - Git repositories (searchable forever, even if deleted later)
# - Container images
# - Build logs
# - Backup files
# - Developer laptops๐ The 10 Most Common Misconfigurations
Based on penetration testing data, security research, and breach analysis, here are the top 10 misconfigurations found in production systems:
#1 โ Default Credentials Still Active
Common offenders:
โโโ admin / admin
โโโ root / root
โโโ admin / password
โโโ admin / <blank>
โโโ guest / guest
โโโ Tomcat Manager: tomcat / s3cret
โโโ Jenkins: admin / admin
โโโ MongoDB: no password
โโโ Redis: no password
โโโ Elasticsearch: no auth by default
โโโ Grafana: admin / admin#2 โ Verbose Error Messages
๐ด BAD โ Reveals internal structure:
{
"error": "SQLSTATE[42S02]: Base table or view not found: 1146
Table 'prod_db.users_v2' doesn't exist (SQL: select *
from `users_v2` where `email` = '[email protected]')",
"file": "/var/www/html/app/Http/Controllers/AuthController.php",
"line": 47,
"trace": [
"/var/www/html/vendor/laravel/framework/src/Illuminate/Database/..."
]
}
Attacker now knows:
- Database type (MySQL - SQLSTATE 42S02)
- Database name (prod_db)
- Table name (users_v2) โ implies v1 might also exist
- Framework (Laravel)
- File structure (/var/www/html/app/Http/Controllers/)
- Vulnerability (likely SQL injection point)#3 โ Missing Security Headers
Headers you MUST have โ but probably don't:
โ Strict-Transport-Security (HSTS)
โ Content-Security-Policy (CSP)
โ X-Frame-Options (Clickjacking)
โ X-Content-Type-Options (MIME sniffing)
โ Referrer-Policy (Information leakage)
โ Permissions-Policy (Feature permissions)
โ Cross-Origin-Opener-Policy (COOP)
โ Cross-Origin-Embedder-Policy (COEP)#4 โ Debug Mode in Production
# ๐ด FOUND IN PRODUCTION (true story)
app.run(debug=True, host='0.0.0.0')
# This gives attackers:
# - Interactive Python console on error pages
# - Full stack traces
# - Environment variables
# - LITERAL REMOTE CODE EXECUTION#5 โ Unnecessary Services and Features Enabled
Common unnecessary features left enabled:
โโโ Sample applications (e.g., /manager/html on Tomcat)
โโโ WebDAV on web servers
โโโ TRACE/TRACK HTTP methods
โโโ Directory listing (autoindex on)
โโโ Default error pages exposing server version
โโโ phpMyAdmin on production
โโโ Jenkins script console
โโโ Kubernetes Dashboard with public access
โโโ Elasticsearch with Kibana publicly accessible
โโโ Database admin tools (adminer, phpMyAdmin, pgAdmin)#6 โ Open Cloud Storage Buckets
Classic AWS S3 misconfigurations:
โ Bucket ACL: "Everyone can read"
โ Bucket Policy: Principal "*" with s3:GetObject
โ Object ACL: "Public read" on individual objects
โ Block Public Access: DISABLED at account level
โ CloudFront OAI not used (direct S3 access allowed)
โ Versioning disabled (no recovery from deletion)
โ Logging disabled (no visibility into access)
โ Encryption not enforced#7 โ Outdated Software and Unpatched Systems
Why misconfiguration?
Because "disable auto-updates" or "we'll patch later"
IS a configuration decision.
Examples:
- Apache 2.2 still running in 2024 (EOL since 2017)
- PHP 5.6 in production (EOL since 2019)
- OpenSSL versions with known CVEs
- Operating systems past end-of-life
- Container base images never updated#8 โ Permissive CORS Configuration
// ๐ด DISASTER
app.use(cors({
origin: '*', // ANY origin can call this API
credentials: true, // AND with user cookies/tokens
methods: '*', // Using ANY HTTP method
allowedHeaders: '*' // With ANY headers
}));
// This is equivalent to saying:
// "Anyone on the internet can make authenticated requests to my API"#9 โ Weak or Default Encryption Settings
Common cryptographic misconfigurations:
โ TLS 1.0 / 1.1 still enabled (should be TLS 1.3 only)
โ Weak cipher suites (RC4, DES, 3DES, EXPORT)
โ Self-signed certificates in production
โ Expired certificates
โ Database encryption disabled "for performance"
โ Backups stored unencrypted
โ Default encryption keys ("CHANGE_ME_IN_PRODUCTION")#10 โ Missing or Overly Permissive Rate Limiting
Endpoints that MUST have rate limiting but often don't:
โ /login โ Enables brute force
โ /register โ Enables account spam
โ /forgot-password โ Enables email enumeration
โ /api/* โ Enables scraping and abuse
โ /verify-otp โ Enables OTP brute force
โ /2fa โ Enables 2FA brute force
โ /search โ Enables data scraping
โ /upload โ Enables storage exhaustionโ๏ธ Cloud Misconfigurations: The Modern Frontier
Cloud environments have introduced an entirely new category of misconfigurations. Here are the most dangerous ones:
AWS Misconfiguration Hall of Shame
The Classic S3 Misconfiguration Timeline
Day 1: Developer creates S3 bucket for "temporary file sharing"
Day 1: Sets bucket to "Public Read" because "it's easier"
Day 2: Developer uploads customer export (CSV with 500K records)
Day 2: Forgets to delete it
Day 7: Automated bucket scanner (BucketStream, S3Scanner) finds it
Day 8: Attacker downloads the entire bucket
Day 90: Data appears on dark web
Day 120: Company discovers the breach via security researcher
Day 125: Public disclosure
Day 365: $5M in fines, class action lawsuits beginIAM Policy Nightmares
// ๐ด THE WORST POLICY EVER WRITTEN (and still very common)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
// This grants EVERYTHING to EVERYTHING. Full admin on the account.
// Found in production more often than you'd believe.// ๐ข PROPERLY SCOPED POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-uploads/users/${aws:userid}/*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
},
"Bool": {
"aws:SecureTransport": "true"
},
"NumericLessThan": {
"s3:ContentLength": "5242880"
}
}
}
]
}Security Group Horror Stories
๐ด THE "I'LL FIX IT LATER" RULE:
Type: All Traffic
Protocol: All
Port Range: All
Source: 0.0.0.0/0 (The entire internet)
Translation: "Anyone, anywhere, on any port, with any protocol
can connect to this resource."
Found in:
- Development environments that became production
- "Temporary" debugging rules
- Default VPC configurations
- Copy-pasted Terraform modulesMulti-Cloud Comparison
| Risk | AWS | Azure | GCP |
|---|---|---|---|
| Public Storage | S3 Public Buckets | Public Blob Containers | Public GCS Buckets |
| Over-permissive Identity | IAM : | Owner on Subscription | Project Owner |
| Open Firewall | SG 0.0.0.0/0 | NSG Any-Any | Firewall 0.0.0.0/0 |
| Public Database | RDS Public Access | SQL DB Public Endpoint | Cloud SQL Public IP |
| Credentials in Code | Hardcoded Keys | Hardcoded Connection Strings | Hardcoded Service Account Keys |
| Logging Disabled | CloudTrail Off | Activity Log Off | Audit Log Off |
๐ณ Container & Kubernetes Misconfigurations
Containers and Kubernetes have introduced their own universe of configuration pitfalls.
Docker Misconfigurations
# ๐ด EVERYTHING WRONG WITH THIS DOCKERFILE
FROM ubuntu:latest # โ "latest" tag = unpredictable
RUN apt-get update && apt-get install -y \
curl \
wget \
netcat \
nmap \ # โ Attack tools in prod
&& apt-get clean
ADD . /app # โ ADD vs COPY, no .dockerignore
WORKDIR /app
RUN chmod 777 /app # โ World-writable
ENV DB_PASSWORD=SuperSecret123 # โ Secret in image
EXPOSE 22 # โ SSH in a container
USER root # โ Running as root
CMD ["python", "app.py", "--debug"] # โ Debug mode# ๐ข PROPERLY HARDENED DOCKERFILE
FROM python:3.11-slim-bookworm AS builder # โ
Specific version, slim base
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim-bookworm # โ
Multi-stage build
# โ
Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -u 1001 appuser
# โ
Set secure defaults
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
# โ
Switch to non-root user BEFORE running
USER appuser
# โ
Use PATH to find user-installed packages
ENV PATH=/home/appuser/.local/bin:$PATH \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# โ
Explicit port, not SSH
EXPOSE 8000
# โ
Health check for orchestration
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8000/health || exit 1
# โ
No debug mode, no shell
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]Kubernetes Misconfiguration Patterns
Dangerous Kubernetes Configurations
# ๐ด DISASTER POD SPECIFICATION
apiVersion: v1
kind: Pod
metadata:
name: vulnerable-pod
spec:
hostNetwork: true # โ Pod uses host's network stack
hostPID: true # โ Pod sees all host processes
hostIPC: true # โ Pod shares host IPC
containers:
- name: app
image: myapp:latest # โ "latest" tag
securityContext:
privileged: true # โ Full root on host
runAsUser: 0 # โ Running as root
allowPrivilegeEscalation: true
capabilities:
add: ["ALL"] # โ All Linux capabilities
volumeMounts:
- name: host-root
mountPath: /host # โ Mounting host filesystem
env:
- name: DB_PASSWORD
value: "SuperSecret" # โ Secret in plaintext
volumes:
- name: host-root
hostPath:
path: / # โ Entire host filesystem# ๐ข PROPERLY HARDENED POD SPECIFICATION
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
automountServiceAccountToken: false # โ
Don't auto-mount SA token
securityContext:
runAsNonRoot: true # โ
Enforce non-root
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault # โ
Default seccomp profile
containers:
- name: app
image: myregistry.io/myapp:1.2.3@sha256:abc123... # โ
Pinned image
imagePullPolicy: Always # โ
Always pull
securityContext:
allowPrivilegeEscalation: false # โ
No privilege escalation
readOnlyRootFilesystem: true # โ
Read-only root FS
runAsNonRoot: true
capabilities:
drop: ["ALL"] # โ
Drop all capabilities
resources: # โ
Resource limits set
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef: # โ
Secret from K8s Secrets
name: db-credentials
key: password
livenessProbe: # โ
Health checks
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {} # โ
Writable tmp only
- name: cache
emptyDir: {}๐ก๏ธ Security Headers: The Easiest Wins You're Missing
Security headers are free, easy, and effective. Yet, the vast majority of websites are missing most of them.
The Essential Security Headers
"text-slate-500 font-normal italic"># ๐ข THE COMPLETE SET
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
Cache-Control: no-store, max-age=0
Clear-Site-Data: "cache", "cookies", "storage"What Each Header Actually Does
Nginx Configuration Example
"text-slate-500 font-normal italic"># ๐ข COMPREHENSIVE SECURITY HEADERS IN NGINX
server {
listen 443 ssl http2;
server_name example.com;
"text-slate-500 font-normal italic"># โโโ TLS Configuration โโโ
ssl_protocols TLSv1.3; "text-slate-500 font-normal italic"># TLS 1.3 ONLY
ssl_prefer_server_ciphers off;
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
"text-slate-500 font-normal italic"># OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
"text-slate-500 font-normal italic"># โโโ Security Headers โโโ
"text-slate-500 font-normal italic"># Force HTTPS for 2 years, include subdomains, allow preloading
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
"text-slate-500 font-normal italic"># Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;
"text-slate-500 font-normal italic"># Prevent clickjacking
add_header X-Frame-Options "DENY" always;
"text-slate-500 font-normal italic"># Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
"text-slate-500 font-normal italic"># Restrict browser features
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
"text-slate-500 font-normal italic"># Content Security Policy (strict)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" always;
"text-slate-500 font-normal italic"># Cross-origin isolation
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
"text-slate-500 font-normal italic"># โโโ Hide Server Information โโโ
server_tokens off; "text-slate-500 font-normal italic"># Don't reveal nginx version
more_clear_headers 'Server'; # Remove Server header entirely
more_clear_headers 'X-Powered-By';
"text-slate-500 font-normal italic"># โโโ Disable Dangerous HTTP Methods โโโ
if ($request_method !~ ^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)$) {
return 405;
}
"text-slate-500 font-normal italic"># โโโ Rate Limiting โโโ
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req zone=general burst=20 nodelay;
"text-slate-500 font-normal italic"># โโโ Hide Specific Paths โโโ
location ~ /\. {
deny all; "text-slate-500 font-normal italic"># Block hidden files (.git, .env, etc.)
}
location ~ \.(log|sql|conf|bak)$ {
deny all; "text-slate-500 font-normal italic"># Block config/log files
}
}๐๏ธ Secure Configuration Patterns
Pattern 1: Immutable Infrastructure
Rule: Configuration changes = new deployment. Never modify running systems.
Pattern 2: Configuration as Code
Every piece of configuration should be:
- โ Stored in version control (Git)
- โ Reviewed via pull requests
- โ Validated by automated checks
- โ Scanned for security issues
- โ Deployed through CI/CD pipelines
Pattern 3: Environment Parity
๐ด BAD โ Different configs per environment:
โโโ dev/ โ Debug ON, auth disabled, open CORS
โโโ staging/ โ Different again
โโโ prod/ โ "Hopefully" secure (nobody really knows)
๐ข GOOD โ Same structure, different values:
โโโ config.yaml (base config)
โโโ config.dev.yaml (dev overrides only)
โโโ config.staging.yaml (staging overrides only)
โโโ config.prod.yaml (prod overrides only)
Benefit: Testing in dev/staging catches configuration bugs
before they hit production.Pattern 4: Secrets Separation
๐ด WRONG:
secrets stored in:
- Code files
- Config files
- Environment variables (in Dockerfiles)
- Git repositories
- Slack messages
- Documentation
๐ข RIGHT:
secrets stored in:
- HashiCorp Vault
- AWS Secrets Manager / Parameter Store
- Azure Key Vault
- GCP Secret Manager
- Kubernetes Secrets (encrypted at rest)
Accessed via:
- IAM roles (not credentials)
- Service accounts
- Short-lived tokens
- Just-in-time accessPattern 5: Least Functionality
QUESTION TO ASK FOR EVERY FEATURE/SERVICE/PORT:
"Do we actually need this?"
If the answer is "I'm not sure" or "maybe someday" โ DISABLE IT
Examples:
โ Default sample applications โ Remove
โ Admin interfaces exposed publicly โ Internal network only
โ Unused HTTP methods (PUT, DELETE on static endpoints) โ Disable
โ Legacy TLS versions โ Disable
โ Debug endpoints in production โ Remove
โ Verbose logging in production โ Set to WARNING or ERROR
โ Default accounts โ Delete๐ Hardening Guides by Technology
Rather than reinventing the wheel, use established hardening guides for every technology in your stack.
CIS Benchmarks
The Center for Internet Security (CIS) Benchmarks are the gold standard for configuration hardening. They exist for:
Operating Systems:
โโโ CIS Red Hat Enterprise Linux Benchmark
โโโ CIS Ubuntu Linux Benchmark
โโโ CIS Microsoft Windows Server Benchmark
โโโ CIS macOS Benchmark
Web Servers:
โโโ CIS Apache HTTP Server Benchmark
โโโ CIS NGINX Benchmark
โโโ CIS Microsoft IIS Benchmark
Databases:
โโโ CIS MySQL Benchmark
โโโ CIS PostgreSQL Benchmark
โโโ CIS MongoDB Benchmark
โโโ CIS Oracle Database Benchmark
โโโ CIS Microsoft SQL Server Benchmark
Cloud:
โโโ CIS Amazon Web Services Foundations Benchmark
โโโ CIS Microsoft Azure Foundations Benchmark
โโโ CIS Google Cloud Platform Foundation Benchmark
Containers:
โโโ CIS Docker Benchmark
โโโ CIS Kubernetes Benchmark
Office Suites:
โโโ CIS Microsoft 365 Benchmark
โโโ CIS Google Workspace BenchmarkQuick Hardening Reference
| Technology | Essential Hardening Steps |
|---|---|
| Linux Server | Disable root SSH, use key-based auth, enable firewall, remove unused packages, apply CIS benchmark |
| Nginx/Apache | Disable server tokens, enable all security headers, disable directory listing, remove default pages |
| MySQL/PostgreSQL | Change default passwords, disable remote root, enable SSL/TLS, enable audit logging, minimal privileges per user |
| MongoDB | Enable authentication, bind to localhost, enable TLS, enable audit log |
| Redis | Set password, bind to localhost, disable dangerous commands (FLUSHALL, CONFIG, EVAL) |
| Elasticsearch | Enable X-Pack security, enable TLS, don't expose to internet, use IP filtering |
| Docker | Use non-root user, read-only filesystem, drop capabilities, use trusted base images |
| Kubernetes | Enable RBAC, apply Pod Security Standards, use Network Policies, scan images |
| AWS | Enable CloudTrail, enable GuardDuty, enable Security Hub, use IAM roles not keys, enable MFA |
๐ค Automation: Infrastructure as Code Security
In modern environments, configuration = code. Which means we can test configuration like code.
The IaC Security Pipeline
Terraform Security Example
"text-slate-500 font-normal italic"># ๐ด INSECURE TERRAFORM
resource "aws_s3_bucket" "data" {
bucket = "my-company-data"
acl = "public-read" "text-slate-500 font-normal italic"># โ PUBLIC
}
resource "aws_security_group" "web" {
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] "text-slate-500 font-normal italic"># โ ALL PORTS, OPEN TO WORLD
}
}
resource "aws_db_instance" "default" {
engine = "mysql"
publicly_accessible = true "text-slate-500 font-normal italic"># โ PUBLIC DATABASE
storage_encrypted = false "text-slate-500 font-normal italic"># โ UNENCRYPTED
backup_retention_period = 0 "text-slate-500 font-normal italic"># โ NO BACKUPS
deletion_protection = false "text-slate-500 font-normal italic"># โ CAN BE ACCIDENTALLY DELETED
username = "admin" "text-slate-500 font-normal italic"># โ DEFAULT USERNAME
password = "ChangeMe123!" "text-slate-500 font-normal italic"># โ WEAK HARDCODED PASSWORD
}"text-slate-500 font-normal italic"># ๐ข SECURE TERRAFORM
resource "aws_s3_bucket" "data" {
bucket = "my-company-data-${random_id.suffix.hex}"
}
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = true "text-slate-500 font-normal italic"># โ
Block all public access
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.data.arn "text-slate-500 font-normal italic"># โ
KMS encryption
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled" "text-slate-500 font-normal italic"># โ
Versioning for recovery
}
}
resource "aws_s3_bucket_logging" "data" {
bucket = aws_s3_bucket.data.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "s3-access-logs/" "text-slate-500 font-normal italic"># โ
Access logging
}
resource "aws_security_group" "web" {
name_prefix = "web-sg-"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from ALB only"
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.alb.id] "text-slate-500 font-normal italic"># โ
Specific source
}
egress {
description = "HTTPS to internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg"
}
}
resource "aws_db_instance" "default" {
identifier = "production-db"
engine = "mysql"
engine_version = "8.0.35"
instance_class = "db.t3.medium"
"text-slate-500 font-normal italic"># Storage
allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true "text-slate-500 font-normal italic"># โ
Encrypted at rest
kms_key_id = aws_kms_key.rds.arn
"text-slate-500 font-normal italic"># Credentials from Secrets Manager
username = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["username"]
password = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)["password"]
"text-slate-500 font-normal italic"># Network
publicly_accessible = false "text-slate-500 font-normal italic"># โ
PRIVATE
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
"text-slate-500 font-normal italic"># Backup
backup_retention_period = 30 "text-slate-500 font-normal italic"># โ
30 days of backups
backup_window = "03:00-04:00"
copy_tags_to_snapshot = true
"text-slate-500 font-normal italic"># Maintenance
auto_minor_version_upgrade = true "text-slate-500 font-normal italic"># โ
Auto security patches
maintenance_window = "mon:04:00-mon:05:00"
"text-slate-500 font-normal italic"># Protection
deletion_protection = true "text-slate-500 font-normal italic"># โ
Can't be deleted
skip_final_snapshot = false
final_snapshot_identifier = "production-db-final"
"text-slate-500 font-normal italic"># Monitoring
monitoring_interval = 60 "text-slate-500 font-normal italic"># โ
Enhanced monitoring
monitoring_role_arn = aws_iam_role.rds_monitoring.arn
"text-slate-500 font-normal italic"># Logging
enabled_cloudwatch_logs_exports = ["audit", "error", "general", "slowquery"]
"text-slate-500 font-normal italic"># Performance insights
performance_insights_enabled = true
performance_insights_kms_key_id = aws_kms_key.rds.arn
}๐ป Code & Config Examples: Vulnerable vs Secure
Example 1: Express.js Application
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ด INSECURE EXPRESS APPLICATION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const express = require('express');
const app = express();
// โ No security middleware
// โ Error details leaked
// โ Default error handler
app.use(express.json({ limit: '100mb' })); // โ Huge payload allowed
// โ Fully permissive CORS
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', '*');
res.header('Access-Control-Allow-Headers', '*');
next();
});
app.get('/api/user/:id', (req, res) => {
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`,
(err, result) => {
if (err) {
res.status(500).json({
error: err.message, // โ Leaks DB errors
stack: err.stack // โ Leaks stack trace
});
return;
}
res.json(result);
}
);
});
// โ Debug info endpoint accessible
app.get('/debug', (req, res) => {
res.json({
env: process.env, // โ Leaks ALL env vars
version: process.version,
platform: process.platform
});
});
app.listen(3000, '0.0.0.0'); // โ Binds to all interfaces
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ๐ข SECURE EXPRESS APPLICATION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const compression = require('compression');
const { v4: uuidv4 } = require('uuid');
const app = express();
// โ
Trust proxy for correct IP detection (behind load balancer)
app.set('trust proxy', 1);
// โ
Disable X-Powered-By header
app.disable('x-powered-by');
// โ
Security headers via Helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
hsts: {
maxAge: 63072000,
includeSubDomains: true,
preload: true
},
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: "same-origin" },
crossOriginResourcePolicy: { policy: "same-origin" }
}));
// โ
Strict CORS โ whitelist only
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));
// โ
Body parser with REASONABLE limits
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: true, limit: '100kb' }));
// โ
Rate limiting
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests, please try again later.'
});
const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Stricter for sensitive endpoints
skipSuccessfulRequests: true
});
app.use('/api/', generalLimiter);
app.use('/api/auth/', strictLimiter);
// โ
Compression (gzip)
app.use(compression());
// โ
Request ID for tracing
app.use((req, res, next) => {
req.id = uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});
// โ
Structured logging
app.use((req, res, next) => {
logger.info({
request_id: req.id,
method: req.method,
url: req.url,
ip: req.ip,
user_agent: req.get('user-agent')
});
next();
});
// โ
Routes with parameterized queries
app.get('/api/user/:id', async (req, res) => {
try {
// โ
Input validation
const id = parseInt(req.params.id, 10);
if (isNaN(id) || id < 1) {
return res.status(400).json({ error: 'Invalid user ID' });
}
// โ
Parameterized query
const result = await db.query(
'SELECT id, email, name FROM users WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(result.rows[0]);
} catch (err) {
// โ
Log internally, generic message externally
logger.error({
request_id: req.id,
error: err.message,
stack: err.stack
});
res.status(500).json({
error: 'Internal server error',
request_id: req.id
});
}
});
// โ
Health check (no sensitive info)
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
// โ
404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// โ
Global error handler (NO stack traces to users)
app.use((err, req, res, next) => {
logger.error({
request_id: req.id,
error: err.message,
stack: err.stack
});
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'An error occurred'
: err.message,
request_id: req.id
});
});
// โ
Bind to localhost in dev, specific interface in prod
const HOST = process.env.HOST || '127.0.0.1';
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, HOST, () => {
logger.info(`Server listening on ${HOST}:${PORT}`);
});
// โ
Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});Example 2: Python Flask Application
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# ๐ด INSECURE FLASK APPLICATION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-key-123' # โ Hardcoded weak key
@app.route('/api/exec')
def execute():
cmd = request.args.get('cmd')
result = os.popen(cmd).read() # โ COMMAND INJECTION
return result
@app.errorhandler(500)
def error(e):
return str(e), 500 # โ Leaks internal errors
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) # โ Debug mode in prod!
# Debug mode exposes an interactive Python console on error pages
# = REMOTE CODE EXECUTION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# ๐ข SECURE FLASK APPLICATION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import os
import secrets
import logging
from flask import Flask, request, jsonify, abort
from flask_talisman import Talisman
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.middleware.proxy_fix import ProxyFix
# โ
Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# โ
Load secrets from environment / secrets manager
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
if not app.config['SECRET_KEY']:
raise RuntimeError("SECRET_KEY environment variable not set")
# โ
Secure session configuration
app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
SESSION_COOKIE_SAMESITE='Strict', # CSRF protection
PERMANENT_SESSION_LIFETIME=1800, # 30 minutes
SESSION_COOKIE_NAME='__Secure-session',
MAX_CONTENT_LENGTH=1 * 1024 * 1024 # 1MB request limit
)
# โ
Proxy fix for correct IP behind load balancer
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
# โ
Security headers via Talisman
csp = {
'default-src': "'self'",
'script-src': "'self'",
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'https:'],
'object-src': "'none'",
'frame-ancestors': "'none'",
'base-uri': "'self'",
'form-action': "'self'"
}
Talisman(
app,
content_security_policy=csp,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=63072000,
strict_transport_security_include_subdomains=True,
strict_transport_security_preload=True,
session_cookie_secure=True,
session_cookie_http_only=True,
frame_options='DENY',
referrer_policy='strict-origin-when-cross-origin'
)
# โ
Rate limiting
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per hour", "50 per minute"],
storage_uri="redis://localhost:6379"
)
# โ
Remove server identification
@app.after_request
def remove_server_header(response):
response.headers['Server'] = ''
return response
# โ
Secure endpoint example
@app.route('/api/data/<int:data_id>')
@limiter.limit("30 per minute")
def get_data(data_id):
# โ
Input validation (int type via route converter)
if data_id < 1 or data_id > 1000000:
abort(400, description="Invalid ID")
try:
# โ
Parameterized query (via ORM or prepared statement)
data = Data.query.filter_by(id=data_id).first()
if not data:
abort(404)
# โ
Authorization check
if not current_user.can_access(data):
abort(403)
return jsonify(data.to_safe_dict())
except Exception as e:
# โ
Log internally
request_id = secrets.token_hex(8)
logger.error(f"[{request_id}] Error in get_data: {e}", exc_info=True)
# โ
Return generic error with reference
return jsonify({
"error": "An unexpected error occurred",
"request_id": request_id
}), 500
# โ
Custom error handlers (no information leakage)
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "Bad request"}), 400
@app.errorhandler(401)
def unauthorized(e):
return jsonify({"error": "Unauthorized"}), 401
@app.errorhandler(403)
def forbidden(e):
return jsonify({"error": "Forbidden"}), 403
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"error": "Rate limit exceeded",
"retry_after": e.retry_after
}), 429
@app.errorhandler(500)
def internal_error(e):
request_id = secrets.token_hex(8)
logger.error(f"[{request_id}] Internal error", exc_info=True)
return jsonify({
"error": "Internal server error",
"request_id": request_id
}), 500
# โ
Health check endpoint (minimal info)
@app.route('/health')
def health():
return jsonify({"status": "ok"}), 200
# โ
NEVER run with Flask's dev server in production
# Use gunicorn/uwsgi instead
if __name__ == '__main__':
# Development only
if os.environ.get('FLASK_ENV') != 'development':
raise RuntimeError(
"Do NOT use Flask's dev server in production. "
"Use gunicorn: 'gunicorn -w 4 -b 0.0.0.0:8000 app:app'"
)
app.run(debug=False, host='127.0.0.1', port=5000)Example 3: Nginx Configuration
"text-slate-500 font-normal italic"># ๐ข PRODUCTION-READY SECURE NGINX CONFIG
"text-slate-500 font-normal italic"># โโโ Global Settings โโโ
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
events {
multi_accept on;
worker_connections 65535;
}
http {
"text-slate-500 font-normal italic"># โโโ Hide Nginx version โโโ
server_tokens off;
more_clear_headers 'Server';
"text-slate-500 font-normal italic"># โโโ MIME types โโโ
include /etc/nginx/mime.types;
default_type application/octet-stream;
"text-slate-500 font-normal italic"># โโโ Logging โโโ
log_format main_ext '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_id';
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
"text-slate-500 font-normal italic"># โโโ Performance โโโ
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
reset_timedout_connection on;
"text-slate-500 font-normal italic"># โโโ Buffer sizes (prevent buffer overflow attacks) โโโ
client_body_buffer_size 1k;
client_header_buffer_size 1k;
client_max_body_size 1m;
large_client_header_buffers 2 1k;
"text-slate-500 font-normal italic"># โโโ Timeouts (prevent slowloris attacks) โโโ
client_body_timeout 10;
client_header_timeout 10;
send_timeout 10;
"text-slate-500 font-normal italic"># โโโ Gzip (secure config) โโโ
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript;
"text-slate-500 font-normal italic"># โ NEVER enable gzip for HTML with sensitive data (BREACH attack)
"text-slate-500 font-normal italic"># โโโ Rate Limiting Zones โโโ
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
"text-slate-500 font-normal italic"># โโโ SSL/TLS Global Settings โโโ
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 valid=300s;
resolver_timeout 5s;
"text-slate-500 font-normal italic"># โโโ HTTP โ HTTPS Redirect โโโ
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https:"text-slate-500 font-normal italic">//$host$request_uri;
}
"text-slate-500 font-normal italic"># โโโ Main HTTPS Server โโโ
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
"text-slate-500 font-normal italic"># โโโ Certificate โโโ
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
"text-slate-500 font-normal italic"># โโโ Security Headers โโโ
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
"text-slate-500 font-normal italic"># โโโ Request Limits โโโ
limit_req zone=general burst=20 nodelay;
limit_conn addr 10;
"text-slate-500 font-normal italic"># โโโ Allowed HTTP Methods โโโ
if ($request_method !~ ^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)$) {
return 405;
}
"text-slate-500 font-normal italic"># โโโ Block Hidden Files โโโ
location ~ /\.(?!well-known) {
deny all;
access_log off;
log_not_found off;
}
"text-slate-500 font-normal italic"># โโโ Block Sensitive File Extensions โโโ
location ~ \.(log|sql|conf|bak|old|orig|save|swo|swp|~|yml|yaml)$ {
deny all;
access_log off;
}
"text-slate-500 font-normal italic"># โโโ Block User-Agents โโโ
if ($http_user_agent ~* (nmap|nikto|wikto|sf|sqlmap|bsqlbf|w3af|acunetix|havij|appscan)) {
return 403;
}
"text-slate-500 font-normal italic"># โโโ Login Endpoint โ Stricter Rate Limiting โโโ
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http:"text-slate-500 font-normal italic">//app_backend;
}
"text-slate-500 font-normal italic"># โโโ API Endpoints โโโ
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http:"text-slate-500 font-normal italic">//app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
}
"text-slate-500 font-normal italic"># โโโ Static Files โโโ
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
"text-slate-500 font-normal italic"># โโโ Default โ Reverse Proxy โโโ
location / {
proxy_pass http:"text-slate-500 font-normal italic">//app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
"text-slate-500 font-normal italic"># โโโ Custom Error Pages (no leaks) โโโ
error_page 400 401 402 403 404 /error.html;
error_page 500 501 502 503 504 /error.html;
location = /error.html {
internal;
root /var/www/errors;
}
}
"text-slate-500 font-normal italic"># โโโ Catch-all for other hosts โโโ
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name _;
ssl_reject_handshake on;
return 444; "text-slate-500 font-normal italic"># Close connection without response
}
}๐งฐ Tools for Detecting Misconfigurations
Cloud Security Posture Management (CSPM)
Security Header Scanners
Online scanners (just paste your URL):
โโโ https:"text-slate-500 font-normal italic">//securityheaders.com โ Mozilla / Scott Helme
โโโ https:"text-slate-500 font-normal italic">//observatory.mozilla.org โ Mozilla Observatory
โโโ https:"text-slate-500 font-normal italic">//www.ssllabs.com/ssltest โ SSL/TLS testing
โโโ https:"text-slate-500 font-normal italic">//hstspreload.org โ HSTS preload status
โโโ https:"text-slate-500 font-normal italic">//csp-evaluator.withgoogle.com โ CSP evaluatorNmap for Port and Service Discovery
"text-slate-500 font-normal italic"># Discover open ports and services
nmap -sV -sC -O target.example.com
"text-slate-500 font-normal italic"># Detect default credentials
nmap --script http-default-accounts target.example.com
"text-slate-500 font-normal italic"># Scan for common misconfigurations
nmap --script "default,safe,vuln" target.example.com
"text-slate-500 font-normal italic"># Check for exposed admin interfaces
nmap --script http-enum target.example.comSpecialized Tools
| Tool | Purpose |
|---|---|
| Shodan | Internet-wide scan database โ find YOUR exposed services |
| Censys | Alternative to Shodan with better filtering |
| GitLeaks | Scan git repos for hardcoded secrets |
| TruffleHog | Deep secret scanning in git history |
| detect-secrets | Pre-commit secret detection |
| Pacu | AWS exploitation framework (for testing) |
| CloudMapper | Visualize AWS infrastructure for security review |
โ The Ultimate Checklist
๐ Security Misconfiguration Checklist
Use this checklist during every deployment, environment setup, and quarterly audit.
๐๏ธ Infrastructure & Cloud
- [ ] No cloud storage buckets are publicly accessible (unless intentional)
- [ ] "Block Public Access" enabled at account level (AWS)
- [ ] All IAM policies follow least privilege (no : policies)
- [ ] MFA enabled on root/admin accounts
- [ ] Root account not used for daily operations
- [ ] Security groups don't allow 0.0.0.0/0 on sensitive ports
- [ ] Databases are not publicly accessible
- [ ] All data encrypted at rest (EBS, RDS, S3)
- [ ] All data encrypted in transit (TLS 1.3)
- [ ] CloudTrail / Activity Log enabled in all regions
- [ ] GuardDuty / Security Center enabled
- [ ] Regular access reviews conducted
- [ ] Unused resources deleted (reduces attack surface)
๐ฅ๏ธ Operating System
- [ ] CIS benchmark applied (or equivalent hardening guide)
- [ ] All default accounts disabled or passwords changed
- [ ] Unnecessary services and packages removed
- [ ] SSH: root login disabled, key-based auth only
- [ ] Firewall enabled with default-deny policy
- [ ] Automatic security updates enabled
- [ ] File permissions reviewed (no world-writable sensitive files)
- [ ] SELinux / AppArmor enabled and enforcing
- [ ] Audit logging configured (auditd / Windows Event Log)
- [ ] Time synchronization configured (NTP)
๐ Web Server (Nginx/Apache/IIS)
- [ ] Server version/tokens hidden
- [ ] Directory listing disabled
- [ ] Default pages and sample apps removed
- [ ] All required security headers configured
- [ ] TLS 1.3 only, strong cipher suites
- [ ] HTTP automatically redirects to HTTPS
- [ ] HSTS enabled with preload
- [ ] Dangerous HTTP methods disabled (TRACE, TRACK)
- [ ] Rate limiting configured
- [ ] Error pages don't leak information
- [ ] Hidden files (.git, .env, .htaccess) blocked
โ๏ธ Application
- [ ] Debug mode DISABLED in production
- [ ] Verbose error messages disabled
- [ ] Stack traces not shown to users
- [ ] Admin interfaces not publicly accessible
- [ ] Default accounts/passwords changed
- [ ] All frameworks and libraries up to date
- [ ] Security headers set programmatically (Helmet, Talisman)
- [ ] CORS configured with whitelist (not *)
- [ ] Request size limits enforced
- [ ] Rate limiting on all endpoints
- [ ] Logging configured (structured, no sensitive data)
- [ ] Health check endpoints don't leak information
๐พ Database
- [ ] Default credentials changed
- [ ] Database not publicly accessible
- [ ] Encryption at rest enabled
- [ ] TLS enabled for connections
- [ ] Audit logging enabled
- [ ] Backup encryption enabled
- [ ] Backup access controls configured
- [ ] Database user permissions follow least privilege
- [ ] No application uses root/admin database account
- [ ] Query performance insights don't log sensitive data
๐ณ Containers
- [ ] Base images from trusted sources
- [ ] Specific image tags (not latest)
- [ ] Images scanned for vulnerabilities
- [ ] Containers run as non-root user
- [ ] Read-only root filesystem where possible
- [ ] All capabilities dropped, only needed ones added
- [ ] No privileged: true containers
- [ ] Resource limits set (CPU, memory)
- [ ] Secrets injected via secret management (not env vars)
- [ ] Health checks configured
โธ๏ธ Kubernetes
- [ ] RBAC enabled and properly configured
- [ ] Pod Security Standards applied (restricted profile)
- [ ] Network Policies defined (default deny)
- [ ] Service accounts: automountServiceAccountToken: false
- [ ] Dashboard not publicly exposed
- [ ] etcd encrypted at rest
- [ ] Audit logging enabled
- [ ] Admission controllers configured (OPA Gatekeeper, Kyverno)
- [ ] CIS Kubernetes benchmark applied
- [ ] Image pull secrets properly managed
๐ CI/CD
- [ ] Secrets not stored in code or build logs
- [ ] IaC security scanning in pipeline (Checkov, tfsec)
- [ ] Container image scanning in pipeline (Trivy, Snyk)
- [ ] Dependency scanning in pipeline (Snyk, Dependabot)
- [ ] Signed commits and artifact signing
- [ ] Deployment credentials rotated regularly
- [ ] Build environments isolated from production
- [ ] Pipeline configurations reviewed via PRs
๐ Conclusion
The Hard Truth
Key Takeaways
๐ด Security Misconfiguration is the #1 cause of cloud breaches โ not because it's complicated, but because it's INVISIBLE.
๐ก Defaults are optimized for ease of setup, NOT security. Changing them is YOUR responsibility.
๐ข Automation is the only scalable defense. Manual audits miss things. Automated scanners don't.
๐ต Configuration drift is inevitable. Continuous monitoring is non-negotiable.
๐ฃ "We'll harden it later" is how breaches happen. Harden on day one.
The Three Rules of Secure Configuration
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Rule 1: ๐ง ASSUME DEFAULTS ARE INSECURE โ
โ โ Every new service, every new resource, โ
โ every new feature starts in an insecure state. โ
โ โ
โ Rule 2: ๐ค AUTOMATE EVERYTHING โ
โ โ Config as code. IaC scanning. Policy as code. โ
โ If a human has to remember it, it will be โ
โ forgotten. โ
โ โ
โ Rule 3: ๐ CONTINUOUSLY MONITOR โ
โ โ Configuration drift is inevitable. โ
โ Detect it in real-time. Alert on changes. โ
โ Auto-remediate where possible. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโThe Cost of Inaction
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MISCONFIGURATION BREACH COSTS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Average breach cost (misconfiguration): $4.14 MILLION โ
โ Average S3 bucket exposure duration: 90 DAYS โ
โ % of breaches detected by third parties: 65% โ
โ % of breaches detected within 24 hours: < 10% โ
โ โ
โ Cost to implement CSPM tool: $20K - $200K/yr โ
โ ROI of catching ONE prevented breach: 20x - 200x โ
โ โ
โ Sources: IBM, Gartner, Verizon DBIR 2024 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโThe Mindset Shift
Stop thinking of configuration as "setup work" that you do once and forget. Start thinking of it as:
Configuration = Code
Code = Reviewable
Reviewable = Testable
Testable = Automatable
Automatable = Continuously Verifiable
Continuously Verifiable = SECUREFinal Words
Security Misconfiguration is deceptively simple. There's no genius exploit involved. There's no sophisticated attacker. There's just an attacker, a scanner, and a defender who forgot to turn a switch.
Don't be that defender.
Build configuration into your DNA. Automate it. Scan it. Monitor it. Review it. Make it impossible to deploy an insecure configuration.
Because somewhere, right now, a bot is scanning the entire internet looking for one open port, one default password, one public bucket, one debug endpoint. If you have one โ and statistically, you probably do โ they will find it.
The question is: will you find it first?
<div align="center">
๐ Further Reading
| Resource | URL |
|---|---|
| OWASP Top 10 โ 2021 | https://owasp.org/Top10/ |
| OWASP Secure Headers Project | https://owasp.org/www-project-secure-headers/ |
| CIS Benchmarks | https://www.cisecurity.org/cis-benchmarks |
| NIST National Checklist Program | https://ncp.nist.gov/ |
| AWS Well-Architected Security Pillar | https://aws.amazon.com/architecture/well-architected/ |
| Azure Security Benchmark | https://docs.microsoft.com/security/benchmark/azure/ |
| CIS Kubernetes Benchmark | https://www.cisecurity.org/benchmark/kubernetes |
| Mozilla Observatory | https://observatory.mozilla.org/ |
| Shodan โ The Search Engine for IoT | https://www.shodan.io/ |
Category: Application Security ยท Cloud Security ยท DevSecOps ยท OWASP Tags: #OWASP #SecurityMisconfiguration #CloudSecurity #CSPM #DevSecOps #A05 #Hardening
"The security of a system is only as strong as its weakest configuration."
"In cloud security, the shared responsibility model is clear: the cloud provider secures the cloud, YOU secure what's IN the cloud. Misconfigurations are always your fault." โ Common Industry Wisdom
"The best time to harden your system was before you deployed it. The second-best time is right now."
</div>