Last reviewed 11 min read

Section 05

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 & Schroeder's classic work, guide architects in creating inherently secure systems.

Principles across the SDLC

Saltzer & Schroeder principles mapped to each lifecycle phase, from requirements to operations.

Scroll sideways to see the whole diagram

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) Vulnerability & Patch Management Known-vulnerable Apache Struts left unpatched; crafted Content-Type header triggered OGNL expression injection and remote code execution (CVE-2017-5638)
Capital One (2019) Least Privilege IAM role had overly broad S3 access, SSRF exploited to reach metadata
SolarWinds (2020) Supply-Chain Integrity / Complete Mediation Compromised build pipeline produced a trusted, signed update without verification — supply chain injection
Log4Shell (2021) Supply-Chain / Dependency Integrity & Timely Patching Logging dependency performed JNDI lookups with an RCE path; widely deployed and slow to patch
Okta / Lapsus$ (2022) Separation of Privilege Single contractor laptop gave access to internal admin tools

These are one-line summaries of principle violations. The canonical, detailed case studies are in Case Studies.

Practical: Fail-Safe Middleware

failsafe-middleware.py
"""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__)

class AuthenticationError(Exception):
    """Raised when a token is missing, malformed, or fails verification."""

class AuthorizationError(Exception):
    """Raised when an authenticated principal lacks the required permission."""

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: unexpected/infrastructure error = deny (fail closed),
            # but surface it as 503 rather than misclassifying it as a 401.
            logger.error("Unexpected auth error: %s", e)
            return jsonify({"error": "service_unavailable"}), 503
    return wrapper

Practical: Least Privilege Azure Role Assignment

least-privilege-role.json
{
  "properties": {
    "roleName": "App Uploads Contributor",
    "description": "Scoped write access to app-uploads-prod container with encryption and IP restrictions",
    "type": "CustomRole",
    "assignableScopes": [
      "/subscriptions/{subscription-id}/resourceGroups/prod-rg"
    ],
    "permissions": [
      {
        "actions": [],
        "notActions": [],
        "dataActions": [
          "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read",
          "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
        ],
        "notDataActions": [
          "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/delete"
        ]
      }
    ]
  }
}
assign-role.sh
# Conditions belong on the ROLE ASSIGNMENT, not the role definition.
az role assignment create \
  --assignee "<app-service-principal-object-id>" \
  --role "App Uploads Contributor" \
  --scope "/subscriptions/{subscription-id}/resourceGroups/prod-rg" \
  --condition "(@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEquals 'app-uploads-prod')" \
  --condition-version "2.0"

Privacy by Design

Privacy by Design (PbD) is a framework developed by Ann Cavoukian that embeds privacy into the design of systems. It gives effect to GDPR Article 25 (data protection by design and by default) 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

Data you don't collect can't be breached. The most secure data is data that doesn't exist.

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

Practical: Data Anonymization

anonymize.py
"""Privacy by Design — anonymize PII before analytics processing."""
import hashlib
import os
import re

# Keyed pseudonymization: the pepper is a secret loaded from the environment.
# Plain sha256(email) is brute-forceable from a list of known addresses; a
# keyed secret defeats precomputation. Fail fast if it is not configured.
PEPPER = os.environ["ANON_PEPPER"]

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

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

NIST CSF 2.0: GV.OC (Organizational Context), ID.AM (Asset Management), PR.DS (Data Security)
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 →

Secure by Design Labs

Audit a feature against the Saltzer & Schroeder principles and remove the data you do not need.

Audit a Feature Against the Principles Custom Lab medium
Pick one feature and rate it against each of the eight Saltzer & Schroeder principlesIdentify the principle it violates most and the design change that fixes itCheck the defaults: does the feature deny by default and fail closed?Confirm no security decision depends on the secrecy of the designWrite the change as an ADR with the security implication
Data Minimization Pass Custom Lab easy
List every field your system collects and the reason it is neededMark fields with no owner or no stated purpose and delete or anonymize themSet a retention period for each remaining field and automate deletionConfirm PII is hashed or pseudonymized before it reaches analyticsRun the Secure Defaults Checklist and fix one failing item

Evidence to Capture

Principles are only reviewable when a decision cites one and an artefact shows it was implemented.

Decision record: The design decision, the principle it invokes (fail-safe defaults, least privilege, economy of mechanism), and the trade-off accepted. A principle named without a trade-off is usually decoration.

Privilege assignment: Principal, role definition identifier, and the scope it is granted at. A role at subscription or management-group scope is the finding; the role name alone tells you nothing.

Failure path in code: The fallback branch of the middleware: the status code and body returned when the authorization service is unreachable.

Collection delta: The fields the form or API accepts next to the fields the feature actually reads. The unused set is the data-minimisation finding, and the schema is the proof.

Anonymisation parameters: The generalisation or k value, the identifier handling, and whether quasi-identifiers were considered. A hashed email is pseudonymised, not anonymised.

Likely False Positives

Principle violations that are either documented trade-offs or implemented lower in the stack.

  • Broad-looking custom role: A role with a familiar name can still be narrow. Read the actions and notActions lists in the role definition before scoring the assignment.
  • Retention that looks excessive: A field kept beyond its apparent purpose may be a statutory requirement. The retention schedule is the evidence that closes it.
  • Unauthenticated health endpoint: Liveness and readiness probes must not depend on the authorization service, or an outage becomes a restart loop. Exempting them is correct design.
  • Identifier in a fixture: Test data that resembles personal data is a data-handling issue only when it derives from production. Check provenance before raising a privacy finding.
  • Protection pushed to the platform: Encryption at the storage layer instead of the application is a legitimate placement when the key is separately custodied. Look for the key policy.

Review Checklist

Principles applied

  • ☐ Confirm each principle cited in the design has an artefact showing the implementation.
  • ☐ Check fail-safe defaults on every control that can fail independently.
  • ☐ Look for complete mediation: a second code path to the same resource that skips the check.
  • ☐ Confirm the least-common-mechanism choice does not collapse two protections into one component.

Privilege and defaults

  • ☐ Read the role definition actions list and confirm no wildcard covers write operations.
  • ☐ Check the assignment scope is the narrowest that still works, and that it is time-bound where the platform allows.
  • ☐ Confirm defaults for a new resource are closed: private networking, no public endpoint, logging on.
  • ☐ Check that a privilege escalation path was considered, not only the direct grant.

Data

  • ☐ Compare the accepted fields against the fields the feature reads and list the delta.
  • ☐ Confirm identifiers are tokenised or removed before data leaves its purpose boundary.
  • ☐ Check the anonymisation parameters are stated and tested against a re-identification attempt.
  • ☐ Verify retention and deletion are enforced by configuration rather than by procedure.

Remediation

Fix the default, then the exception

Structural

  • • Change the platform default so a newly created resource is closed, and require an explicit opt-in to expose it.
  • • Replace the broad assignment with a scoped one at the resource group or resource level, and delete the inherited one rather than layering a second grant.
  • • Move the identifier handling into the write path so minimisation is enforced by the schema, not by a review.

Compensating

  • • Where a broad grant is temporarily unavoidable, add a time-bound approval and alert on use of the elevated permission.
  • • For retained personal data, record the legal basis, the owner, and the deletion date in the data inventory.