Defense in Depth: Beyond Password Hashing
Defense in Depth: Beyond Password Hashing
Password hashing represents just one layer in a comprehensive authentication security strategy. Relying solely on strong hashing creates a single point of failure that sophisticated attackers can exploit. Defense in depth applies multiple independent security controls, ensuring that compromise of one layer doesn't lead to complete system breach. This approach acknowledges that no single security measure is perfect and that real-world systems face diverse, evolving threats.
Rate limiting forms the first line of defense against online attacks. By restricting the number of authentication attempts per account, IP address, or time period, systems prevent brute force attacks regardless of password hash strength. Effective rate limiting must balance security with usability—overly restrictive limits frustrate legitimate users, while permissive limits enable attacks. Progressive delays, where wait times increase with failed attempts, provide good balance.
import time
import redis
from datetime import datetime, timedelta
from functools import wraps
class RateLimiter:
"""Comprehensive rate limiting for authentication systems"""
def __init__(self, redis_client):
self.redis = redis_client
def check_rate_limit(self, identifier, limit_type='login'):
"""Check if action is rate limited"""
limits = {
'login_per_account': (5, 300), # 5 attempts per 5 minutes
'login_per_ip': (20, 300), # 20 attempts per 5 minutes
'global_failed': (1000, 3600), # 1000 failures per hour globally
'password_reset': (3, 3600), # 3 resets per hour
}
if limit_type not in limits:
return False, 0
max_attempts, window_seconds = limits[limit_type]
key = f"rate_limit:{limit_type}:{identifier}"
# Get current attempt count
attempts = self.redis.incr(key)
# Set expiry on first attempt
if attempts == 1:
self.redis.expire(key, window_seconds)
if attempts > max_attempts:
ttl = self.redis.ttl(key)
return True, ttl
return False, 0
def progressive_delay(self, username):
"""Implement progressive delays for failed attempts"""
fail_key = f"failed_attempts:{username}"
failures = self.redis.incr(fail_key)
# Set expiry for 1 hour
if failures == 1:
self.redis.expire(fail_key, 3600)
# Calculate delay based on failures
if failures <= 3:
delay = 0
elif failures <= 5:
delay = 2 ** (failures - 3) # 2, 4 seconds
elif failures <= 10:
delay = 30 # 30 seconds
else:
delay = 300 # 5 minutes
return delay, failures
def clear_limits(self, username, ip_address):
"""Clear rate limits after successful login"""
keys_to_clear = [
f"rate_limit:login_per_account:{username}",
f"rate_limit:login_per_ip:{ip_address}",
f"failed_attempts:{username}"
]
for key in keys_to_clear:
self.redis.delete(key)
# Example usage with decorator pattern
def rate_limited(limit_type='login'):
"""Decorator for rate-limited endpoints"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Extract identifier based on limit type
request = args[0] # Assuming first arg is request
if limit_type == 'login_per_ip':
identifier = request.remote_addr
elif limit_type == 'login_per_account':
identifier = request.form.get('username', 'unknown')
else:
identifier = 'global'
# Check rate limit
limiter = RateLimiter(redis.Redis())
is_limited, retry_after = limiter.check_rate_limit(identifier, limit_type)
if is_limited:
return {
'error': 'Too many attempts. Please try again later.',
'retry_after': retry_after
}, 429
return func(*args, **kwargs)
return wrapper
return decorator
Account lockout policies provide another defensive layer but require careful implementation to avoid denial-of-service vulnerabilities. Temporary lockouts after repeated failures protect against sustained attacks while allowing legitimate users to regain access. Permanent lockouts should trigger administrative review rather than automatic enforcement. Consider implementing CAPTCHA challenges before lockouts to distinguish automated attacks from user errors.