R0KernelPath
module 233 / 262

Security Principles & Threat Modeling

Security is not a feature you bolt on — it is a mindset you build in from the first line of design.

~18h focused study10 concepts

Overview

Security engineering begins not with firewalls or encryption, but with thinking. Before writing a single line of defense, a security engineer asks: who are my adversaries, what do they want, how might they get it, and what would it cost them? This module teaches you that mental discipline — the analytical scaffolding that distinguishes a security practitioner from someone who merely installs software.

The field is built on a surprisingly small number of foundational ideas. The CIA triad gives you a vocabulary for what you are protecting. Threat modeling gives you a systematic method for discovering what can go wrong before it does. The principle of least privilege and defense in depth give you architectural heuristics that have withstood decades of real-world attack. Together, these ideas form the lens through which every serious security engineer reads a design.

What makes this material fascinating — and genuinely difficult — is that security is an adversarial discipline. In most of computer science, you write code and nature is indifferent: if your sorting algorithm is slow, it is slow uniformly. In security, nature has an agenda. Your adversaries are intelligent, adaptive, patient, and motivated. A system that is 99.9% correct can still be 100% broken if that 0.1% is the authentication check. This asymmetry is what makes security both humbling and thrilling.

This module lays the intellectual foundation for everything that follows in the Security and Cryptography track — from applied cryptography to web security to operating system hardening. Master these principles and you will find yourself noticing vulnerabilities in designs long before any tool flags them, because you will have internalized how attackers actually think.

why this matters for the kernel

Security failures are not exotic edge cases — they are the dominant failure mode of modern software systems. Data breaches expose billions of records. Ransomware shuts down hospitals. Supply-chain attacks compromise millions of downstream users through a single trusted dependency. These failures almost never happen because cryptography was broken; they happen because someone did not think carefully about who could do what to whom, and under what circumstances.

Understanding security principles matters not just for security specialists but for every software engineer, because every system is a security system whether you intend it or not. A web developer who does not understand the same-origin policy will build a vulnerable application. A systems programmer who does not reason about privilege will write code that becomes a pivot point for attackers. A database architect who does not model their threat surface will store secrets in places they should not. The principles in this module — CIA, threat modeling, least privilege, defense in depth — are the shared vocabulary of the entire field, and internalizing them changes how you read every design document and code review for the rest of your career.

Threat Model Data Flow with Trust Boundaries

rendering diagram…

A typical three-tier web application with explicit trust boundaries. Each arrow crossing a boundary is a potential attack vector and must validate all inputs. STRIDE threats map to specific boundary crossings: Spoofing and Tampering at the browser-to-DMZ boundary; Information Disclosure and Elevation of Privilege at the DMZ-to-internal boundary.

The concepts

01The CIA Triad: Confidentiality, Integrity, Availability

The CIA triad is the foundational taxonomy of security properties. Every security goal in every system can be expressed as protecting one or more of these three properties.

Confidentiality means that information is accessible only to those authorized to see it. Your bank balance should not be readable by random strangers. A private message should not be interceptable by a network eavesdropper. The mechanisms that enforce confidentiality include encryption, access controls, and information-flow policies.

Integrity means that data and systems are modified only by authorized parties in authorized ways. Your bank balance should not be changeable by unauthorized parties (even if they cannot read it). An executable binary on disk should not be silently replaced by malware. Integrity protections include cryptographic hashes, digital signatures, and write access controls. Crucially, integrity is a distinct property from confidentiality: a message can be readable by the public but still need integrity guarantees (you need to know it came from who it claims to).

Availability means that authorized users can access the system when they need to. A service that has been DDoS'd to silence, a database encrypted by ransomware, or a disk that has been physically destroyed has had its availability compromised. Availability protections include redundancy, rate limiting, backups, and DDoS mitigation.

These three properties are in tension with each other in interesting ways. Maximum confidentiality (encrypt everything, trust no one) hurts availability (now you need key management, and key loss = data loss). Maximum availability (cache everything everywhere, replicate aggressively) hurts confidentiality (more copies = more exposure surface). The CIA triad does not resolve these tensions — it names them clearly so you can reason about the tradeoffs.

A fourth property, Non-repudiation, is sometimes added to form the CIAAN or CIA+ triad. Non-repudiation means that an actor cannot credibly deny having performed an action — think of a digital signature that proves you signed a contract. It is essential in legal and financial contexts.

Concrete example: Consider an email system.

  • Confidentiality: TLS in transit and end-to-end encryption (e.g., PGP) prevent eavesdropping.
  • Integrity: DKIM signatures verify that the message body was not tampered with after leaving the sender's server.
  • Availability: Redundant MX records and server clustering ensure the mail server responds even if one node fails.
# Minimal illustration: checking all three CIA properties

import hashlib, hmac, time

# CONFIDENTIALITY: only authorized users can read
def read_secret(user, secret_store):
    if user not in secret_store['authorized']:
        raise PermissionError("Access denied")  # confidentiality enforced
    return secret_store['data']

# INTEGRITY: detect unauthorized modification
def verify_integrity(data: bytes, expected_mac: bytes, key: bytes) -> bool:
    computed = hmac.new(key, data, hashlib.sha256).digest()
    return hmac.compare_digest(computed, expected_mac)  # timing-safe compare

# AVAILABILITY: fail gracefully, don't let one bad request kill the service
def rate_limited_handler(request_log, max_per_second=100):
    now = time.time()
    recent = [t for t in request_log if now - t < 1.0]
    if len(recent) >= max_per_second:
        return 429  # Too Many Requests — availability protection
    recent.append(now)
    return 200

02Threat Modeling: Systematic Adversarial Thinking

Threat modeling is the process of systematically identifying what could go wrong in a system, who might cause it, and how to prioritize defenses. It is the security equivalent of a failure mode analysis in engineering — except your failure modes are intelligent.

The most widely used threat modeling framework is STRIDE, developed at Microsoft. STRIDE is an acronym for six categories of threats:

CategoryProperty ViolatedExample
SpoofingAuthenticityAttacker pretends to be the admin
TamperingIntegrityAttacker modifies data in transit
RepudiationNon-repudiationUser denies having placed an order
Information DisclosureConfidentialityStack trace leaks database schema
Denial of ServiceAvailabilityFlood of requests crashes the server
Elevation of PrivilegeAuthorizationRegular user gains admin access

The threat modeling process follows four steps:

  1. Decompose the system: draw a Data Flow Diagram (DFD) that shows every component, every data store, every external entity, and every trust boundary (a line across which data flows between different privilege levels).
  2. Identify threats: for each element in the DFD, systematically ask which STRIDE categories apply.
  3. Mitigate threats: for each identified threat, determine a countermeasure or accept the risk with documented rationale.
  4. Validate: verify that mitigations work and reassess after changes.

PASTA (Process for Attack Simulation and Threat Analysis) is a risk-centric alternative that maps threats to business impact. VAST scales threat modeling to large organizations with automation. For most developers, STRIDE on DFDs is the right starting point.

The most important output of threat modeling is not a list of vulnerabilities — it is a shared mental model of the system among the team. When everyone has done a threat model together, design reviews become conversations about security rather than after-thoughts.

# Pseudocode: a simple threat model for a login endpoint
# Component: POST /login
# Trust boundary: Internet → Web Server

threats = [
    # (STRIDE category, description, mitigation)
    ("Spoofing",    "Attacker submits someone else's credentials",
                    "bcrypt password hashing + MFA"),
    ("Tampering",   "MITM modifies username in transit",
                    "TLS 1.3 in transit; reject HTTP"),
    ("Repudiation", "User denies logging in from foreign IP",
                    "Append-only auth log with timestamp + IP"),
    ("Info Disclosure", "Timing attack reveals valid usernames",
                    "Constant-time comparison; same response for bad user/pass"),
    ("DoS",         "Brute-force floods the endpoint",
                    "Exponential backoff + account lockout + CAPTCHA"),
    ("Elevation",   "SQLi in username field grants admin session",
                    "Parameterized queries; WAF; least-privilege DB user"),
]

for category, description, mitigation in threats:
    print(f"[{category}] {description}")
    print(f"  Mitigation: {mitigation}\n")

03Attack Surfaces and Attack Vectors

The attack surface of a system is the total set of points where an adversary can attempt to inject input, extract output, or interfere with behavior. Every exposed endpoint, every input field, every API, every network port, every file the system reads, every environment variable it trusts — all of these are part of the attack surface.

A key insight is that attack surface grows faster than value. Every feature you add increases the attack surface. Every library you import brings in its author's bugs. Every protocol you support is another avenue for abuse. This is why experienced security engineers have an instinctive wariness about feature creep and dependency sprawl — they are not being obstructionist, they are doing surface-area arithmetic.

Attack vectors are specific paths through the attack surface. Common categories:

  • Network vectors: unauthenticated network endpoints, insecure protocols, open ports
  • User input vectors: web forms, API parameters, file uploads, URL paths
  • Physical vectors: USB drops, hardware implants, cold-boot attacks
  • Social engineering: phishing, pretexting (not a software vulnerability, but often the initial access vector)
  • Supply chain: compromised build tools, malicious dependencies (e.g., the XZ Utils backdoor, 2024)
  • Side channels: timing, power consumption, electromagnetic emissions

Reducing attack surface is one of the highest-value security activities:

  • Disable unused services and close unused ports
  • Delete dead code paths
  • Minimize the number of external-facing interfaces
  • Prefer smaller dependencies (or vendor them and audit them)
  • Run services as unprivileged users with minimal filesystem visibility

The concept of attack surface area can be measured with tools like attack surface analyzers, but the mental habit of asking "who can reach this, with what input, and what happens?" is more valuable than any tool.

# Attack surface analysis: a minimal HTTP server
# Each line that touches external input is a potential attack vector

from http.server import BaseHTTPRequestHandler
import subprocess, os

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        # ATTACK VECTOR 1: attacker controls self.path
        # Path traversal: GET /../../../etc/passwd
        filename = self.path.lstrip('/')          # naive
        # ATTACK VECTOR 2: filename used in OS call — command injection risk
        data = open(filename).read()              # path traversal
        self.wfile.write(data.encode())

# Reduced attack surface version:
SAFE_ROOT = '/var/www/static'

class SafeHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # Resolve path and verify it stays inside SAFE_ROOT
        requested = os.path.normpath(
            os.path.join(SAFE_ROOT, self.path.lstrip('/')))
        if not requested.startswith(SAFE_ROOT):
            self.send_error(403)         # reject path traversal
            return
        if not os.path.isfile(requested):
            self.send_error(404)
            return
        with open(requested, 'rb') as f:
            self.wfile.write(f.read())   # only static files, no exec

04The Principle of Least Privilege

The Principle of Least Privilege (PoLP) states: every principal (user, process, service, module) should operate with the minimum set of permissions necessary to perform its legitimate function, for the minimum duration necessary.

The principle was formalized by Jerome Saltzer and Michael Schroeder in their landmark 1975 paper "The Protection of Information in Computer Systems" — one of the most influential papers in security. They derived it from a simple observation: privilege that is not needed cannot be misused. Restricting privilege limits the blast radius of a compromise.

Why it works: when an attacker compromises a component, they inherit that component's permissions. If your web server process runs as root, a web vulnerability gives the attacker root. If it runs as www-data with read access to /var/www and write access to /var/log/nginx, a web vulnerability gives the attacker... not much.

Practical applications:

  1. Unix file permissions: the classic mechanism. rwxr-xr-x — owner can do everything; group and world can only read and execute.
  2. setuid/setgid: allows a program to run with elevated privileges for specific operations (e.g., passwd runs as root but is owned by root with setuid bit). Dangerous if not carefully audited.
  3. Linux capabilities: instead of all-or-nothing root, Linux capabilities split root into ~40 fine-grained privileges (e.g., CAP_NET_BIND_SERVICE lets a process bind port 80 without being root).
  4. Database roles: give the application user SELECT on the tables it reads and INSERT/UPDATE on the tables it writes, never DROP TABLE or access to tables it does not need.
  5. OAuth scopes: request only the permissions your application needs (read:email, not repo:write).
  6. AWS IAM policies: each Lambda function gets a role with exactly the S3 bucket and DynamoDB table it needs — nothing else.

Temporary elevation: even when elevated privilege is necessary, hold it for as short a time as possible. The Unix pattern of seteuid(0) → do privileged work → seteuid(original_uid) immediately exemplifies this.

// C: temporary privilege elevation — drop root immediately after use
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>

int open_protected_log(const char *path) {
    uid_t real_uid = getuid();   // remember who we really are
    // Process was launched with setuid root — we temporarily have euid=0
    int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600);
    // Drop back to real user ID immediately — hold privilege as briefly as possible
    if (seteuid(real_uid) != 0) {
        perror("seteuid failed");  // If we can't drop, abort
        close(fd);
        return -1;
    }
    // From here on, we run as the unprivileged real_uid
    return fd;
}

// Linux capabilities: bind port 80 without being root
// $ sudo setcap cap_net_bind_service=+ep ./myserver
// Now myserver can bind port 80 but has no other root capabilities

05Defense in Depth

Defense in depth (also called layered security) is the principle that security should not depend on any single mechanism. If one layer fails, the next layer should limit the damage. The analogy is a medieval castle: moat, outer wall, inner wall, keep, and last-resort tower. An attacker who crosses the moat has not won — they face the outer wall.

The rationale is probabilistic. If each layer independently fails with probability p, and you have n independent layers, the probability that all layers fail is p^n. Even if p is embarrassingly large (0.1), three independent layers give an overall failure probability of 0.001. (The independence assumption is where real systems often fail — correlated failures from a single root cause can defeat all layers simultaneously.)

Layers in a typical web application:

  1. Perimeter: firewall, WAF (Web Application Firewall), DDoS mitigation
  2. Network: VPC/subnet isolation, security groups, mTLS between services
  3. Host: hardened OS image, SELinux/AppArmor policy, unneeded services removed
  4. Application: input validation, parameterized queries, output encoding, authentication and authorization checks
  5. Data: encryption at rest, column-level encryption for PII, minimal retention
  6. Monitoring: SIEM, anomaly detection, alerts on suspicious access patterns

Defense in depth is not the same as redundancy. Redundancy duplicates the same mechanism to improve reliability. Defense in depth deploys different mechanisms that protect against different failure modes. Five firewalls from the same vendor with the same rule set is redundancy (protects against hardware failure) but not defense in depth (a zero-day in that vendor's firewall bypasses all five).

The insider threat problem: layered defenses focused on the perimeter fail against insiders or post-compromise attackers who are already inside. This is why modern security architecture embraces Zero Trust: never implicitly trust anything based on network location; verify identity and authorization at every hop, every time.

# Defense in depth: three independent checks before a sensitive action
# Each layer uses a different mechanism — all three must pass

def transfer_funds(session_token, amount, to_account):
    # Layer 1 — Authentication: is this a valid session?
    user = session_store.get(session_token)
    if not user or user.session_expired():
        raise AuthError("Invalid or expired session")

    # Layer 2 — Authorization: is this user allowed to transfer?
    if not user.has_permission('transfer_funds'):
        raise AuthzError("Insufficient permissions")

    # Layer 3 — Business logic validation (independent of auth)
    if amount <= 0 or amount > user.daily_limit:
        raise ValueError("Amount out of bounds")
    if not is_valid_account(to_account):  # format + existence check
        raise ValueError("Invalid destination account")

    # Layer 4 — Anomaly detection (async — belt AND suspenders)
    risk_score = fraud_engine.score(user, amount, to_account)
    if risk_score > FRAUD_THRESHOLD:
        queue_for_manual_review(user, amount, to_account)
        raise FraudSuspicion("Transaction held for review")

    # Only now do we execute
    return ledger.execute_transfer(user, amount, to_account)

06Trust Models and Trust Boundaries

A trust model is an explicit statement of what entities a system believes, relies upon, or gives authority to, and under what conditions. Every system has a trust model — the question is whether it is explicit (designed) or implicit (discovered by attackers).

A trust boundary is a line in a system design across which the level of trust changes. Data flowing across a trust boundary must be validated before use, because the source on the other side is less trusted. Classic trust boundaries:

  • Internet → DMZ → internal network → database tier
  • Kernel space → user space
  • Web browser → web server
  • User input → application logic

The fundamental rule: never trust input from across a trust boundary without validation. This is why SQL injection, XSS, and buffer overflows all share a root cause: the application trusted data from a less-trusted domain (user input) and passed it into a privileged operation (SQL query, DOM, memory) without sufficient validation.

The confused deputy problem is a classic trust-model failure. A deputy (a program with privilege) is tricked by a less-privileged client into using its authority inappropriately. The canonical example: a compiler that writes to a billing file when told to by an unprivileged user. The CSRF attack is the web incarnation: a browser (the deputy) is tricked into using its cookie-based authority to make requests on behalf of a malicious site.

Ambient authority vs capability-based security: most systems grant authority based on identity (who you are) and ambient authority (all permissions you hold, all the time). Capability-based systems grant authority by passing unforgeable tokens (capabilities) that give access only to the specific resource you need. Object-capability systems (like Deno's permission model) make least privilege the default rather than the exception.

# Confused Deputy — CSRF example (pseudocode)
# The browser (deputy) has auth cookies for bank.com
# Attacker's page tricks it into using that authority

# On evil.com:
# <img src="https://bank.com/transfer?to=attacker&amount=1000">
# Browser sends the request WITH the victim's bank.com cookie
# The bank sees a valid authenticated request — confused deputy!

# Mitigation: CSRF token (synchronizer token pattern)
# Server generates a random per-session token and embeds it in forms
# Browser must send back the token in the request body (not auto-sent)
# An attacker on evil.com cannot read the token (same-origin policy)
# so they cannot forge a valid request

# Flask example:
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
CSRFProtect(app)   # now every POST must include a valid CSRF token

# The CSRF token is NOT a cookie — it must come from the response body
# An attacker who cannot read the page cannot extract it

07Thinking Like an Attacker: Adversarial Mindset

Security is unique among engineering disciplines in that correctness is not enough. You must also think about exploitation: how might a clever adversary leverage imperfection? This adversarial mindset is a learnable skill, not an innate talent.

The attacker's goal is asymmetric. You must defend everything; the attacker needs to find one flaw. You must be right every time; they only need to be right once. This asymmetry means that security cannot be achieved by "doing enough" — it requires prioritizing correctly.

Attacker modeling: a threat model is only as good as its adversary model. When modeling threats, be specific:

  • What is their motivation? (Financial gain, espionage, sabotage, curiosity, activism)
  • What are their capabilities? (Script kiddie with public exploits vs. nation-state with zero-days and months of time)
  • What is their access? (External attacker, authenticated user, insider with physical access)
  • What are their constraints? (Time, cost, risk of detection)

In most real-world threat models, you do not need to defend against a nation-state adversary with unlimited resources and zero-days. You need to defend against financially motivated attackers who will move on when the cost exceeds the reward. This is risk-based security.

Red teaming is the practice of having a dedicated team attempt to break a system, simulating a real attacker. The red team's job is to find paths that the builders missed because builders think about how a system is supposed to work; attackers think about how it can be made to behave unexpectedly.

Five attacker habits to internalize:

  1. Look for input that is never validated (the forgotten code path)
  2. Look for privilege that is never dropped (the lingering authority)
  3. Look for state that is not checked at the right time (TOCTOU — Time-Of-Check Time-Of-Use races)
  4. Look for error paths that skip security checks (the exception handler that bypasses auth)
  5. Look for default configurations (default passwords, open debug endpoints, verbose error messages)
# TOCTOU (Time-of-Check, Time-of-Use) race condition
# Classic attacker technique: change the resource between the check and the use

import os

# VULNERABLE: check then act — window for a symlink swap
def write_to_file_unsafe(path: str, data: str):
    if os.path.exists(path) and not os.path.isfile(path):
        raise ValueError("Not a file")         # CHECK
    # Between here and the open() call, attacker replaces `path`
    # with a symlink to /etc/passwd — the open() follows the symlink!
    with open(path, 'w') as f:                 # USE — race condition
        f.write(data)

# SAFE: use O_NOFOLLOW and open with exclusive flags
def write_to_file_safe(path: str, data: str):
    # O_CREAT|O_EXCL fails if file exists (atomic) — no race
    # O_NOFOLLOW refuses to follow symlinks
    fd = os.open(path,
                 os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW,
                 0o600)
    with os.fdopen(fd, 'w') as f:
        f.write(data)
    # The check and the use are now the SAME atomic operation

08Security Policies and Security Mechanisms

A fundamental distinction in security engineering is between policies (what is allowed) and mechanisms (how it is enforced). This separation is not merely academic — conflating them is a common source of design errors.

A security policy is a precise statement of the desired security properties of a system. Examples:

  • "No user may read another user's files" (Bell-LaPadula confidentiality)
  • "The integrity of this binary must be verified before execution" (code signing)
  • "All traffic between microservices must be mutually authenticated" (Zero Trust policy)

A security mechanism is a technical implementation that is supposed to enforce a policy. Examples:

  • Unix file permissions (mechanism for the first policy above)
  • A digital signature check (mechanism for the second)
  • mTLS with client certificate validation (mechanism for the third)

Why the separation matters: a mechanism can be technically correct but fail to enforce the policy if the policy was wrongly specified. Conversely, a correctly specified policy can be violated if the mechanism has bugs. Security analysis requires asking both: "Is the mechanism correctly implemented?" AND "Does this mechanism actually enforce the policy I care about?"

Saltzer and Schroeder's design principles (1975) remain the gold standard for mechanism design:

  1. Economy of mechanism: keep it simple. Complexity is the enemy of security.
  2. Fail-safe defaults: default to denial, not permission.
  3. Complete mediation: every access to every object must be checked every time (not cached or skipped).
  4. Open design: the design should not depend on secrecy (Kerckhoffs's principle).
  5. Separation of privilege: require more than one condition to grant access.
  6. Least privilege: already covered above.
  7. Least common mechanism: minimize shared state between principals (shared mechanisms are shared attack surface).
  8. Psychological acceptability: the mechanism must be usable enough that users do not circumvent it.
# Policy vs Mechanism: the gap that causes breaches

# POLICY: "only the owner of a record can update it"
# Mechanism: check user_id == record.owner_id before UPDATE

# Correct policy, INCORRECT mechanism (IDOR — Insecure Direct Object Reference):
def update_record_bad(user_id: int, record_id: int, new_data: dict):
    record = db.get(record_id)         # fetch by ID
    db.update(record_id, new_data)     # UPDATE — no ownership check!
    # Attacker: change record_id in the request to any value
    # Policy is defined, mechanism fails to enforce it

# Correct mechanism:
def update_record_good(user_id: int, record_id: int, new_data: dict):
    record = db.get(record_id)
    if record is None:
        raise NotFound()
    if record.owner_id != user_id:     # enforce ownership policy
        raise PermissionError("You do not own this record")
    db.update(record_id, new_data)

# NOTE: the fix is not encryption or logging — it is enforcing
# the policy at every access path (complete mediation).

09Fail-Safe Defaults and Secure by Default

Fail-safe defaults is one of Saltzer and Schroeder's eight principles, and it deserves its own treatment because it is so frequently violated.

The principle: when a system fails, cannot make a determination, or encounters an unexpected state, it should default to the most restrictive behavior. If your authentication system crashes, users should be denied access — not granted it. If your firewall rule does not match a packet, it should drop — not forward.

This seems obvious, but real systems violate it constantly:

  • A firewall misconfiguration logs an error but allows traffic through
  • An auth middleware that throws an exception is caught by a blanket except: pass that continues to the handler
  • A feature flag service is unreachable, so the system defaults to "flag is on" — where the flag controls a security check

Secure by default is the product manifestation of this principle: software should ship in a secure configuration, with insecure features requiring explicit opt-in. Historically, many systems shipped with insecure defaults because security came at a usability cost and operators were expected to harden after deployment. This almost never happened. The modern standard: security on by default, with explicit opt-out for edge cases.

Positive vs negative security models:

  • A positive model (allowlist) specifies what is permitted and denies everything else. Default deny. This is fail-safe.
  • A negative model (denylist) specifies what is forbidden and allows everything else. Default allow. This is NOT fail-safe.

Input validation must use positive models wherever possible. Instead of checking for known-bad characters in a filename (denylist, will miss some), validate that the filename matches the pattern [a-zA-Z0-9_-]+\.[a-z]{2,4} (allowlist, rejects anything unexpected).

# Fail-safe default: deny on error, not allow

def is_authorized(user_id: int, resource_id: int) -> bool:
    try:
        perms = permission_service.get(user_id, resource_id)
        return perms.can_read
    except Exception:
        # WRONG: return True  # "fail open" — grants access on error!
        return False          # RIGHT: "fail closed" — deny on error

# Allowlist (positive model) for file extension validation
import re

ALLOWED_EXTENSION = re.compile(r'^[a-zA-Z0-9_-]+\.(jpg|jpeg|png|gif|pdf)$')

def is_safe_filename(name: str) -> bool:
    # Positive model: only allow known-good patterns
    return bool(ALLOWED_EXTENSION.match(name))
    # Denylist alternative (DO NOT USE):
    # return '../' not in name and not name.endswith('.php')
    # — will miss: null bytes, colons, alternate path separators...

10Risk, Cost-Benefit Analysis, and Residual Risk

Security is not about achieving zero risk — that is impossible. It is about making rational decisions about which risks to accept, mitigate, transfer, or avoid, given limited resources.

Risk is conventionally expressed as:

Risk = Threat × Vulnerability × Impact

Or equivalently: Risk = Likelihood × Impact.

  • Threat: the probability that an adversary will attempt to exploit a weakness (informed by your adversary model)
  • Vulnerability: the probability that the attempt succeeds given the current defenses
  • Impact: the cost to the organization if it does succeed (financial, reputational, legal, operational)

A vulnerability with high impact but near-zero threat (e.g., a nation-state-only attack on a small startup's internal wiki) may be lower priority than a moderate-impact vulnerability with near-certain exploitation (e.g., an unpatched public-facing server).

Countermeasure cost-benefit: a mitigation is rational if its cost is less than the expected loss reduction it provides:

cost_of_countermeasure < (risk_before - risk_after) × asset_value

Residual risk is the risk that remains after mitigations. Every organization has residual risk. The goal is to make it acceptable and visible — documented, monitored, and covered by incident response plans — not zero.

Security economics: Bruce Schneier's observation that security failures are often economic failures. When the cost of a defense exceeds the cost of the attack to the attacker, attackers attack. When defenders incur costs that benefits accrue to others (externalities), systems are under-secured. Understanding these incentive structures is part of being a security engineer.

# Risk scoring: a simple quantitative model
# CVSS (Common Vulnerability Scoring System) does this formally
# Here: a simplified version for internal prioritization

from dataclasses import dataclass

@dataclass
class Vulnerability:
    name: str
    likelihood: float   # 0.0-1.0: probability of exploitation per year
    impact: float       # 0.0-10.0: severity if exploited
    mitigation_cost: float  # arbitrary cost units

    @property
    def risk_score(self) -> float:
        return self.likelihood * self.impact

    def is_worth_fixing(self, asset_value: float) -> bool:
        expected_annual_loss = self.risk_score * asset_value / 10.0
        return self.mitigation_cost < expected_annual_loss

vulns = [
    Vulnerability("SQL injection on login",   0.9, 9.5, 5),
    Vulnerability("Nation-state supply chain", 0.01, 10.0, 50000),
    Vulnerability("User enumeration via timing", 0.3, 3.0, 20),
]

for v in sorted(vulns, key=lambda x: -x.risk_score):
    print(f"{v.name}: risk={v.risk_score:.2f}, fix={v.is_worth_fixing(10000)}")
# Output (at asset_value=10000):
# SQL injection: risk=8.55, fix=True
# User enumeration: risk=0.90, fix=True
# Nation-state: risk=0.10, fix=False  (mitigation costs 500x expected loss)

Key insights

  • ✦Security is adversarial: unlike most engineering, your system is actively probed by intelligent agents who adapt to your defenses. A system that is 99.9% correct can be 100% broken.
  • ✦The CIA triad (Confidentiality, Integrity, Availability) is not a checklist — it is a vocabulary for precise reasoning about tradeoffs. Adding a feature often trades one property for another.
  • ✦Threat modeling done right is not a document that sits in a drawer — it is a shared mental model of the system that changes how every engineer reads every design review.
  • ✦Least privilege and defense in depth are complementary: least privilege minimizes the blast radius of any single compromise; defense in depth ensures that compromising one component does not mean compromising the system.
  • ✦The policy/mechanism distinction is critical: you can have a correctly implemented mechanism that enforces the wrong policy, or a correct policy with a buggy mechanism. Security analysis must check both.
  • ✦Fail-safe defaults are counterintuitive because they prioritize security over availability in failure modes — but in practice, security failures are often more costly than brief unavailability.
  • ✦The confused deputy problem (and its web incarnation, CSRF) shows that trust is transitive and can be exploited: whenever a privileged agent acts on behalf of a less-trusted principal, you need explicit verification.
  • ✦Real security is about risk management, not risk elimination. The right question is never 'is this safe?' but 'is this risk level acceptable given the cost of mitigation?'

Common pitfalls

  • ⚠Security through obscurity: relying on attackers not knowing how your system works (instead of on the system being genuinely hard to exploit). Obscurity is a delaying tactic, not a security property — and it fails permanently the first time the secret is leaked.
  • ⚠Fail-open error handling: catching exceptions from security checks and continuing as if the check passed. Every auth check, every permission check must fail closed.
  • ⚠Perimeter-only thinking: investing everything in the firewall/WAF and assuming internal traffic is safe. Once an attacker is inside the perimeter (via phishing, insider threat, or lateral movement), perimeter-only defenses offer nothing.
  • ⚠Denylist/blacklist validation: trying to enumerate all bad inputs rather than defining and enforcing good ones. Every denylist is incomplete by definition; use allowlists for security-sensitive validation.
  • ⚠TOCTOU bugs from check-then-act patterns: validating a resource, then using it later — the resource can change between check and use. Always combine check and use into a single atomic operation where security depends on it.
  • ⚠Over-privileged service accounts and build systems: the CI/CD pipeline or the monitoring service has admin credentials 'because it's easier.' Supply chain attacks specifically target these over-privileged principals.
  • ⚠Assuming threats are always external: insider threats and post-compromise lateral movement are statistically significant. Zero Trust architecture exists precisely because 'trusted network' is a dangerous assumption.
  • ⚠Conflating authentication and authorization: verifying identity (authn) does not mean the verified identity is allowed to do the thing it is asking for (authz). Both checks are necessary; both must be present at every access path.

Practice

  1. 01
    medium

    Threat model a password reset flow. Draw the data flow diagram (on paper or a tool like draw.io), identify all trust boundaries, then systematically apply each STRIDE category to each component and data flow. Produce a table of at least 8 distinct threats with mitigations. Pay particular attention to the email delivery step.

  2. 02
    medium

    Implement a minimal file-serving HTTP server in Python (or C) that is secure against path traversal attacks. Write a test harness that sends at least five malicious paths (including Unicode-encoded separators, null bytes, and double-dot sequences) and verifies each is rejected. Measure and minimize the attack surface of your implementation.

  3. 03
    hard

    Reproduce a TOCTOU race condition. Write a Python program with two threads: one that performs a check-then-act on a temporary file, and one that replaces the file with a symlink to /tmp/sensitive between the check and the act. Then fix the program using O_NOFOLLOW and atomic open flags. Document why the fix is race-free.

  4. 04
    easy

    Apply the principle of least privilege to a simple web application. Take any small Flask or Express app with a database. Audit every database operation and create a minimal set of database roles with exactly the permissions each operation needs (no more). Write a migration that creates these roles. What operations can you eliminate entirely by restricting the application's DB role?

  5. 05
    hard

    Design a security policy (not a mechanism) for a multi-tenant SaaS application where users can share documents. Express the policy as precise logical rules (e.g., 'user U can read document D iff...') covering at least: ownership, explicit sharing, organization-level access, and admin access. Then identify two mechanisms that could enforce it and analyze how each could fail to enforce the policy correctly.

  6. 06
    medium

    Perform a quantitative risk analysis on three real vulnerabilities from the NVD (National Vulnerability Database, nvd.nist.gov). For each, estimate likelihood (using CVSS exploitability score as a proxy), impact (using CVSS impact score), and a rough mitigation cost. Rank them by risk score and justify which you would patch first for a small startup vs. a large bank.

Resources

📜The Protection of Information in Computer Systems
paperfree

Saltzer and Schroeder (1975) — the foundational paper. Eight design principles that still define the field.

📕Computer Security: Art and Science
bookpaidMatt Bishop, Addison-Wesley, 2nd ed. 2018

The most rigorous and comprehensive textbook on security foundations, including formal policy models.

📕Threat Modeling: Designing for Security
bookpaidAdam Shostack, Wiley, 2014

The definitive practical guide to STRIDE and threat modeling, by the Microsoft engineer who formalized the process.

🎓6.858 Computer Systems Security — MIT OpenCourseWare
coursefree

Full lecture notes, labs, and readings from MIT's graduate security course. Covers threat modeling, isolation, cryptography, web security, and more.

🎓CS 255: Introduction to Cryptography — Stanford
coursefree

Dan Boneh's Stanford course. Starts with security principles and definitions before cryptographic constructions.

📄OWASP Threat Modeling Cheat Sheet
docsfree

Practical reference for STRIDE application, DFD construction, and risk rating.

📕Secrets and Lies: Digital Security in a Networked World
bookpaidBruce Schneier, Wiley, 2000 (updated 2015)

Schneier's essential text on security economics, risk, and the human dimensions of security. Accessible to non-specialists.

📕Security Engineering (3rd edition) — free online
bookfree

Ross Anderson's comprehensive and freely available textbook. Part I covers security principles, psychology, and economics in depth.

Checkpoints — you can now…

  • ✓You can define Confidentiality, Integrity, and Availability precisely and give an example of a real attack that violates each property — including attacks that violate two properties simultaneously.
  • ✓You can draw a Data Flow Diagram for a simple web application, identify all trust boundaries, and apply each STRIDE category to at least three components.
  • ✓You can explain the Principle of Least Privilege and apply it concretely: reduce the privileges of a Unix process, a database user, or a cloud IAM role without breaking its legitimate function.
  • ✓You can design a defense-in-depth stack for a web application that would limit damage even if the outermost layer (WAF or firewall) were completely bypassed.
  • ✓You can distinguish between a security policy and a security mechanism, and identify at least two ways a mechanism might fail to enforce a correctly stated policy.
  • ✓You can identify and fix a TOCTOU race condition and explain why atomic operations are the correct solution.
  • ✓You can perform a basic quantitative risk analysis, rank vulnerabilities by expected loss, and justify which mitigations are cost-effective.