MODULE 1

Threat Modeling

Structured analysis of who could attack, how, and what to protect.

STRIDE

Microsoft's 6-category threat taxonomy. Walk every component, asset, and trust boundary through each category.

LetterThreatDefense property
SSpoofing identityAuthentication
TTampering with dataIntegrity
RRepudiationNon-repudiation (signed audit log)
IInformation disclosureConfidentiality
DDenial of serviceAvailability
EElevation of privilegeAuthorization
STRIDE × component matrix

Data Flow Diagrams & Trust Boundaries

Draw external entities, processes, data stores, data flows. Trust boundaries cross where credentials change (browser→server, service→DB, public→private VPC). Every boundary crossing gets a STRIDE pass.

Simple web-app threat model (data flow + trust boundaries)

Attack Trees

Root = attacker's goal. Children = necessary conditions (AND/OR). Leaves = concrete attacks. Use to identify the cheapest path for the attacker and harden that first.

Attack tree: steal admin credentials
MODULE 2

Cryptography Essentials

What to use, when. Never roll your own.

Symmetric Encryption

Same key encrypts and decrypts. Use for data at rest and any large payload. Modern pick: AES-256-GCM or ChaCha20-Poly1305 — both are AEAD (authenticated encryption with associated data), so they encrypt AND integrity-check.

# Python: correct AES-GCM with random nonce
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aes = AESGCM(key)
nonce = os.urandom(12)  # 96-bit nonce — NEVER reuse with same key
ct = aes.encrypt(nonce, b"secret", associated_data=b"context-v1")
pt = aes.decrypt(nonce, ct, b"context-v1")

Asymmetric (Public-Key)

Different keys for encrypt/decrypt or sign/verify. Slow → use for key exchange and signatures, not bulk data.

  • RSA-2048+ — widely supported, slow. Avoid for new code.
  • ECDSA P-256 — fast, standard in TLS / JWT / code signing.
  • Ed25519 — fastest, simpler impl, deterministic signatures, preferred for new code.
  • X25519 — key agreement (Diffie-Hellman) counterpart to Ed25519.

Post-Quantum Cryptography (PQC)

Classical RSA/ECC fall to Shor's algorithm on a large quantum computer. NIST finalized the first PQC standards on 2024-08-13:

  • ML-KEM (FIPS 203) — module-lattice key-encapsulation (ex-Kyber). Replaces RSA/ECDH key exchange.
  • ML-DSA (FIPS 204) — module-lattice signatures (ex-Dilithium). General-purpose ECDSA/RSA-signature replacement.
  • SLH-DSA (FIPS 205) — stateless hash-based signatures (ex-SPHINCS+). Conservative fallback, security independent of lattice hardness.

Hash Functions

One-way, fixed-length digest. Use cases:

  • Integrity: SHA-256 / SHA-3-256 / BLAKE3 — any modern cryptographic hash.
  • Password storage: use a KDF, not a plain hash. See below.
  • Content addressing: BLAKE3 for speed; SHA-256 for ecosystem compat.

Key Derivation & Password Hashing

Passwords are low-entropy — a plain SHA-256 falls to GPU brute-force. Use a slow, memory-hard KDF.

  • Argon2id — modern winner of PHC. Default for new systems. Parameters: m=64 MB, t=3, p=1 is a safe baseline.
  • scrypt — memory-hard, well supported.
  • bcrypt — cost 12+. Capped at 72-char password (truncates longer).
  • PBKDF2-HMAC-SHA-256 — fallback if FIPS required. iter ≥ 600k (2023 OWASP).
# Python: Argon2id
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=64*1024, parallelism=1)
hashed = ph.hash("user-password")        # store this
ph.verify(hashed, "user-password")       # raises on mismatch
if ph.check_needs_rehash(hashed):        # after param bump
    hashed = ph.hash("user-password")

Slow-on-purpose: attacker with GPU farm sees O(1) hashes/sec; legitimate login path is fine.

Password verification flow (Argon2id)

HMAC & Signatures

HMAC = keyed-hash MAC. Use for integrity + authenticity when both sides share a symmetric secret (webhooks, cookies, JWT HS256). Never use plain hash(secret + data) — length-extension attacks.

Digital signatures = asymmetric HMAC. Signer holds private key, verifiers hold public. Ed25519 / ECDSA for new code.

# Webhook signature verification (Python)
import hmac, hashlib
def verify(body: bytes, header_sig: str, shared_secret: bytes) -> bool:
    expected = hmac.new(shared_secret, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_sig)  # constant-time!
MODULE 3

Authentication

Who is the caller? Passwords, MFA, tokens, federation.

Passwords Done Right

  • Never store plaintext. Never log passwords. Never include in error messages.
  • Store argon2id(password, salt, params). Salt is stored inside the hash string — library handles it.
  • Enforce NIST 800-63B minimums: ≥ 8 chars, no composition rules, reject known-breached (HIBP k-anonymity API).
  • Rate-limit per-IP and per-account. Lock or MFA-challenge after N fails.
  • Forgot-password: time-limited single-use token emailed; token hashed in DB.

Multi-Factor Auth

  • TOTP (RFC 6238) — 6-digit code from shared seed, 30-s window. Works offline. Prefer over SMS.
  • WebAuthn / FIDO2 — hardware or platform authenticators (Touch ID, Windows Hello, YubiKey). Phishing-resistant because origin-bound.
  • SMS — vulnerable to SIM swap. Only for recovery / low-risk, never primary for admin.

Sessions vs Stateless Tokens

Sessions: random opaque ID in cookie, server looks up state in Redis/DB. Pros: instant revoke. Cons: stateful, sticky needed.

Stateless JWT: signed claims. Pros: scale-out, no DB per request. Cons: hard to revoke before expiry.

Practical: short-lived access JWT (~15 min) + opaque refresh token stored server-side. Revoke refresh → access expires quickly.

JWT Pitfalls

  • alg: "none" — library MUST reject. Historical CVE.
  • Algorithm confusion — server expects RS256 but attacker sends HS256 signed with the server's public key treated as secret. Pin algorithm; don't trust header alg.
  • Weak HS256 secret — brute-forceable. Use 256-bit random or switch to RS256/Ed25519.
  • Long TTL — hours/days without refresh is bad. Access ≤ 15m.
  • Storing in localStorage — accessible to any XSS. Prefer HttpOnly cookie with SameSite=Lax.
  • No audience / issuer check — token from service A accepted by service B.
JWT structure
# Correct JWT verify (Python, PyJWT)
import jwt
payload = jwt.decode(
    token, public_key,
    algorithms=["RS256"],      # pin — not trust header
    audience="api.example.com",
    issuer="https://idp.example.com",
    options={"require": ["exp", "iat", "sub", "aud", "iss"]},
)

OAuth 2.0 + PKCE

OAuth delegates authorization. Auth-code + PKCE is the right flow for all public clients (SPA, native, mobile). Client credentials for machine-to-machine.

OAuth 2.0 Authorization Code + PKCE

OIDC & SAML

OIDC = OAuth + id_token (JWT with user claims). Modern federation default.

SAML 2.0 = XML-based SSO, still dominant in enterprise. More complex; beware XML signature wrapping attacks. Prefer OIDC for new deployments.

MODULE 4

Authorization

What is the caller allowed to do?

RBAC

Users → Roles → Permissions. Simple, widely supported, doesn't express resource-level rules well ("Alice can edit only her docs").

ABAC

Attribute-based. Policy = function of (subject attrs, resource attrs, action, environment). Expressive; needs a policy engine. Standards: XACML (complex), Rego (OPA), Cedar (AWS).

# OPA Rego — allow doc edit if owner OR admin
package docs
default allow = false
allow {
  input.action == "edit"
  input.resource.owner == input.subject.id
}
allow {
  input.subject.roles[_] == "admin"
}

ReBAC (Google Zanzibar-style)

Relationship-based — who has what relation to what. Scales to billions of tuples. Pattern: tuples (user, relation, object) with indirection via usersets ("user:alice is in group:eng"). Used by Google Drive, GitHub, Figma.

ReBAC check: can alice edit doc42?

Least Privilege

  • Default deny; explicit allow.
  • Scope tokens narrowly (per-service audience, per-action scope).
  • Rotate service credentials; prefer workload identity (AWS IRSA, GCP WIF) over long-lived keys.
  • Separate read vs write keys/roles.
MODULE 5

Web Vulnerabilities — OWASP Top 10 Defense

Concepts + correct defenses. Defense-only (no exploit recipes).

A05:2025 / A03:2021 Injection — SQL / NoSQL / Command

Cause: concatenating untrusted input into a query. Defense: parameterized statements. Never build a query via string formatting.

# Python — parameterized (safe)
cur.execute("SELECT * FROM users WHERE email = %s AND active = %s", (email, True))

# NEVER this:
# cur.execute(f"SELECT * FROM users WHERE email = '{email}'")

For NoSQL (MongoDB), reject operator objects in user input: validate shape with a schema (pydantic, joi). For shell commands, use subprocess.run([...]) with a list, never shell=True.

A05:2025 / A03:2021 XSS — Cross-Site Scripting (under Injection)

  • Reflected — input echoed in response. Defense: context-aware output encoding.
  • Stored — input saved then rendered for others. Same defense, at render time.
  • DOM-based — client JS writes untrusted data to sink (innerHTML, document.write).

Defense stack (all layers):

  • HTML-encode on output. Use framework auto-escape (React's JSX, Jinja2 autoescape, Handlebars).
  • Never inject untrusted data into a script, style, onclick, or href=javascript:. If needed, use Trusted Types (MDN).
  • CSP (Content-Security-Policy) header with default-src 'self'; script-src 'nonce-xxx' 'strict-dynamic'. Blocks inline <script> and eval.
  • HttpOnly + Secure + SameSite=Lax on session cookies → stolen DOM can't reach them.

A01:2025 / A01:2021 CSRF — Cross-Site Request Forgery (Broken Access Control)

Attacker's site triggers a state-changing request to yours using the victim's cookies. Defense:

  • SameSite=Lax cookies (default in modern browsers) block most cross-site POSTs.
  • CSRF token: synchronizer-token pattern — server issues a random token, client echoes in a custom header; server compares constant-time.
  • Check Origin / Referer header as a second layer.
  • For pure-JSON APIs using Bearer tokens (no cookies), CSRF is not an issue.

A01:2025 / A10:2021 SSRF — Server-Side Request Forgery (folded into Broken Access Control)

User input becomes a URL the server fetches. Attacker targets 169.254.169.254 (cloud metadata) or internal services. Defense:

  • Allow-list of host/port/scheme.
  • Resolve DNS yourself, reject private-range IPs (RFC1918 + link-local + loopback). Beware TOCTOU — re-resolve and pin.
  • Disable redirects OR re-check each hop.
  • Prefer a dedicated egress proxy with metadata-service blocked.
  • On AWS: enforce IMDSv2 (session-token required) so a blind GET can't read creds.

A01:2025 / A01:2021 IDOR — Insecure Direct Object Reference

Endpoint trusts an object ID from the request without authz check. Defense: every object fetch must check the caller owns/has-access to the object. Don't rely on "UUID is random enough" — that's obscurity, not security.

# FastAPI: correct ownership check
@app.get("/doc/{doc_id}")
def get_doc(doc_id: str, user = Depends(current_user)):
    doc = db.get(doc_id)
    if doc is None or not can_read(user, doc):
        raise HTTPException(404)  # don't leak existence
    return doc

A08:2025 / A08:2021 Insecure Deserialization (Software/Data Integrity Failures)

pickle.loads, yaml.load, Java ObjectInputStream on untrusted input = arbitrary code exec. Use JSON with a strict schema (pydantic, jsonschema). If you must use pickle, sign payloads with HMAC and verify before loading.

Other OWASP Hits

  • A04:2025 / A02:2021 Crypto Failures — weak TLS config, hard-coded keys. Use Mozilla SSL Config Generator.
  • A06:2025 / A04:2021 Insecure Design — threat-model at design time (mod1).
  • A02:2025 / A05:2021 Misconfig — defaults (default creds, open S3 bucket, open CORS). Scan with checkov / tfsec.
  • A03:2025 / A06:2021 Software Supply Chain Failures — expanded from "Vulnerable & Outdated Components" to cover dependencies, build systems, and distribution. Dep scanning: npm audit, pip-audit, Snyk, Dependabot.
  • A07:2025 / A07:2021 Auth Failures — see mod3.
  • A09:2025 / A09:2021 Security Logging & Alerting Failures — structured audit log; can you reconstruct who did what to whom, yesterday?
  • A10:2025 Mishandling of Exceptional Conditions — new in 2025: improper error handling, failing open, logic errors. Fail closed; don't leak internals in errors.
MODULE 6

Transport Security

TLS 1.3, mTLS, HSTS, cert pinning.

TLS 1.3

1-RTT handshake (0-RTT with PSK). Only AEAD ciphers. Drops RSA key exchange, SHA-1, CBC. See networking mod6 for the handshake timeline.

  • Pin cipher suites: TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256.
  • ECDHE for forward secrecy (baked-in in 1.3).
  • Post-quantum: the hybrid X25519MLKEM768 named group (X25519 + ML-KEM-768) is now the de-facto default key exchange — enabled by default in Chrome and Firefox and across Cloudflare's edge.
  • Disable old protocols: SSLv2/3, TLS 1.0, TLS 1.1.

mTLS

Both sides present certs. Used service-to-service in mesh (Istio, Linkerd). Identity = SPIFFE/SPIRE IDs embedded in cert SAN.

HSTS + HTTPS Enforcement

# Strict-Transport-Security header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Tells browser: only HTTPS for this host for 1 year. Submit to hstspreload.org to bake into browser source — no first-request window.

Cert Pinning & CT

Pinning = app hard-codes accepted cert or public-key hash. Kills MITM via rogue CA but brittle on rotation. Mobile apps common, web is deprecated (HPKP dead).

Certificate Transparency = public append-only log of issued certs. Monitor for unauthorized certs on your domain (crt.sh, Cert Spotter).

TLS certificate chain
MODULE 7

Secrets Management

Keep secrets out of code, config, and logs.

Never Commit Secrets

  • Pre-commit hook: gitleaks / detect-secrets.
  • Rotate anything ever committed — even if rolled back, it's in history.
  • .env files: gitignored, but still unencrypted on disk. OK for dev, NOT prod.

Secret Managers

  • HashiCorp Vault — dynamic secrets, leases, policies.
  • AWS Secrets Manager / Parameter Store, GCP Secret Manager, Azure Key Vault.
  • Kubernetes: external-secrets operator syncs from a manager into K8s Secret.

Envelope Encryption

Encrypt data with a DEK (data encryption key). Encrypt the DEK with a KEK held by KMS. Store {ciphertext, wrapped_DEK}. Rotation: re-wrap DEK by KEK rotation — don't re-encrypt data.

Envelope encryption

Rotation

  • All credentials have a max age. Short-lived tokens via OIDC federation (AWS IRSA, GCP WIF) > long-lived access keys.
  • Design for overlap: new secret valid before old retired.
  • Alert on un-rotated secrets past threshold.
MODULE 8

Secure Coding Patterns

Defaults that prevent whole classes of bugs.

Validate Input, Encode Output

Validation is allow-list (positive pattern). Encoding is context-aware: HTML body ≠ HTML attr ≠ URL ≠ JSON ≠ script. Use the framework's escaper for the target context.

Rate Limiting

Token bucket per (IP, account, endpoint). Prevents brute-force + abuse. See system-design mod9 for token-bucket viz.

Dependency Scanning

  • SCA (Software Composition Analysis) — match lockfiles against CVE DB. Snyk, GitHub Dependabot, npm audit, pip-audit, OSV-scanner.
  • SAST — static analysis on your source. Semgrep, CodeQL.
  • DAST — probe the running app. ZAP, Burp.
  • SBOM — Software Bill of Materials in SPDX/CycloneDX format.

Supply-Chain Integrity

SCA/SAST/SBOM tell you what is in your build; supply-chain integrity proves the artifact you run is the one your build actually produced — untampered, from a trusted source. OWASP Top 10:2025 promotes this to its own category (A03:2025 Software Supply Chain Failures), and the xz-utils backdoor (CVE-2024-3094) is the canonical cautionary tale.

SLSA — provenance levels

SLSA (Supply-chain Levels for Software Artifacts) v1.0 defines Build levels that attest how an artifact was built:

  • Build L1 — provenance exists (build platform, process, top-level inputs). May be unsigned: catches mistakes, not tampering.
  • Build L2 — built on a hosted platform that emits signed provenance, so consumers can authenticate its origin.
  • Build L3 — hardened, isolated builds; signing key is unreachable from user-defined build steps, so a malicious build job can't forge provenance.

Signing & attestation (sigstore / cosign)

cosign signs container images and arbitrary artifacts. Keyless signing avoids long-lived keys: it exchanges a CI OIDC token for a short-lived cert from Fulcio, signs, and records the signature + cert in Rekor, an append-only transparency log. Verifiers then bind the signature to a specific identity (e.g. the GitHub Actions workflow that built it).

# Verify an image is signed by the expected CI identity (cosign keyless)
cosign verify \
  --certificate-identity-regexp '^https://github.com/acme/.+/.github/workflows/.+@refs/tags/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/acme/app:1.4.2
# Verify a signed SBOM attestation attached to the image
cosign verify-attestation --type spdxjson \
  --certificate-identity-regexp '^https://github.com/acme/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/acme/app:1.4.2

Pin everything: lockfiles + hashes

A signature only helps if you fetch the exact bytes it covers. Pin transitive deps to content hashes, not version ranges — then a swapped upstream artifact fails the install.

# requirements.txt with hash pinning (pip refuses any mismatch)
requests==2.32.3 \
  --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# install: every package MUST carry a verified hash
pip install --require-hashes -r requirements.txt

Safe Deserialization

JSON only, with a schema. Never pickle.loads or yaml.load on untrusted data. YAML: use yaml.safe_load.

Logging Hygiene

  • Never log: passwords, tokens, full PAN, full SSN, keys, session cookies.
  • Mask or hash sensitive fields. Include request ID for trace correlation.
  • Structured JSON logs; centralize in SIEM.
MODULE 9

Network & Zero Trust

Don't trust the network — authenticate every request.

Beyond Perimeter

Legacy model: hard shell (firewall) around soft interior. Breaks once attacker is inside. Google's BeyondCorp model: every request authenticated + authorized regardless of origin network.

Zero Trust Primitives

  • Identity-aware proxy (IAP) between client and app. Terminates TLS, authenticates user via OIDC, forwards identity headers to backend.
  • Service mesh mTLS (Istio, Linkerd) — every intra-cluster call authenticated + encrypted by the data-plane proxy.
  • Workload identity — SPIFFE SVIDs for services; no shared secrets.
  • Egress control — explicit allow-list of external domains a workload may reach.
BeyondCorp-style request flow

Network Segmentation

Even in zero-trust, minimize blast radius: separate VPCs per environment (prod/stage/dev), private subnets for data stores, PrivateLink for cross-account. Public subnet only for LB/NAT.

MODULE 10

Interview Cheat Sheet

Questions you WILL be asked.

"How does HTTPS work?"

Client hello → server hello + cert chain → client verifies chain against trust store → ECDHE key agreement → derive session keys → AEAD cipher (TLS 1.3 = 1-RTT). See networking mod6.

"Design an auth system."

Identify users, workloads, and federation. Argon2id for passwords + WebAuthn for MFA. Short-lived access JWT (RS256, 15 min) + opaque refresh token stored server-side. OAuth PKCE for public clients. Session revocation via refresh-token blacklist or JWT + short TTL.

"How to store passwords?"

Argon2id(m=64MB, t=3, p=1) — library stores salt inside hash. Never plain hash. Rate-limit login. Check against HIBP on set.

"What's in a JWT? Risks?"

Header (alg, kid) + payload (iss, sub, aud, exp, iat, custom claims) + signature, base64url-joined. Risks: alg=none, algorithm confusion, weak HS256 secret, long TTL, storing in localStorage.

"Prevent SQL injection, XSS, CSRF, SSRF."

Parameterized queries · context-aware output encoding + CSP · SameSite cookies + CSRF token · allow-list + re-resolve DNS + block private IPs + IMDSv2.

"Secret management for a service on K8s?"

External-secrets operator → fetches from Vault/Secrets Manager → K8s Secret → projected as env/volume. Pod uses IRSA/WIF for manager auth — no shared keys. Rotate automatically.

"How do you design a rate limiter?"

Token bucket per (key). Redis with INCR + EXPIRE, or Envoy local rate limit + global. Burst = bucket size; steady = refill rate. See system-design mod9.

"Encrypt data at rest — envelope encryption why?"

Symmetric DEK fast for bulk; KEK in KMS never leaves the boundary. Rotation = re-wrap DEK, no data re-encryption. Cheap audit: KMS logs every unwrap.

MODULE 11

API Security, Cloud/K8s Hardening & Detection/Response

The OWASP API Top 10, cloud/container hardening, and the blue-team side.

OWASP API Top 10 & BOLA: the #1 API killer

Web app security assumes a browser + server-rendered pages; APIs invert that — the client is untrusted code you don't control, and every object is addressable by ID. The OWASP API Security Top 10 (2023) exists because classic WAFs and the web Top 10 miss API-specific flaws. The worst by far is BOLA (Broken Object-Level Authorization, API1): the endpoint authenticates who you are but never checks whether this user may touch this object.

  • BOLA (API1)GET /api/orders/1043 works; change to 1044 and you read another tenant's order. AuthN passed, object-level AuthZ missing.
  • Broken Authentication (API2) — weak JWT validation (alg:none, no signature check), credential stuffing, no lockout.
  • BOPLA (API3) — Broken Object Property-Level Auth = mass assignment (write extra fields) + excessive data exposure (read extra fields).
  • BFLA (API5) — Broken Function-Level Auth: a regular user calls an admin-only route because access is enforced only in the UI.
  • Unrestricted Resource Consumption (API4) — no rate/size/cost limits → DoS and cloud bill blowups.

The fix for BOLA is never trust an ID from the client as proof of ownership. Re-derive authorization from the session on every request, scoped by tenant:

# VULNERABLE: any authenticated user can read any order
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user=Depends(auth)):
    return db.query(Order).get(order_id)          # no ownership check!

# SAFE: object lookup is *scoped* to the caller, 404 (not 403) to avoid enumeration
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user=Depends(auth)):
    order = db.query(Order).filter(
        Order.id == order_id,
        Order.tenant_id == user.tenant_id,        # authz baked into the query
    ).first()
    if order is None:
        raise HTTPException(404)                  # don't leak existence
    return order

Mass assignment & broken function-level authorization

Mass assignment (part of API3/BOPLA) happens when a framework auto-binds the whole request body onto a model. The client sends fields the developer never intended to be writable — role, is_admin, balance, verified — and the ORM dutifully saves them. BFLA (API5) is its function-level cousin: guessing or reading the admin route (POST /api/admin/users) and calling it directly because authorization lives only in the frontend menu.

FlawAttacker actionRoot causeDefense
Mass assignmentAdd "role":"admin" to signup JSONBind-all model / no field allow-listExplicit input schema (DTO) with allow-listed fields
Excessive exposureRead password_hash, ssn in responseSerialize entire ORM objectExplicit output schema; never return model.__dict__
BFLACall DELETE /admin/users/5 as normal userUI-only gating; verb/path not checked server-sideServer-side role check per route; deny-by-default
# VULNERABLE: Pydantic/ORM binds everything the client sends
user = User(**request.json())     # {"email":..,"password":..,"is_admin":true}

# SAFE: separate INPUT schema (allow-list) from the DB model
class SignupIn(BaseModel):
    email: EmailStr
    password: SecretStr           # is_admin / role simply do not exist here
user = User(email=data.email, password_hash=hash(data.password), role="user")

# BFLA defense: deny-by-default decorator on every admin route
@app.delete("/api/admin/users/{uid}")
@require_role("admin")            # enforced in the API, not the SPA
def delete_user(uid: int): ...

GraphQL abuse & rate-limit bypass

GraphQL replaces many fixed REST endpoints with one flexible query engine — which moves the attack surface into query shape. A single request can demand deeply nested, cyclic, or fan-out data that costs the server orders of magnitude more than it costs the client to send. REST rate limits (requests/minute) are meaningless when one request does the damage of ten thousand.

  • Deep/nested query DoS — cyclic relations (author → posts → author → posts…) explode work exponentially.
  • Introspection leakage__schema hands attackers your full type graph, including hidden mutations. Disable in prod.
  • Batching / aliasing bypass — 1 HTTP request with 1000 aliased mutations bypasses per-request rate limits (a favorite for brute-forcing OTP/login).
  • Field-level authz gaps — resolvers that trust the parent object skip per-field checks (GraphQL BOLA).
# Alias-based rate-limit bypass: 1 request = 1000 login attempts
mutation {
  a1: login(user:"admin", pass:"aaaa") { token }
  a2: login(user:"admin", pass:"aaab") { token }
  # ... a1000: login(...)   one HTTP POST, one "request" to a naive limiter
}

Defenses stack: cap query depth and complexity (cost analysis), limit aliases/batch size, disable introspection, and rate-limit by cost not by request count.

// graphql-armor / envelop style guards
import { costLimit, maxDepth, maxAliases } from '@escape.tech/graphql-armor';
const plugins = [
  maxDepth({ n: 7 }),                 // reject queries deeper than 7 levels
  maxAliases({ n: 15 }),              // kill the 1000-alias brute force
  costLimit({ maxCost: 5000 }),       // assign per-field cost, budget the query
];
// prod schema: introspection disabled
const server = new ApolloServer({ schema, introspection: false });

Least-privilege IAM policy design

Cloud breaches are overwhelmingly IAM misconfigurations, not zero-days. The goal is least privilege: grant the minimum actions, on the minimum resources, under the minimum conditions. AWS evaluates policies as explicit deny > explicit allow > implicit deny — default is deny, one Deny overrides any Allow. The classic anti-pattern is "Action":"*","Resource":"*" attached to an app role “to make it work.”

// BAD: god-mode role — a compromised pod owns the account
{ "Effect": "Allow", "Action": "*", "Resource": "*" }

// GOOD: scoped to specific actions, a specific bucket prefix, with guardrails
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "ReadTenantUploads",
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:ListBucket"],
    "Resource": [
      "arn:aws:s3:::acme-uploads",
      "arn:aws:s3:::acme-uploads/${aws:PrincipalTag/tenant}/*"
    ],
    "Condition": {
      "Bool": { "aws:SecureTransport": "true" },
      "StringEquals": { "aws:PrincipalOrgID": "o-1a2b3c" }
    }
  }]
}
  • Scope Resource, not just Actions3:GetObject on * reads every bucket; pin the ARN and prefix.
  • Conditions as guardrailsaws:SourceIp, aws:PrincipalOrgID, aws:MultiFactorAuthPresent, VPC endpoint conditions.
  • Permission boundaries + SCPs — cap the maximum any role can be granted, so a developer can't self-escalate to admin.
  • No long-lived keys — use IAM Roles / IRSA (IAM Roles for Service Accounts) / Workload Identity; rotate and expire via STS.
  • Right-size from evidence — start deny-all, add actions from CloudTrail “Access Analyzer” last-accessed data, not from guesses.

Kubernetes hardening: RBAC + Pod Security + NetworkPolicy

A default K8s cluster is flat and permissive: pods can talk to every other pod, service accounts often have broad rights, and containers can run as root. Hardening is three orthogonal layers — who can call the API (RBAC), what a pod is allowed to be (Pod Security), and who a pod can talk to (NetworkPolicy).

ControlThreat it stopsDefault (unsafe)Hardened
RBACCompromised SA reads secrets / creates podsBroad roles, cluster-admin bindingsNamespaced Role, verbs+resources allow-listed
Pod Security Admissionroot, privileged, hostPath escapeNo enforcementrestricted profile enforced per namespace
NetworkPolicyLateral movement pod→podAll pods can reach all podsDefault-deny + explicit allows
# 1) RBAC: least-privilege Role — read pods in ONE namespace, nothing else
kind: Role
metadata: { namespace: payments, name: pod-reader }
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]      # no create/delete, no secrets
---
# 2) Pod Security Admission: enforce the 'restricted' profile
apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted   # blocks root/privileged
    pod-security.kubernetes.io/enforce-version: latest
---
# 3) NetworkPolicy: default-deny all ingress in the namespace
kind: NetworkPolicy
metadata: { namespace: payments, name: default-deny-ingress }
spec:
  podSelector: {}                      # selects every pod
  policyTypes: ["Ingress"]             # with no ingress rules => deny all
# Hardened pod spec matching the 'restricted' profile
securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities: { drop: ["ALL"] }
  seccompProfile: { type: RuntimeDefault }
automountServiceAccountToken: false    # don't hand every pod an API token

SSRF to the metadata endpoint & robust SSRF defense

SSRF (Server-Side Request Forgery) tricks your server into making requests to attacker-chosen URLs. In the cloud the crown jewel is the instance metadata service (IMDS) at 169.254.169.254 — it hands out temporary IAM credentials to anything on the box. The 2019 Capital One breach was exactly this: SSRF → IMDS → role creds → 100M records from S3.

# The attack: app fetches a user-supplied "image URL"
POST /fetch  body: url=http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role
# Response: temporary AccessKeyId / SecretAccessKey / Token  -> full S3 access
  • Enforce IMDSv2 — session-token + PUT required, and set hop-limit to 1 so a container/proxy can't reach it. Kills most SSRF-to-IMDS.
  • Allow-list, never deny-list — validate the resolved destination against an explicit allowed set; blocking “169.254.*” misses 0177.0.0.1, [::ffff:169.254.169.254], DNS names, and redirects.
  • Block private ranges — reject RFC1918 (10/8, 172.16/12, 192.168/16), loopback, link-local 169.254/16, and IPv6 equivalents after resolution.
  • Disable redirects or re-validate each hop; a 302 to 169.254.169.254 defeats front-door checks.

DNS rebinding defeats naive validation: the attacker's domain resolves to a safe public IP when you validate, then re-resolves to 169.254.169.254 milliseconds later when you connect (TOCTOU on DNS). The fix is resolve once, pin, validate the pinned IP, then connect to that exact IP.

import socket, ipaddress
BLOCKED = [ipaddress.ip_network(n) for n in
           ("10.0.0.0/8","172.16.0.0/12","192.168.0.0/16",
            "127.0.0.0/8","169.254.0.0/16","::1/128","fc00::/7")]

def safe_resolve(host: str) -> str:
    # resolve ONCE, validate, then connect to THIS ip (no second lookup)
    ip = socket.getaddrinfo(host, None)[0][4][0]
    addr = ipaddress.ip_address(ip)
    if any(addr in net for net in BLOCKED):
        raise ValueError(f"blocked target {ip}")   # stops rebinding + IMDS
    return ip   # caller connects to pinned ip, Host header = original host

Detection & response: SIEM, WAF/DDoS, and the IR lifecycle

Prevention fails eventually — detection and response bound the damage. A SIEM (Security Information & Event Management: Splunk, Elastic SIEM, Sentinel) ingests logs from every layer, normalizes them, correlates across sources, and fires alerts. The metric that matters is MTTD/MTTR (mean time to detect/respond): the industry median dwell time was historically 200+ days — good detection cuts that to hours.

Logging pipeline:
[apps/hosts/cloud/k8s/waf] --agents/Fluent Bit--> [Kafka/queue]
     --> [normalize + enrich (geoIP, threat intel)] --> [SIEM index]
         --> detection rules / ML anomaly --> [alert -> on-call + SOAR playbook]
  • WAF — filters L7: SQLi/XSS signatures, OWASP CRS rules, per-route rate limits, bot scoring. Necessary but bypassable — defense-in-depth, not a substitute for fixing code.
  • DDoS defense — anycast + scrubbing (Cloudflare, AWS Shield); L3/4 volumetric absorbed at the edge, L7 needs rate-limit + challenge (JS/CAPTCHA).
  • Anomaly / UEBA — baseline normal (login geo/time, request volume, data egress) and alert on deviation: impossible-travel logins, a service account suddenly listing all S3 objects.
  • Log integrity — ship logs off-box to append-only/WORM storage so an attacker can't erase their trail; monitor for gaps.

The incident response lifecycle (NIST SP 800-61) is the muscle memory: Preparation → Detection & Analysis → Containment → Eradication → Recovery → Post-incident (lessons learned). Containment buys time (isolate the host, rotate the leaked credentials); eradication removes the foothold; recovery restores from known-good; the blameless post-mortem prevents recurrence.

Defending TOCTOU races & memory-safety bugs

Two low-level bug classes underpin many escalations. TOCTOU (Time-Of-Check to Time-Of-Use) is a race: you validate a resource, then use it, and an attacker swaps it in the gap. Memory-safety bugs (buffer overflow, use-after-free, integer overflow) let attackers corrupt state or execute code — still ~70% of severe CVEs in C/C++ codebases (Microsoft/Chromium data).

/* VULNERABLE TOCTOU: check then use, attacker swaps path for a symlink between */
if (access("/tmp/job", W_OK) == 0) {   /* TIME OF CHECK  */
    fd = open("/tmp/job", O_WRONLY);   /* TIME OF USE -> now points at /etc/passwd */
    write(fd, data, len);
}
/* SAFE: operate on the handle atomically, no re-resolution of the name */
fd = open("/tmp/job", O_WRONLY | O_NOFOLLOW | O_CREAT | O_EXCL, 0600);
if (fd >= 0) { /* opened THIS file exclusively; no window to swap */ }
  • Beat TOCTOU — collapse check+use into one atomic operation: open the fd then fstat the fd (not the path), use O_NOFOLLOW/O_EXCL, openat with dir handles; for DB/state use transactions with row locks or compare-and-swap, not read-then-write.
  • Memory safety by construction — prefer memory-safe languages (Rust, Go, Java) for new code; the borrow checker eliminates use-after-free/data races at compile time.
  • Harden legacy C/C++ — ASan/UBSan in CI, fuzzing (libFuzzer/AFL++), -D_FORTIFY_SOURCE=3, stack canaries, ASLR, DEP/NX, bounds-checked containers (std::span, .at()), and never strcpy/gets.
  • Integer overflow — validate sizes before malloc(n * size); use checked arithmetic (__builtin_mul_overflow, Rust checked_mul).