Hitchhiker's Guide — Hardening Web, API, Identity, and TLS
The operational companion to the Phase 02 WARMUP. The WARMUP builds the concepts (the request chain, authN vs authZ, OAuth/OIDC/JWT, the web attack classes, AEAD/KMS); this guide is the how — the attack lab, the table-driven negative authorization matrix, identity and TLS drills, chained-failure analysis, and the production proof that authorization fails closed. All targets are owned lab applications with synthetic data.
Table of Contents
- Reference Service Architecture
- Build an Attack Lab
- The Negative Authorization Matrix
- SSRF Hardening Procedure
- Identity Failure Drills
- TLS / mTLS Operations Lab
- Logging Standard
- Chained Failure Analysis
- Penetration-Test Report Standard
- Production Proof
- Worked Examples
- Interview Drills
- References
Reference Service Architecture
browser/client ─→ edge proxy ─→ application ─→ policy decision ─→ data service
│ │ │
TLS logs audit ID object filter (tenant, not caller-controlled)
Stand up: two tenants, human and workload identities, a local OIDC provider, a webhook sender, an outbound-fetch worker, and synthetic sensitive objects. The point is to test identity and policy propagation through hops (Phase 02 WARMUP, Chapter 10), not just individual route handlers — a correct route handler that loses the subject across a service hop is still a confused deputy.
Build an Attack Lab
Seed these abuse cases against the lab and capture request/decision/audit IDs for each:
- change an object ID to another tenant's (IDOR/BOLA);
- use a reader token on update/delete;
- replay an expired token; swap
issoraud; - send duplicate parameters and conflicting
Content-Types (parser differential); - ask the service indexer to read restricted content (confused deputy);
- upload a traversal / polyglot / oversized fixture;
- request metadata / a private IP through the outbound-fetch feature (SSRF).
The Negative Authorization Matrix
Authorization tests are table-driven and negative-heavy — most rows should be deny:
cases = [
# (principal, action, resource, expect_allow)
("reader", "read", "shared-doc", True),
("reader", "update", "shared-doc", False),
("admin-t1", "delete", "tenant2-doc", False), # cross-tenant
("expired-tok", "read", "shared-doc", False),
("workload-tok", "read", "user-route", False), # wrong audience/type
]
For every action, test: unauthenticated, wrong role, wrong tenant, disabled account, expired token, wrong issuer/audience/type, stale relationship, and service-to-service misuse. Include search, counts, exports, error paths, and timing — not just direct reads. Assert the policy decision and the audit record, not only the HTTP status (a 403 with the wrong audit reason is still a bug). A new endpoint absent from the matrix must fail CI.
SSRF Hardening Procedure
The outbound-fetch defense, in order (Phase 02 WARMUP, Chapter 11):
- canonicalize the URL once with a strict parser;
- restrict scheme/port (allowlist
https, specific ports); - resolve through a controlled DNS resolver and pin the result;
- reject loopback, link-local (
169.254.0.0/16), private (RFC1918), and metadata ranges — validate the resolved IP, not the hostname string; - route through an egress proxy with logging;
- re-validate on every redirect hop (defeats DNS rebinding and redirect-to-internal).
Prove it with a fixture whose DNS flips from public to 169.254.169.254 between check and connect.
Identity Failure Drills
- rotate the signing key while clients cache the old JWKS; prove overlap works and the old key is later rejected;
- revoke a user and measure every session/token/cache exposure window;
- replay an authorization response/code in a different browser session (state/PKCE must reject it);
- present a workload token to a user endpoint (audience/type mismatch must reject);
- remove a service relationship and prove cached policy expires;
- rotate a webhook secret with overlap, then reject the old version.
"Short-lived" is not a number until you've combined token, cache, queue, retry, and offline-processing lifetimes — the real exposure window is the max across all of them.
TLS / mTLS Operations Lab
Capture local handshakes and identify negotiation, certificate chain, key exchange, and the
encrypted records (openssl s_client -connect, tshark -O tls). Then test failure handling:
hostname mismatch, unknown CA, expiration, missing intermediate, absent client certificate (mTLS),
and rotation overlap. Operational rules:
- automate certificate issuance and rotation; design overlapping rotation and emergency revocation;
- separate workload identity from DNS routing;
- validate the trust domain and intended service, not just chain validity (mTLS authenticates the peer; the app still authorizes the action);
- monitor expiry and failed-handshake rates;
- never disable verification to troubleshoot — fail closed with useful telemetry instead.
Logging Standard
Record: subject ID, tenant, action, resource ID, policy version, decision, reason, request ID, and latency. Never log: bearer tokens, assertions, recovery URLs, passwords, private keys, or sensitive request bodies. Add canary tokens/secrets and assert via test that they never appear in any log sink.
Chained Failure Analysis
Real breaches chain individually-moderate flaws. Model the chain and break it at each link independently:
weak outbound-URL validation → metadata/workload credential → valid cloud identity
→ broad data permission → incomplete data-access logging
Independent controls: URL policy, egress restriction, credential delivery (no ambient creds), IAM least privilege, data-layer authorization, and detection. Label each control preventive / blast-radius reduction / detective / recovery so a reviewer sees the defense-in-depth, not a single point of failure.
Penetration-Test Report Standard
For each owned-lab finding: request preconditions, the exact security property violated, the minimum proof, affected routes/objects, root cause, the fix, a regression test, a detection, and residual risk. Avoid payload collections with no engineering analysis — a report is findings and fixes, not a list of strings.
Production Proof
- deployed route inventory matched to authorization tests (no untested route);
- key/session/secret rotation with measured exposure windows;
- egress tests including redirects and DNS changes;
- canaries proving tokens/secrets never enter logs;
- datastore queries proving the tenant filter is not caller-controlled;
- incident runbooks for signing-key, session-store, and webhook-secret compromise.
Worked Examples
JWT verification — the vulnerable vs the correct call
The single most common identity bug is decode() masquerading as verify(). Vulnerable:
import jwt
claims = jwt.decode(token, options={"verify_signature": False}) # DISASTER: trusts anything
user = claims["sub"]
An attacker simply forges a token with any sub. Now the correct verification (Phase 02 WARMUP,
Chapter 8) — note every pinned parameter:
claims = jwt.decode(
token,
key=jwks.get_key(unverified_header["kid"]), # resolve key by TRUSTED kid from a known JWKS
algorithms=["RS256"], # PIN the algorithm — reject 'none' & HS/RS confusion
issuer="https://idp.example.com/", # iss must be our trusted issuer
audience="https://api.example.com/orders", # aud — token must be minted FOR THIS service
leeway=30, # bounded clock skew for exp/nbf
)
# Then still authorize: claims['sub'] may touch THIS object? (object-level authZ, separate step)
The two classic forgeries this blocks: {"alg":"none"} (no signature) and algorithm confusion,
where an RS256 verifier is tricked into treating the public key as an HMAC secret by sending
{"alg":"HS256"}. Pinning algorithms=["RS256"] defeats both.
Reading a TLS handshake
openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3 </dev/null
What to look for, and what each proves:
Protocol : TLSv1.3 ← negotiated version (reject < 1.2)
Cipher : TLS_AES_256_GCM_SHA384 ← AEAD suite + forward-secret key exchange
Verify return code: 0 (ok) ← chain validated to a trusted root
subject=CN=api.example.com ← name must match the host you asked for (SNI/SAN)
issuer=CN=Trusted Root CA
A Verify return code: 21 (unable to verify the first certificate) means a missing intermediate
— a real production outage cause, not a reason to disable verification. The interview trap: "TLS is
green, so the request is authorized." No — TLS authenticated the server's name; the app still
authorizes the action (WARMUP Chapter 4).
An IDOR finding, end to end
# Authenticated as user A (tenant 1). Invoice 8123 belongs to tenant 2.
GET /api/invoices/8123 → 200 OK {"total": 4200, "tenant": 2} ← BUG: cross-tenant read
Root cause : route authenticates the user but trusts the path object ID without a tenant/
relationship check.
Fix : SELECT ... WHERE id = :id AND tenant_id = :caller_tenant (filter not caller-controlled)
Regression : test_reader_cannot_read_other_tenant_invoice() asserts 404 (not 403 — don't leak existence)
Detection : alert on object-access where resource.tenant != subject.tenant
Interview Drills
- "Walk me through OAuth authorization-code + PKCE on a whiteboard." Draw browser → authz server
(with
client_id, exactredirect_uri,state,code_challenge) → user authenticates → redirect back withcode+state→ app verifiesstate, exchangescodeon the back channel withcode_verifier. Then explain what each anti-forgery element prevents (state= login CSRF, PKCE = stolen-code reuse, exact redirect URI = code theft, nonce = ID-token replay). - "A user reads another tenant's record by changing a URL ID. Diagnose and fix." IDOR/BOLA: name the root cause (trusting a user-controlled object ID post-auth), fix with object-level authZ on the full tuple, return 404 not 403, add a per-route×tenant test, add a detection. (Worked above.)
- "How do you stop SSRF in a webhook fetcher?" Parse once → scheme/port allowlist → resolve via a controlled resolver and validate the resolved IP → re-validate every redirect (pin the address to beat DNS rebinding) → egress proxy → no ambient credentials. Validate the address you connect to, not the string.
- "XSS vs CSRF — different fixes, why?" CSRF abuses ambient cookie authority (the attacker causes
a request but can't read the response) →
SameSite+ anti-CSRF token + Origin check. XSS runs script inside your origin with full user authority → contextual output encoding + CSP.HttpOnlyblocks cookie theft, not the script making authenticated requests. - "Where else does object-level authZ have to apply besides direct GETs?" Batch, search, export, GraphQL resolvers, background jobs, cache hits, counts, and even error messages — every path that returns data.
References
- RFC 9700 (OAuth 2.0 Security BCP), RFC 7636 (PKCE), OpenID Connect Core; RFC 7519/7515 (JWT/JWS).
- RFC 8446 (TLS 1.3), RFC 5280 (X.509); RFC 6265bis (cookies /
SameSite). - OWASP ASVS; OWASP Cheat Sheets (Authorization, Session, CSRF, SSRF, Deserialization).
- PortSwigger Web Security Academy labs (OAuth, request smuggling, SSRF).
- NIST SP 800-63B (authentication); NIST SP 800-57 (key management).