Secure by Design Principles
Secure by Design means building security into the foundation of systems rather than adding it later. These principles, derived from Saltzer and Schroeder's classic work, guide architects in creating inherently secure systems.
Saltzer & Schroeder's Principles
1. Economy of Mechanism
Keep the design as simple as possible. Every added complexity increases the attack surface.
- • Simpler systems are easier to analyze and verify
- • Fewer components = fewer vulnerabilities
- • Avoid clever or tricky implementations
2. Fail-Safe Defaults
Base access decisions on permission rather than exclusion. Default to deny.
- • New users have no access until explicitly granted
- • Missing configuration = secure state
- • Errors result in denied access, not granted
3. Complete Mediation
Every access to every object must be checked for authorization.
- • No caching of access decisions that can go stale
- • Check permissions at each request, not just login
- • Verify authorization at the resource level
4. Open Design
Security should not depend on the secrecy of the design (Kerckhoffs's principle).
- • Algorithms should be publicly known and reviewed
- • Security comes from keys, not hidden mechanisms
- • Open source allows more eyes to find bugs
5. Separation of Privilege
Require multiple conditions or parties to grant access to critical operations.
- • Two-person rule for sensitive operations
- • MFA requires multiple factors
- • Approval workflows for privileged actions
6. Least Privilege
Every program and user should operate with the minimum privileges needed.
- • Time-limited elevated access
- • Scoped API tokens and service accounts
- • Drop privileges after initialization
7. Least Common Mechanism
Minimize shared mechanisms between users to reduce information leakage.
- • Separate processes for different trust levels
- • Tenant isolation in multi-tenant systems
- • Avoid shared caches with sensitive data
8. Psychological Acceptability
Security mechanisms should not make the system harder to use than without them.
- • Users shouldn't need to bypass security to work
- • Intuitive security interfaces
- • Balance security with usability
Real Breaches Mapped to Principle Violations
| Breach | Violated Principle | What Failed |
|---|---|---|
| Equifax (2017) | Complete Mediation | Unpatched Apache Struts — access checks skipped via deserialization |
| Capital One (2019) | Least Privilege | IAM role had overly broad S3 access, SSRF exploited to reach metadata |
| SolarWinds (2020) | Economy of Mechanism | Complex build pipeline trusted without verification — supply chain injection |
| Log4Shell (2021) | Least Common Mechanism | Logging library performed JNDI lookups — shared mechanism with RCE path |
| Okta / Lapsus$ (2022) | Separation of Privilege | Single contractor laptop gave access to internal admin tools |
Practical: Fail-Safe Middleware
"""Fail-safe middleware — deny access on ANY error condition."""
from flask import Flask, request, jsonify, g
from functools import wraps
import logging
logger = logging.getLogger(__name__)
def fail_safe_auth(f):
"""If anything goes wrong during auth, default to DENY."""
@wraps(f)
def wrapper(*args, **kwargs):
try:
# Attempt to verify token
token = request.headers.get("Authorization", "")
if not token:
return jsonify({"error": "unauthorized"}), 401
user = verify_token(token) # May throw
check_permissions(user, request) # May throw
g.current_user = user
return f(*args, **kwargs)
except AuthenticationError:
logger.warning("Auth failed for %s", request.remote_addr)
return jsonify({"error": "unauthorized"}), 401
except AuthorizationError:
logger.warning("Authz denied for %s", getattr(g, 'current_user', 'unknown'))
return jsonify({"error": "forbidden"}), 403
except Exception as e:
# FAIL SAFE: any unexpected error = deny
logger.error("Unexpected auth error: %s", e)
return jsonify({"error": "unauthorized"}), 401 # NOT 500
return wrapper"""Fail-safe middleware — deny access on ANY error condition."""
from flask import Flask, request, jsonify, g
from functools import wraps
import logging
logger = logging.getLogger(__name__)
def fail_safe_auth(f):
"""If anything goes wrong during auth, default to DENY."""
@wraps(f)
def wrapper(*args, **kwargs):
try:
# Attempt to verify token
token = request.headers.get("Authorization", "")
if not token:
return jsonify({"error": "unauthorized"}), 401
user = verify_token(token) # May throw
check_permissions(user, request) # May throw
g.current_user = user
return f(*args, **kwargs)
except AuthenticationError:
logger.warning("Auth failed for %s", request.remote_addr)
return jsonify({"error": "unauthorized"}), 401
except AuthorizationError:
logger.warning("Authz denied for %s", getattr(g, 'current_user', 'unknown'))
return jsonify({"error": "forbidden"}), 403
except Exception as e:
# FAIL SAFE: any unexpected error = deny
logger.error("Unexpected auth error: %s", e)
return jsonify({"error": "unauthorized"}), 401 # NOT 500
return wrapperPractical: Least Privilege IAM Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificS3Bucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::app-uploads-prod/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
},
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
}
}
},
{
"Sid": "DenyAllElse",
"Effect": "Deny",
"Action": "s3:*",
"NotResource": "arn:aws:s3:::app-uploads-prod/*"
}
]
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificS3Bucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::app-uploads-prod/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
},
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
}
}
},
{
"Sid": "DenyAllElse",
"Effect": "Deny",
"Action": "s3:*",
"NotResource": "arn:aws:s3:::app-uploads-prod/*"
}
]
}Practical: Data Anonymization
"""Privacy by Design — anonymize PII before analytics processing."""
import hashlib
import re
def anonymize_record(record: dict) -> dict:
"""Strip or hash PII fields while preserving analytical value."""
anon = record.copy()
# k-anonymity: generalize quasi-identifiers
if "age" in anon:
anon["age_range"] = f"{(anon['age'] // 10) * 10}-{(anon['age'] // 10) * 10 + 9}"
del anon["age"]
# Pseudonymize direct identifiers
if "email" in anon:
anon["user_hash"] = hashlib.sha256(
(anon["email"] + PEPPER).encode()
).hexdigest()[:16]
del anon["email"]
# Suppress high-sensitivity fields entirely
for field in ["ssn", "credit_card", "phone"]:
anon.pop(field, None)
# Redact IPs to /24
if "ip_address" in anon:
anon["ip_subnet"] = re.sub(r"\.\d+$", ".0/24", anon["ip_address"])
del anon["ip_address"]
return anon"""Privacy by Design — anonymize PII before analytics processing."""
import hashlib
import re
def anonymize_record(record: dict) -> dict:
"""Strip or hash PII fields while preserving analytical value."""
anon = record.copy()
# k-anonymity: generalize quasi-identifiers
if "age" in anon:
anon["age_range"] = f"{(anon['age'] // 10) * 10}-{(anon['age'] // 10) * 10 + 9}"
del anon["age"]
# Pseudonymize direct identifiers
if "email" in anon:
anon["user_hash"] = hashlib.sha256(
(anon["email"] + PEPPER).encode()
).hexdigest()[:16]
del anon["email"]
# Suppress high-sensitivity fields entirely
for field in ["ssn", "credit_card", "phone"]:
anon.pop(field, None)
# Redact IPs to /24
if "ip_address" in anon:
anon["ip_subnet"] = re.sub(r"\.\d+$", ".0/24", anon["ip_address"])
del anon["ip_address"]
return anonPrivacy by Design
Privacy by Design (PbD) is a framework developed by Ann Cavoukian that embeds privacy into the design of systems. It's required by GDPR and similar regulations.
1. Proactive not Reactive
Anticipate and prevent privacy issues before they occur.
2. Privacy as Default
Maximum privacy without user action required.
3. Privacy in Design
Embedded into design, not added as an afterthought.
4. Full Functionality
Avoid false tradeoffs; achieve both privacy and functionality.
5. End-to-End Security
Protect data throughout its entire lifecycle.
6. Visibility & Transparency
Operations verifiable by users and auditors.
7. Respect for User Privacy
User-centric design with strong defaults, consent mechanisms, and data subject rights.
Data Minimization
The Best Security
Collection Minimization
- • Only collect data you actually need
- • Question every field in forms
- • Use derived data instead of raw data when possible
Retention Minimization
- • Define retention periods for all data types
- • Automate data deletion
- • Anonymize instead of delete when aggregates needed
Access Minimization
- • Limit who can access sensitive data
- • Implement need-to-know access controls
- • Log and audit all access to sensitive data
Secure Defaults Checklist
- ☐ Authentication required for all endpoints by default
- ☐ HTTPS enforced, HTTP redirects to HTTPS
- ☐ Security headers enabled (CSP, HSTS, X-Frame-Options)
- ☐ Debug mode disabled in production
- ☐ Error messages don't expose stack traces
- ☐ Admin interfaces on separate ports/paths
- ☐ Database connections use TLS
- ☐ Logging enabled but sensitive data redacted
- ☐ Rate limiting active on all endpoints
- ☐ CORS restricted to known origins
Framework Alignment
ISO 27002:2022: A.5.1 (Information Security Policies), A.5.9 (Inventory of Information), A.8.11 (Data Masking), A.8.12 (Data Leakage Prevention)
CIS Controls v8.1: 3 (Data Protection), 4 (Secure Configuration of Enterprise Assets)
GDPR Articles: Art. 25 (Data Protection by Design/Default), Art. 5.1c (Data Minimisation)
Related: Security Frameworks →