Skip to main content

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 Takeaway

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

FeatureInsecure DefaultSecure Default
PasswordsOptionalRequired, min 12 chars, complex
MFAOpt-inRequired, SMS/email as minimum
HTTPSOptionalEnforced, HTTP redirects to HTTPS
DebuggingAlways onDisabled, only with DEBUG_MODE env var
Data accessPublic by defaultPrivate, explicit share required
API endpointsUnauthenticatedAuthenticated, require API key
Error messagesVerbose (reveals system info)Generic (security by obscurity)
DependenciesAny versionOnly explicitly approved versions
PermissionsAll privilegesLeast privilege, users request access
Audit loggingOptionalAlways 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

  1. 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)
  2. 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
  3. 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.

References