Defense in Depth and Secure Defaults
Layer defenses and secure configurations from the start
TL;DR
Defense in Depth: Don't rely on a single security mechanism. Layer multiple defenses so breaching one doesn't compromise the system. Network firewall, then application auth, then database encryption, then audit logging. If firewall fails, auth still protects. If attacker steals credentials, encryption protects data. Audit logs reveal the breach. Secure Defaults: Systems should be secure out of the box without configuration. Passwords required (not optional), encryption enabled (not opted-in), debugging disabled (not exposed), least privilege applied (not full permissions). Users configuring "more secure" than default indicates poor defaults.
Learning Objectives
- Understand layered security and defense in depth
- Identify security layers in systems
- Design secure defaults for products and infrastructure
- Reduce configuration burden by securing by default
- Recognize single points of failure in security
Motivating Scenario
A service is only protected by API authentication. Attacker compromises credentials via phishing. All data exposed. The service had no other defense.
Compare: Attacker compromises credentials. API auth fails—logging records unusual access pattern. Attacker continues, trying SQL injection. Database rejects malformed queries. Attacker tries to read backups. Encryption prevents plaintext reading. Attacker eventually gives up. Audit logs reveal entire attack.
Layers: API auth, input validation, database protection, encryption, audit logs, monitoring.
Core Concepts
Layers of Defense
1. Perimeter: Firewall, network segmentation, WAF (Web Application Firewall) block obvious attacks.
2. Application: API authentication, input validation, authorization checks prevent logical attacks.
3. Data: Encryption at rest and in transit prevents data exfiltration even if compromised.
4. Monitoring & Response: Audit logs detect breaches; automated responses contain damage.
Each layer independent. Failure in one doesn't cascade.
Secure Defaults in Practice
Bad Default: require_mfa = false (optional MFA, easily disabled)
Secure Default: require_mfa = true (always on, opt-out requires admin override)
Bad Default: Passwords stored plaintext Secure Default: Passwords hashed with bcrypt, cost=12
Bad Default: Debug endpoints exposed in production Secure Default: Debug disabled; enabled only in development with explicit flag
Bad Default: All data world-readable Secure Default: Private by default; public only with explicit permission
Practical Defense Layers
User Request
↓
[Layer 1] Network Firewall
- Only allow known ports (80, 443)
- Block malformed packets
→ If breached: proceed to Layer 2
↓
[Layer 2] API Authentication
- Verify user identity (OAuth, mTLS)
- MFA required
→ If breached: proceed to Layer 3
↓
[Layer 3] Authorization & Input Validation
- User can only access own data
- SQL injection attempts rejected
→ If breached: proceed to Layer 4
↓
[Layer 4] Database Protection
- Sensitive data encrypted
- Read-only replicas for non-critical queries
→ If breached: proceed to Layer 5
↓
[Layer 5] Audit Logging & Monitoring
- All access logged
- Anomalies detected (unusual query, bulk export)
→ Automated response: revoke tokens, isolate resources
Attacker must breach multiple layers. Detection occurs at each layer.
Secure Configuration Examples
# ❌ Insecure Defaults
database:
require_password: false # Optional password
require_tls: false # Unencrypted connections allowed
enable_all_permissions: true # Users all powerful by default
api:
require_authentication: false # Optional auth
debug_endpoints_enabled: true # Debug endpoints exposed
cors_allowed: "*" # Cross-origin requests from anywhere
rate_limit: null # No rate limiting
# ✅ Secure Defaults
database:
require_password: true # Password mandatory
password_min_entropy: 128 # Strong password required
require_tls: true # All connections encrypted
enable_all_permissions: false # Deny by default
audit_logging: true # All operations logged
api:
require_authentication: true # Auth mandatory
require_mfa: true # Two-factor required
debug_endpoints_enabled: false # Debug disabled
debug_enabled_only_with: "DEBUG_TOKEN env var" # Opt-in for dev only
cors_allowed: null # No CORS by default
cors_allowed_origins: [] # Explicit whitelist only
rate_limit: 1000_per_hour # Reasonable default
rate_limit_bypass: requires_admin_token # Hard to bypass
Secure defaults reduce misconfiguration. Users don't accidentally expose data.
Design Review Checklist
- No single point of failure in security
- Multiple layers defend against different attack vectors
- Failure in one layer doesn't compromise others
- Secure defaults prevent misconfigurations
- Optional security features have explicit opt-in (not opt-out)
- Encryption enabled by default
- Debug/dev features disabled in production
- Audit logging captures all security-relevant events
- Monitoring detects anomalies and alerts
- Manual or automated response procedures documented
Self-Check
- What's an example of a secure default vs insecure default?
- How does defense in depth help if one layer is breached?
- If you had to choose between strong auth or strong encryption, which layer would you strengthen?
One layer broken is a problem; all layers broken is unlikely. Layer defenses and secure by default to prevent misconfiguration.
Next Steps
- Read Complete Mediation for enforcement mechanisms
- Study Fail Securely for handling failures safely
- Explore Identity & Access for authentication/authorization layers
Layered Security in Practice
Example: Financial Transaction System
Attack Scenario: Attacker targets a transaction service
Layer 1: Perimeter (Network)
- Firewall allows only 443 (HTTPS)
- DDoS protection (CloudFlare, AWS Shield)
- Rate limiting (max 1000 req/s per IP)
→ Attacker's bulk requests blocked at perimeter
Layer 2: Transport
- TLS 1.3 enforced (encrypts all data in transit)
- Certificate pinning (prevents MITM)
- mTLS for service-to-service (mutual auth)
→ Attacker can't eavesdrop or impersonate
Layer 3: Application (Auth & Authz)
- OAuth 2.0 + JWT tokens (verify identity)
- Multi-factor authentication (verify user owns account)
- Rate limiting (max 10 API calls/minute per user)
- Input validation (reject malformed requests)
→ Attacker can't authorize requests without user creds
Layer 4: Business Logic
- Transaction amount validation (reject invalid amounts)
- Duplicate transaction detection (idempotency keys)
- Fraud detection (ML models flag suspicious patterns)
→ Attacker can't exploit logical vulnerabilities
Layer 5: Data
- Encryption at rest (AES-256, stored in HSM)
- Row-level security (users see only own data)
- Database activity monitoring (detect unusual queries)
→ Attacker can't read sensitive data even if DB compromised
Layer 6: Monitoring & Response
- Audit logging (all access logged)
- Anomaly detection (unusual activity triggers alert)
- Automated response (revoke tokens, isolate resources)
- Incident response playbook
→ Attacker detected quickly, containment automated
Result: Attacker must breach all 6 layers. Each layer independent.
Breaching one doesn't cascade to others.
Secure Defaults Checklist
| Feature | Insecure Default | Secure Default |
|---|---|---|
| Passwords | Optional | Required, min 12 chars, complex |
| MFA | Opt-in | Required, SMS/email as minimum |
| HTTPS | Optional | Enforced, HTTP redirects to HTTPS |
| Debugging | Always on | Disabled, only with DEBUG_MODE env var |
| Data access | Public by default | Private, explicit share required |
| API endpoints | Unauthenticated | Authenticated, require API key |
| Error messages | Verbose (reveals system info) | Generic (security by obscurity) |
| Dependencies | Any version | Only explicitly approved versions |
| Permissions | All privileges | Least privilege, users request access |
| Audit logging | Optional | Always on, immutable logs |
Building Secure-by-Default Products
# Bad product: Insecure by default
class UserRepository:
def get_user(self, user_id):
# Accessible to anyone
return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
# Problem: Developer can accidentally expose user data
---
# Good product: Secure by default
class UserRepository:
def get_user(self, user_id, requester_id):
# Access control built in
if not self.authorize(requester_id, 'view_user', user_id):
raise UnauthorizedError("Cannot view this user")
return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
# Better: Developer must provide context, access control enforced at library level
---
# Best: Secure framework
class DatabaseAccess:
@require_auth('view_user')
@audit_log('user_view')
def get_user(self, user_id):
# Authentication & authorization enforced at framework level
# No way to bypass without explicitly removing decorators
return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
# Developer can't accidentally open security holes
Self-Check
-
What's an example of a secure default vs insecure default?
- Insecure: Admin accounts default to password 'admin' (user must change)
- Secure: Admin accounts created without password, require setup (user must configure)
-
How does defense in depth help if one layer is breached?
- Single layer: One breach = full compromise
- Multiple layers: One breach = limited damage, other layers still protect
- Example: Stolen password + encryption = data still protected
-
If you had to choose between strong auth or strong encryption, which layer would you strengthen?
- Both are important, but auth is higher priority
- Auth prevents breach in first place
- Encryption protects if breach occurs
- Ideal: Both strong
Real-World Example: Payment System Layers
User makes $100 payment:
Layer 1 (Network): Request transmitted over TLS
Layer 2 (Auth): User authenticated with OAuth + MFA
Layer 3 (Business Logic): Amount validated (under user's limit)
Layer 4 (Fraud): ML model flags as normal transaction
Layer 5 (Data): Card number encrypted before storage
Layer 6 (Monitoring): Transaction logged for audit
If Layer 1 breached (someone intercepts HTTPS):
→ TLS decryption hard (AES-256)
→ But: Layer 2 stops them (can't authenticate)
If Layer 2 breached (attacker steals password):
→ MFA required (can't authenticate without SMS)
→ But: Layer 3 stops them (fraudulent amounts detected)
If Layer 3 breached (attacker crafts large transaction):
→ Fraud detection stops them (ML flags suspicious activity)
→ But: Layer 5 stops them (can't read encrypted card data)
Result: $100 transaction successful, attacker stopped at multiple layers
One Takeaway
One layer broken is a problem; all layers broken is unlikely. Layer defenses and secure by default to prevent misconfiguration and reduce blast radius of breaches.