Advanced System Design Tutorial 0/70 lessons ~6 min read Lesson 63

    Architecture Security and Threat Modeling

    TechLearningPro opened a public partner API so enterprise learning systems could enroll their employees and pay for seats. The design review talked about OAuth, a gateway, and "PCI

    Course progress0%
    Focus
    15 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Architecture Security and Threat Modeling

    Advanced System Design & Software Architecture · Lesson 63 · Architecture Practice, Interviews and Capstone

    Lesson Overview

    TechLearningPro opened a public partner API so enterprise learning systems could enroll their employees and pay for seats. The design review talked about OAuth, a gateway, and "PCI compliant" in the same breath as the slide title. Nobody had drawn where a partner credential was allowed to travel, which component was allowed to decide that this employee belonged to that tenant, or what happened to a card number if a well-meaning log line included the request body. The first abuse case arrived from a customer's security team, not from a criminal: "Your integration token, if copied out of our CI log, can list every learner in our tenant. Show us the model that says otherwise."

    Threat modeling is how you answer that letter before it is sent. It is a structured review of a data-flow diagram: assets, trust boundaries, and the ways a principal can violate a property you meant to keep. STRIDE is the security lens. LINDDUN is the privacy lens, and it does not always agree with STRIDE, because a perfect audit trail and a minimum of personal data pull in opposite directions. The output is not a claim that the system is secure. The output is a list of threats, the control that addresses each one, and the residual risk a named person accepts.

    Microservices Security and Zero Trust already specifies the runtime controls this course trusts: user tokens distinct from workload identity, audience-checked JWTs, mutual TLS or an equivalent authenticated channel, network policy, and database roles that match the schema. This lesson does not re-implement those controls. It decides where they must sit for a public API that enrolls learners and takes payment, and which threats remain after they sit there. Design a Payment Processing System keeps cardholder data at the acquirer. The threat model is how you stop a later feature from dragging it back.

    You will produce a model a staff review can reject: a diagram with boundaries, a STRIDE pass, a LINDDUN pass on the personal data, an abuse case, and a residual a human signed.

    Learning Objectives

    By the end of this lesson you will be able to:

    1. Build a data-flow diagram that marks trust boundaries, data stores, and external principals for an architecture under review.
    2. List assets and security properties in language a control can honor: confidentiality, integrity, availability, and accountability.
    3. Apply STRIDE to interactions across a boundary, and state the control that belongs on the data owner rather than only at the gateway.
    4. Apply LINDDUN to personal data and explain the conflict between security non-repudiation and privacy.
    5. Write abuse cases that name a principal, a goal, and a violated invariant, including confused-deputy and cross-tenant access.
    6. Place secrets so application code never becomes the long-term store, and so logs and images are not accidental copies.
    7. Separate authentication from authorization, and require the service that owns the row to enforce the tenant and object check.
    8. Record residual risk with an owner and a revisit trigger, and refuse a review that ends with "no issues found."

    Prerequisites

    Detailed Core Concepts

    What a threat model is for

    A threat model is an argument of the form: these are the assets, these are the principals, this is how data moves, these are the ways a property fails, these are the controls, this is what we still accept. It is produced early enough that a control can change the shape, which is why it belongs in an architecture review and not only in a pre-release scan. A scanner can see a dependency version. It cannot see that the enrollment service trusts a tenant id supplied by the caller.

    Adam Shostack's four-question sketch is the right size for a design review. What are we working on? What can go wrong? What are we going to do about it? Did we do a good job? The first question is a diagram, not a slogan. The last question is a residual-risk list with names, not a feeling that the meeting was thorough.

    The model is also a scope boundary. "Threat-model the company" produces a poster. "Threat-model partner enrollment and seat purchase for one tenant, from the partner's CI system through capture" produces decisions. Out of scope is written down: physical office security, the acquirer's internal network, the learner's personal device malware beyond what a token lifetime can limit. Out of scope is not "unimportant." It is "not this review."

    Assets and properties

    An asset is something an attacker, or a mistake, can harm. For this API the assets are concrete:

    • Partner and learner credentials, and the signing keys that make tokens believable.
    • The tenant boundary: one customer's roster is not another's.
    • Enrollment and entitlement rows, because they are how a learner gets a course they did not pay for, or loses one they did.
    • Money: a capture must match an authorized amount and must not double.
    • Personal data: name, email, learning progress. This is the LINDDUN list more than the STRIDE list.
    • Availability of sign-in and capture, which Non-Functional Requirements and SLO Design measures and which denial-of-service threats spend.

    Properties come from the classic security trio plus accountability. Confidentiality: a principal learns a fact only when a rule allows it. Integrity: a fact changes only by an authorized writer, and the change is the one they intended. Availability: an authorized principal can complete the operation inside the SLO. Accountability: you can answer who did a sensitive thing, after the fact, without having logged the secret itself.

    Write assets as nouns you can point at in a store or a message. "Customer trust" is a business outcome. The roster table is an asset.

    Trust boundaries

    A trust boundary is a place where the set of things you believe changes. Crossing it requires identification and a check. Useful boundaries for TechLearningPro:

    • The public internet and the gateway. TLS ends here for browsers and partners. The gateway sees unauthenticated bytes.
    • The gateway and the enrollment service. The gateway has validated a token's signature and audience. It has not proved that this partner may enroll that employee. That proof uses data the gateway does not own.
    • Enrollment and the payment service. A workload identity may call capture. The learner's token is not forwarded blindly, which is the confused-deputy lesson from lesson 44.
    • Each service and its database. The network path is not the grant. The database role is.
    • A tenant and another tenant, even though both rows live in one table. This boundary is logical and is the one reviews forget, because no network line is drawn.
    • CI and production. A pipeline identity that can deploy cannot also read every tenant's roster with a long-lived partner token someone pasted into a log.
    • Your systems and the acquirer. You send a tokenized payment instrument or a client-side confirmation. You do not send a raw card number through enrollment logs.

    Inside a boundary, you still authenticate calls if the boundary is a Kubernetes namespace full of unrelated pods. Lesson 44's assumption holds: network location is not a principal. Threat modeling draws the boundary anyway, because it tells you which controls are redundant and which are the only check you have.

    STRIDE, used on interactions

    STRIDE is a Microsoft mnemonic for threat classes, not a ranking of severity.

    ThreatProperty harmedArchitectural shape
    SpoofingAuthenticationCaller pretends to be a partner, a learner, or a service
    TamperingIntegrityBody, token, price, or return URL is altered in flight or at rest
    RepudiationAccountabilityA refund or an enrollment cannot be tied to a principal
    Information disclosureConfidentialityRoster, token, or email is read by a principal who should not see it
    Denial of serviceAvailabilityA public route consumes the capture budget or the pool
    Elevation of privilegeAuthorizationA learner token performs a partner-admin operation, or tenant A reads tenant B

    Two ways to apply it are common. STRIDE-per-element walks each process, store, and flow and asks all six questions. It is thorough and it repeats itself. STRIDE-per-interaction asks the six questions where a flow crosses a boundary, which is where a check can actually sit. This lesson uses per-interaction for the walkthrough and a short per-element pass on the stores, because stores are where disclosure lives after the request is gone.

    STRIDE will not invent your business rules. "A partner may enroll an employee who has not accepted the invite" is an abuse of a product rule. The mnemonic will only surface it if you remember elevation and the asset. Bring the invariants from the domain into the review, or the model will be a generic API checklist with your logo on it.

    LINDDUN, and the argument with STRIDE

    LINDDUN is a privacy threat taxonomy developed at KU Leuven. The categories:

    • Linkability. Two records can be tied to the same person across contexts they did not expect to join. A payment customer id and a learning-progress id in one analytics export.
    • Identifiability. A person can be picked out. A "anonymous" progress event that includes a rare course combination and a company name.
    • Non-repudiation. A person cannot deny an action. This is a privacy threat when the product promised not to keep that proof, and a security requirement when finance must prove who approved a purchase. The same word, opposite goals. Write which one you are honoring on each flow.
    • Detectability. An observer learns that an action happened even without the content. A partner can see that a named employee opened a remedial course, from a timing or a status API, when the contract said only completions are visible.
    • Disclosure of information. The content leaks. Overlaps STRIDE's information disclosure. LINDDUN pushes you to ask whether you needed the content at all.
    • Unawareness. The person does not know collection or a choice exists. A default checkbox the partner sets on behalf of every employee.
    • Non-compliance. The design cannot meet the promise already made in a privacy notice, a data-processing agreement, or a retention rule.

    You do not need a second novel of controls. You need the conflicts on the table. Security wants a long-lived audit of every enroll call, including email. Privacy wants the email absent from the audit, a short retention, and no join to the payment processor's identifier. The resolution is an architectural split: the audit stores tenant id, partner client id, enrollment id, and a hash of the actor, with a defined retention; the email stays in the directory service that already has a reason to hold it. That split is a decision in the sense of lesson 61, with security and privacy each holding a veto on their fact.

    Data minimization is the LINDDUN tactic that most often changes a diagram. A field that is not stored cannot be disclosed, linked, or subpoenaed from you. The threat model asks, for every personal field in a message: who reads it, how long it lives, and what breaks if it is deleted. If nothing breaks, delete it from the design.

    Abuse cases

    A use case is something a legitimate actor wants. An abuse case is a goal that violates an invariant, written in the same style so testers can turn it into a case. Keep the description at the level of the invariant, not at the level of an exploit procedure.

    Examples that belong in this review:

    • A partner integration client, presenting a valid client-credentials token for tenant A, reads enrollments for tenant B by changing an identifier in the path.
    • A learner token, valid and audience-correct for the learner API, is replayed against the partner admin route.
    • A caller obtains a capture idempotency key and resubmits it with a different amount or a different course.
    • A browser flow is sent back to an arbitrary host after payment, so the return URL becomes an open redirect on a domain the learner trusts.
    • A CI log contains a long-lived partner secret, and anyone with log access acts as the partner.
    • An analyst export joins progress to payment identifiers for a marketing campaign the privacy notice does not describe.

    Each abuse case names the principal, the boundary they cross, the asset, and the control that should make the case fail. "Hacker attacks the API" is not an abuse case. It has no invariant.

    Authentication, authorization, and where the check lives

    Authentication answers who the caller is. Authorization answers whether that caller may perform this operation on this object. A gateway can authenticate, and it can enforce coarse scopes: this client is a partner integration, this token includes enrollments.write. The object check — this employee is in this tenant, this seat is unpaid, this idempotency key was bound to this amount — uses data the owning service stores. If the gateway tries to be the only authorization point, it either reimplements every rule or it trusts headers the services should not trust.

    The header X-User-Id, or a tenant id in a query string that overrides the token, is the recurring defect. Lesson 44 forbids it. The threat model makes the forbid local: the enrollment service reads the tenant from the token's claim, after validating issuer, audience, and signature, and treats a body field named tenantId as untrusted input to be compared, not obeyed. A mismatch is a deny and an audit event. It is not a "flexibility" feature.

    Scopes are not rows. A scope enrollments.write that allows every tenant the platform hosts is an elevation path waiting for one confused client. Bind the token to a tenant at issuance: a claim tenant_id that the authorization server sets and the resource server checks against the row. For user-delegated access, the subject is the learner, and the row's learner id must match. For partner access, the subject is the client, and a directory lookup must show the employee is in the tenant. Those are two grants. One token shape with a nullable subject will be mis-issued.

    OWASP's API Security Top 10 names the object-level failure BOLA: broken object level authorization. You will see it called IDOR in older write-ups. The architectural mitigation is identical: the data owner loads the row and checks the principal against it on every read and write. Security through an unguessable identifier is not the control. Identifiers leak through logs, referrers, and exports.

    Secrets as a data-flow problem

    A secret is an asset with a special failure mode: a copy is as good as the original, and you often cannot tell which copy was used. The threat model tracks copies.

    Places copies appear, and the control that belongs there:

    • Source of truth. A secret manager the platform provides — cloud secret manager, or Vault, or the managed store your company already runs — addressed by workload identity. Application code receives the value at start or on a refresh. It does not ship the value in a git repository or an image layer.
    • Runtime memory. The process needs the value. Core dumps and debug endpoints are part of the model. Disable unauthenticated actuators. Do not put secrets in exception messages.
    • Logs and traces. Lesson 43's rule, repeated as a threat: authorization headers, cookies, request bodies of payment and login, and connection strings are not fields. Structured logging with an allowlist of keys beats a denylist, because a new field will be missed by a denylist.
    • CI. Short-lived workload identity for deploy. No static cloud key in a pipeline variable that gets printed by a debug step. Partner tokens used in tests are sandbox credentials with no production tenant.
    • Browsers. A partner client secret does not belong in a single-page app. Browser flows use authorization-code with PKCE, which OAuth 2.0's public-client guidance already requires. Confidential clients stay on a server.
    • Rotation. A secret you cannot rotate without an outage will not be rotated. The design includes a second version overlapping the first, which is an availability requirement as much as a security one.

    The Java in this lesson depends on Spring's environment abstraction and on TLS. It does not invent a vendor client. The port is yours:

    java
    public interface SigningKeySource {
    /** Current and previous key ids, so a rotation overlap verifies both. */
    List<VerificationKey> verificationKeys();
    }

    An adapter reads from the platform store using the workload's identity. A test adapter reads from a fixture. The domain code never opens a file called prod-keys.pem.

    Denial of service is a security threat and an SLO threat

    STRIDE's D is often handed to the infrastructure team and forgotten. On a public API it is the place payment and search policies diverge, as lesson 62 said. An unauthenticated route that triggers a capture attempt can burn the capture error budget and the acquirer's rate limit. The control is architectural: unauthenticated callers can start a browser payment session at the acquirer, not open unlimited rows in your ledger; partner tokens have a quota per tenant; the gateway enforces a maximum body size and a maximum fan-out. Rate Limiting, Throttling and Load Shedding is the mechanism. The threat model names the route that must not be cheap to call.

    Residual risk

    A model with every threat "mitigated" is unfinished, because some risks are accepted. Write them in a table: threat, why you accept it, who accepts it, what would make you reopen. Example: a compromised partner administrator can enroll and remove employees in that tenant until the customer's IdP revokes them. You accept this because the customer owns that administrator relationship. You do not accept the same person touching another tenant. The owner is the product director for partnerships, and the reopen trigger is a contractual requirement for step-up approval on bulk enroll.

    Acceptance is an ADR consequence. It is not a risk register that nobody reads. If the control is "we will pen-test later," the threat is open, not accepted. A test is a way to check a control. It is not the control.

    Architecture and Design Diagrams

    Trust boundaries for partner enrollment and seat purchase:

    mermaid
    flowchart TB
    subgraph internet [Untrusted network]
    partner[Partner backend]
    browser[Learner browser]
    end
    subgraph edge [Edge trust boundary]
    gw[API gateway]
    end
    subgraph apps [Authenticated services]
    enroll[Enrollment service]
    dir[Directory]
    pay[Payment service]
    end
    subgraph data [Data boundary]
    edb[(Enrollment DB)]
    ddb[(Directory DB)]
    pdb[(Payment ledger)]
    end
    acq[Acquirer]
    idp[Authorization server]
    partner -->|client credentials TLS| gw
    browser -->|auth code plus PKCE| gw
    gw --> idp
    gw --> enroll
    gw --> pay
    enroll --> dir
    enroll --> edb
    dir --> ddb
    pay --> pdb
    browser -->|payment instrument| acq
    pay -->|capture by reference| acq

    The cardholder data crosses from the browser to the acquirer. The payment service sees a reference. That arrow is the PCI scope decision, drawn so a later design cannot "simplify" it by posting the card to the gateway.

    Authorization sequence for a partner enroll call:

    mermaid
    sequenceDiagram
    actor Partner
    participant GW as Gateway
    participant AS as Authorization server
    participant EN as Enrollment
    participant DIR as Directory
    Partner->>AS: client credentials, tenant-bound client
    AS-->>Partner: access token, aud=enrollment, tenant_id
    Partner->>GW: POST /tenants/{id}/enrollments
    GW->>GW: verify signature, iss, aud, exp
    GW->>EN: forward token, no X-User-Id
    EN->>EN: tenant_id claim must equal path id
    EN->>DIR: employee in this tenant?
    DIR-->>EN: yes, under workload identity
    EN->>EN: insert row for that tenant only
    EN-->>Partner: 201

    If the path id and the claim differ, the sequence ends in a deny before the directory call. The directory is not asked to answer a cross-tenant question.

    STRIDE on that one interaction, as a map rather than a novel:

    mermaid
    flowchart LR
    flow[Partner to enrollment write]
    flow --> s[Spoofing: token of another client]
    flow --> t[Tampering: amount or employee id in body]
    flow --> r[Repudiation: no audit of client id]
    flow --> i[Disclosure: response includes other learners]
    flow --> d[DoS: unbounded enroll batch]
    flow --> e[Elevation: path tenant overrides claim]

    Technical Explanation

    How to run the review in ninety minutes

    Spend the first twenty minutes on the diagram until every participant agrees it matches the intended design, not the design they wish they had. Missing data stores are the usual find: a Redis cache of rosters, a log pipeline, a data warehouse export, a dead-letter topic. Caches and logs are stores. If they hold personal data or tokens, they are in the model.

    Spend the next forty minutes on interactions that cross a boundary, STRIDE first, then LINDDUN on any flow that touches personal data. One facilitator writes the row. The author of the design is not the only person allowed to declare a threat mitigated. The control has to be a component that exists: "enrollment compares tenant_id to the path and uses a schema role that cannot read another tenant's table" is a control. "We use best practices" is not.

    Spend the last thirty minutes on abuse cases and residuals. Pick three abuse cases and walk them through the diagram. If the walk does not fail closed, you have a design change, not a documentation change. End with the acceptances and the reopen triggers. Unfinished threats get owners and dates. They do not get the word "mitigated" as a courtesy.

    Token binding details that belong in the model

    The resource server checks at least: signature using keys from a JWKS document it fetched over TLS from a configured issuer; iss; aud containing this API; exp and nbf with a small clock skew; a tenant claim issued by the authorization server, not by the client. Lesson 44 shows the Spring Security resource-server setup that performs these checks. The threat model says what remains if any check is skipped. Skip aud, and a token minted for the directory API is replayed at enrollment. Skip the tenant comparison, and the path id becomes the authorization decision. Those two sentences are more useful than a long list of header names.

    Partner clients are confidential and use client credentials, with a secret or a private key JWT as the authorization server allows. Learner clients are public and use authorization code with PKCE. Refresh tokens for learners are bound to the client and rotated. None of this requires a custom token format. OAuth 2.0 and OpenID Connect are the contracts. A home-grown token "for simplicity" removes the library that checks alg and audience, which is how spoofing returns.

    Idempotency as an integrity control

    Idempotency and Exactly-Once Processing treats keys as a correctness tool. In this model the key is also an integrity boundary. The first request binds the key to a hash of the operation: tenant, employee, course, amount, currency. A retry with the same key and the same hash returns the original result. A retry with the same key and a different hash is a conflict, not a second capture. The abuse case "change the amount on retry" fails closed. Store the hash next to the key. Do not store the raw card, and do not log the full key if the key itself is a bearer secret; an opaque random key is fine to log in truncated form if support needs it.

    Tenant isolation is a query invariant

    Whether the deployment is a shared schema with a tenant_id column or a schema per tenant, as lesson 60 discusses, the threat is the same: a missing predicate. Shared schema is easier to get wrong once. Schema-per-tenant is easier to get wrong in migrations and connection pools, where a pool borrowed from tenant A serves a request for tenant B. The model names the mechanism: a request-scoped tenant context taken from the token, a repository method that does not offer an unscoped findById, and a test that loads two tenants and asserts the deny. Repository, Unit of Work and Specification is the low-level place that test lives. The architecture review asks whether an unscoped method exists at all.

    Row-level security in PostgreSQL can be a backstop. It is not a reason to skip the application check. Database features fail open when a role is marked BYPASSRLS for a migration and the role is reused. Note that residual if you depend on it.

    Privacy flows that are not security bugs

    A nightly export of "all progress events with email and payment customer id" may be perfectly authenticated and still fail LINDDUN. The control is purpose limitation: the export job's identity can read progress, the payment service does not put its customer id into the progress topic, and a join requires a separate decision with a retention and a named purpose. Event payloads from lesson 61's outbox guidance apply: identifiers, not copies of personal data, unless a reader has a reason and a deletion story.

    Retention is a control you can test. "We delete on request" with no job and no backup story is a non-compliance threat. Backups that keep a deleted email for the backup's lifetime are a residual you must write down, because Disaster Recovery and Business Continuity will otherwise optimize for restore completeness against the privacy promise. The two lessons have to name the same retention.

    What this review does not do

    It does not produce exploit payloads, proof-of-concept requests, or a procedure for breaking a system you do not operate. It does not replace a penetration test, a dependency scan, or a code review of the JWT library. Those activities look for implementation mistakes inside an accepted design. Threat modeling looks for a design that implements the wrong check perfectly. Both finds matter. Mixing them produces a meeting that debugs a header and never notices the missing tenant predicate.

    It also does not invent a cryptography protocol. TLS for the channel, a vetted JWT library for the token, the acquirer's hosted fields for the card. Novel crypto is a threat of its own.

    Practical Implementation or Design Walkthrough

    Problem statement

    Threat-model TechLearningPro's public API for one flow: a partner enrolls an employee and purchases a seat. The partner calls from their backend. The employee later signs in as a learner. Payment is confirmed with the acquirer. Produce the diagram, the threat table for the enroll write and the capture, the LINDDUN notes for email and progress, and the residuals.

    Assumptions and constraints

    Assumptions: one authorization server you operate; tokens are JWTs; enrollment and payment are separate services with separate databases; the acquirer hosts the card form; peak enroll is the lesson 62 envelope, not a new number.

    Constraints: card PAN and CVC never touch TechLearningPro logs, databases, or traces; a partner client is bound to one tenant at registration; learners are public OAuth clients; services authenticate with workload identity as in lesson 44; the gateway is not allowed to mint a tenant id the token did not contain.

    Assets in scope

    Partner client secrets, learner refresh tokens, signing keys, roster membership, enrollment rows, payment references and amounts, learner email, course progress. The card number is an asset you refuse to hold. That refusal is control number one and is drawn on the diagram.

    Threat table for the enroll interaction

    IDClassAbuseControlResidual
    T1SpoofingStolen partner secret from CISecret manager, rotation, CI uses a sandbox client; production secret never in the job logA customer can still leak their own secret; blast radius is one tenant
    T2SpoofingToken for another API replayedaud checked at gateway and again at enrollmentClock skew window of 60 seconds
    T3ElevationPath tenant id differs from claimDeny on mismatch; repository has no unscoped findBug in a new endpoint that skips the helper
    T4ElevationLearner token calls partner routeRoute requires partner client grant, not a user grantMis-issued token from the authorization server
    T5TamperingBody employee id not in tenantDirectory check under workload identity, deny by defaultDirectory replication lag of a few seconds, accepted
    T6DisclosureList endpoint returns another tenantQuery predicate and response review; no "admin all tenants" scope on this APISupport break-glass, separate model
    T7RepudiationPartner denies a bulk enrollAudit of client id, tenant, enrollment id; email not in the auditAudit retained 180 days, then gone
    T8DoSMillion-row bulk enrollMax batch size, per-tenant quota, asynchronous job above 100 rowsQuota misconfiguration
    T9LinkabilityProgress event carries payment customer idSeparate identifiers; join is a new decisionWarehouse owner may request a join later
    T10DisclosureEmail in the error body and the logError code enum; structured log allowlistA new field added without review

    T3 is the one that changes code shape. The service method starts from the authenticated tenant, not from the path:

    java
    public Enrollment enroll(Jwt jwt, String pathTenantId, EnrollCommand cmd) {
    String tenant = jwt.getClaimAsString("tenant_id");
    if (tenant == null || !tenant.equals(pathTenantId)) {
    throw new AccessDeniedException("tenant");
    }
    if (!jwt.getAudience().contains("enrollment")) {
    throw new AccessDeniedException("audience");
    }
    directory.assertEmployee(tenant, cmd.employeeId());
    return repository.insert(tenant, cmd.employeeId(), cmd.courseId());
    }

    repository.insert takes the tenant as a non-null argument and the SQL includes tenant_id = ? on every read. There is no insert(cmd) overload. The audience check is defense in depth behind the gateway, cheap next to the cost of trusting the network. AccessDeniedException maps to 403 with a stable error code and no echo of the other tenant's data. A 404 is sometimes preferred so a caller cannot probe which ids exist. For a path that already contains the caller's claimed tenant, 403 on mismatch is honest. For a lookup of an enrollment id, prefer 404 when the row is outside the tenant, so existence in another tenant is not disclosed. That distinction belongs in the API contract from lesson 40.

    Capture interaction, additional rows

    The browser sends the payment instrument to the acquirer. Your service receives a payment method reference. The threat "enrollment logs the JSON body" is closed by not having the PAN in the body, and by an allowlist logger so a future field cannot sneak in. The threat "same idempotency key, different amount" is closed by binding the key to the hash of tenant, course, and amount. The threat "service forwards the learner token to the acquirer" is closed by using client credentials or the acquirer's own server-side credential, the confused-deputy control from lesson 44.

    Open redirect on the return URL: store no arbitrary URL. The return path is a route on your host, the same reasoning as the permalink decision in lesson 23. A partner that wants to land on their LMS gets a fixed allowlisted prefix per tenant, configured by your staff, not a query parameter.

    LINDDUN on the employee record

    Email is required to send the invite and is owned by the directory. Enrollment stores the employee id. The invite worker reads the email through the directory API and does not persist a second copy. Progress events carry the employee id and the course id, not the email, and not the payment customer id. The privacy notice says the employer can see completion, not every page view. The detectability control is the event schema: page views are not on the partner API. Unawareness: the employer is the one who contracted, and the notice to the employee is part of the invite template, which is a product deliverable in the same release as the API. Non-compliance residual: backups retain deleted employees for the backup lifetime of 30 days, and the data-processing agreement says so.

    Validation

    • A test issues a token for tenant A and calls a tenant B path; expects deny and zero directory calls if you short-circuit, or a directory call that is itself tenant-scoped. Assert no row.
    • A test uses a token whose aud is the directory API; expects deny at enrollment.
    • A test retries capture with the same idempotency key and a different amount; expects conflict and a single ledger row.
    • A test posts a body shaped like a card number to the enroll route; expects rejection by schema, and a log assertion that the body was not written. The assertion is on your logger allowlist, not on a secret scanner alone.
    • A review checklist line: every new route names its grant, its tenant source, and its personal-data fields.

    Expected result

    The customer's security letter can be answered with the diagram, T1's blast radius, and T3's predicate. Residuals are signed: directory lag, backup retention, a compromised tenant admin inside their own tenant, support break-glass as a separate model you have not finished. Nothing in the packet says "no issues found."

    Enterprise Real-World Scenarios

    The partner token that was a tenant master key

    A launch customer asked for a quick integration. The authorization server issued one client, put the tenant id in the partner's request body, and gave the client enrollments.write for the whole platform "until we have time to segment." The gateway checked the signature and passed the body through. A test fixture in the customer's CI used production by mistake. The log line included the client secret because the HTTP client's debug flag was on. The secret left the building in a build log their contractor could read.

    What the contractor could have done is less important than what the design allowed: any holder of that secret could pass any tenantId. The incident response rotated the secret, which closed T1 for the future and did not close T3 for the past design. The structural fix was the claim binding in the walkthrough, a per-tenant client, the debug flag banned in the partner SDK's examples, and a log allowlist. The residual that product accepted, in writing, was that a customer who leaks their own secret can still mutate their own tenant until rotation. They cannot mutate another tenant even with that secret. That sentence is the one the customer's security team needed.

    The review failure was social as much as technical. The "temporary" global scope was a type 1 decision in the sense of lesson 61: external clients would store the behavior. There was no RFC because the change was a checkbox in an admin UI. The strategy note gained a line. Grants that cross tenants are a one-way design and require a threat-model row before the client is issued. The checkbox was removed.

    Progress data, sold as a feature, modeled as linkability

    Marketing asked for a dashboard that showed "companies whose employees struggle in module 3, correlated with purchase plan." The implementation plan was a warehouse join of progress events, directory emails, and payment customer ids. Every service involved was authenticated. The STRIDE pass a team ran on the pipeline found nothing, because nothing was spoofed. The privacy counsel asked a LINDDUN question: the employer contract allowed completion status for their own employees, not a cross-customer inference, and the employee notice did not mention payment data at all.

    The join was rejected. The dashboard that shipped showed each employer their own completion counts, aggregated below a small-number threshold so a team of three people could not be re-identified by a rare course pair. That threshold is an identifiability control. Payment data stayed in the payment warehouse with a different grant. The residual, accepted by counsel, is that an employer can still see completions for named employees, because that is the contract. The reopen trigger is any request to add page-level telemetry or payment status to that API. STRIDE did not make this decision. A model that only runs STRIDE will approve the join. The lesson's insistence on a privacy pass is the difference.

    Best Practices

    • Start from a data-flow diagram that includes logs, caches, exports, and dead-letter stores. A store you forgot is the disclosure path.
    • Name assets as records and keys, not as "trust."
    • Use STRIDE on boundary crossings and on stores. Bring domain invariants with you, or elevation will stay generic.
    • Run LINDDUN when personal data appears. Expect a conflict with audit, and write the split.
    • Write abuse cases a tester can turn into an authorization test. Keep them at the invariant.
    • Authenticate at the edge and authorize again at the data owner. Do not trust a tenant id from the body or from X-User-Id.
    • Bind partner tokens to one tenant inside the token. Treat a platform-wide scope as a threat, not as a convenience.
    • Keep PAN out of the system by a drawn data flow, not by a policy paragraph alone. Allowlist log fields.
    • Treat idempotency keys as integrity: same key, different body, is a conflict.
    • Track secret copies: source of truth, memory, logs, CI, browsers. Rotation overlap is part of the design.
    • End with residual risk, an owner, and a reopen trigger. "Mitigated" is allowed only when a control exists in the design.
    • Re-run the model when a new public route, a new export, or a new identity grant appears. Do not wait for the annual questionnaire.

    Common Mistakes and Anti-Patterns

    The scanner as the model. A dependency report is attached as the threat model. It happens because the report is concrete and the diagram is a meeting. The consequence is an unscoped findById shipping next to a fully patched library. The repair is the four questions and a data-flow diagram before any tool output.

    Gateway-only authorization. The edge checks that the JWT is signed, and the service trusts the path. It happens because the gateway is "the security layer." The consequence is BOLA across tenants the moment a route forgets a filter. The repair is the predicate in the service that owns the row, with a test for a foreign tenant.

    STRIDE without privacy. The join of payment and progress is authenticated, encrypted, and audited, and still unlawful relative to the notice. It happens because STRIDE does not ask about purpose. The repair is a LINDDUN pass on every new analytics export.

    Secrets in the convenient copy. A client secret in a frontend, a debug HTTP log, or a terraform state world-readable to the company. It happens because the happy path needs the value once. The consequence is a master key in a system you do not monitor. The repair is the copy list, workload identity, and an allowlist logger.

    Unbound idempotency. The key deduplicates requests but the second body is ignored or, worse, applied. It happens because the key was added for retries only. The consequence is an integrity bug that looks like a network retry. The repair is to persist the request hash and conflict on mismatch.

    Open return URLs. A return_to parameter makes the partner integration "flexible." It happens because permalinks and payment returns get designed from a blog post. The consequence is an open redirect on your domain. The repair is lesson 23's rule: destinations are a function of configuration you control, or a fixed allowlist per tenant.

    Risk acceptance by silence. The minutes say the team discussed security and agreed to proceed. It happens because nobody wants to own a residual. The consequence is an implicit acceptance of the worst reading. The repair is a table with a name and a reopen trigger, stored as an ADR consequence.

    Troubleshooting and Design Review

    Symptom: A partner can read another tenant's enrollment by changing an id.

    Likely cause: the tenant comes from the path or body, and the query is unscoped. The token was valid, so authentication "worked."

    Investigation: read the repository method and the token claims used. Look for a global admin scope issued to partner clients.

    Corrective action: bind tenant into the token, deny on mismatch, remove the unscoped query, rotate any client that had a platform-wide grant.

    Symptom: The audit log contains emails and authorization headers.

    Likely cause: a request logger on the framework default, or an exception that includes the body.

    Investigation: sample production logs after a test enroll. Check the logger configuration and the payment route specifically.

    Corrective action: allowlist fields. Rebuild the log pipeline's retention so the bad days expire. Treat the leak as a disclosure incident proportional to what was stored.

    Symptom: Payment return URLs sometimes leave your domain.

    Likely cause: a return_to parameter or a stored partner URL with no host allowlist.

    Investigation: read the location header construction. Search for persisted absolute URLs.

    Corrective action: same as the permalink ADR. Allowlist hosts per tenant, configured out of band, or drop the feature.

    Symptom: The threat model document is a year old and does not show the warehouse.

    Likely cause: the model was a launch artifact. New stores were "analytics," not "architecture."

    Investigation: diff data stores in production against the diagram. Include topics, buckets, and BI tools.

    Corrective action: add the store, run LINDDUN on its fields, and put "new store requires a model delta" on the RFC template from lesson 61.

    Symptom: Directory lag allows an enroll of an employee removed seconds ago.

    Likely cause: the accepted residual in T5, or a cache that exceeded what was accepted.

    Investigation: compare the cache TTL to the residual's wording.

    Corrective action: if the business now requires immediate revocation, the residual is no longer acceptable. Read from the source of truth for the grant, and pay the latency in the enroll SLO on purpose.

    Design-review questions: Where are the trust boundaries, including tenant and CI? Which stores exist, including logs and caches? For the enroll write, which claim is the tenant, and what happens if the path disagrees? Where is the card number allowed to be? Which personal fields are copied beyond their source of truth, and for how long? What does a second tenant's token receive, status code included? Which secrets exist in CI and in browsers? What is accepted, by whom, and what reopens it?

    Interview Questions and Answers

    Q1. What do you produce in a threat model, and what do you refuse to claim?

    I produce a data-flow diagram with trust boundaries, a list of assets, STRIDE on the interactions that cross those boundaries, LINDDUN where personal data is involved, a few abuse cases tied to invariants, controls that exist in the design, and residual risks with owners. I refuse to claim the system is secure or that no issues were found. A model that mitigates every line by assertion has not finished the fourth Shostack question: did we do a good job?

    Q2. What is a trust boundary in an API that already sits behind a gateway?

    A trust boundary is any place the set of principals you believe changes. The internet to the gateway is one. The gateway to the enrollment service is another, because the gateway does not own the roster. The service to its database is another. Two tenants in one table are a boundary even though no socket is crossed. CI to production is a boundary. I draw all of them. Network location inside the cluster is not one of the things I believe, which is the zero-trust assumption from lesson 44.

    Q3. Walk STRIDE on a partner calling POST /tenants/{id}/enrollments.

    Spoofing: a stolen client secret or a token minted for a different audience. Tampering: an employee id or a batch body that is not what the directory would allow. Repudiation: no record of which client created the seat. Information disclosure: the response or a list call includes another tenant or an email the caller does not need. Denial of service: an unbounded batch burns the enroll path. Elevation: the path tenant overrides the token, or a learner token hits this route. Each one maps to a control in the design. The elevation check is the one I will write on the board, because teams remember authentication and forget the predicate.

    Q4. Why do STRIDE and LINDDUN sometimes conflict?

    STRIDE's repudiation threat pushes you to keep a detailed, long-lived record of who did what, often including identifiers that name a person. LINDDUN's non-repudiation, linkability, and disclosure threats push you to keep less, for a shorter time, and to avoid joins across contexts. On enroll, I keep client id, tenant id, and enrollment id for a defined retention, and I leave email in the directory. I do not put a payment customer id on a progress event just because a join would be convenient. The conflict is a product decision with two vetoes, not a reason to skip one of the models.

    Q5. Where should authorization live?

    Authentication can sit at the gateway: signature, issuer, audience, expiry. Authorization of the object sits in the service that owns the row. That service takes the tenant and subject from the validated token and compares them to the data. A tenant id in the body is untrusted. X-User-Id from the network is untrusted. Coarse scopes are useful and are not a substitute for the row check. This is the BOLA mitigation. An unguessable id is not.

    Q6. How do you treat secrets in an architecture review?

    I list the copies. The source of truth is a secret manager reached with workload identity. The process may hold the value in memory. Git, images, logs, traces, exception text, browser bundles, and CI debug output must not. Partner browser apps do not get a client secret; they use authorization code with PKCE. Rotation has an overlap window so availability and secrecy are not enemies. I do not invent a custom crypto protocol for storage. I use the platform store and TLS.

    Q7. Why is an idempotency key a security control?

    Because a retry is an integrity boundary. If the same key can be replayed with a different amount, course, or tenant, the caller can turn a network retry into a different transaction, or a stolen key into a mutated one. I bind the key to a hash of the authorizing fields and return a conflict when the hash differs. I still never store the PAN beside it. The key deduplicates the operation you authorized, not whatever body arrives later.

    Q8. What do you accept as residual risk in a multi-tenant API?

    I accept that a customer's own administrator, or a leak of that customer's own client secret, can change that customer's data until they revoke or we rotate. I accept a few seconds of directory lag if the SLO and the contract allow a just-removed employee to be enrolled and then repaired. I accept backup retention that outlives a deletion, if the privacy notice says so. I do not accept cross-tenant access, platform-wide partner scopes, or card data in our logs. Each acceptance has a name and a condition that reopens it, such as a contract that starts requiring step-up on bulk enroll.

    Q9. How does this relate to a penetration test?

    The model is the design argument: the right checks in the right components. A penetration test and a scanner look for implementation mistakes and known vulnerabilities inside that design. I want both. I do not accept a pen-test booking as the mitigation for a missing tenant predicate. The predicate has to be in the design first, or the test becomes a scavenger hunt the next feature will fail again. I also do not turn the architecture review into exploit writing. The abuse case states the invariant that must fail closed.

    Q10. A product manager wants a return_to URL after payment so partners land in their LMS. What do you decide?

    I refuse an arbitrary URL, because that is an open redirect on a host the learner trusts, and it is a phishing channel. I allow a per-tenant prefix that our staff configures, on an allowlist of hosts, compared precisely, over HTTPS only. If the partner cannot fit that, the flow ends on our receipt page and the partner polls enrollment status with their server credential. The decision is the same shape as the lesson-permalink ADR: the destination is a function of data we control.

    Q11. What would make you update the model the week after launch?

    A new public route, a new data store, a new grant, or a new field of personal data. The warehouse export and the debug log are the usual ones, because they do not look like features. I would also update it when a residual's assumption dies, such as directory lag becoming unacceptable after a real incident. I would not wait for a calendar quarter if the data flow changed. The model is a decision record about invariants, and stale invariants are how T3 comes back as a "small" endpoint.

    Hands-On Exercises

    Exercise: Threat-model a public API with identity and payment

    Problem statement. TechLearningPro will let a partner backend enroll employees and buy seats, and will let those employees sign in and see progress. Payment uses an external acquirer. Produce a threat model for that flow. You may narrow scope to one journey, and you must say what you left out.

    Tasks.

    1. Draw the data-flow diagram with trust boundaries. Include the authorization server, the acquirer, logs, and one analytics store if you have personal data there.
    2. List assets. State explicitly where the PAN is allowed to exist.
    3. Run STRIDE on the partner enroll write and on capture. Use a table: threat, control, residual.
    4. Run LINDDUN on email and on progress. Call out any conflict with the audit log.
    5. Write three abuse cases as invariants, including one cross-tenant read and one idempotency mismatch. Do not include exploit steps.
    6. Specify where authentication stops and authorization starts, and which claim carries the tenant.
    7. List secret copies and say which copies are forbidden.
    8. Record two accepted residuals with owners and reopen triggers, as ADR consequences.

    Expected deliverables. The diagram, the two tables, the abuse cases, and the residual list.

    Evaluation rubric.

    Assessment areaWeightWhat "good" looks like
    Requirements and assumptions15%Scope and out-of-scope are explicit; assets are concrete
    Architecture and design quality25%Boundaries include tenant and logs, not only the firewall
    Technical correctness20%Token checks and the tenant predicate are coherent; PAN stays at the acquirer
    Scalability, reliability, and security20%DoS quota and payment integrity are both present
    Trade-off reasoning10%A STRIDE versus LINDDUN conflict is resolved in writing
    Documentation and communication10%Residuals have names; the model does not claim zero risk

    A diagram with a single "secure gateway" box and a conclusion of "PCI compliant" is not a threat model.

    Lesson Summary

    Threat modeling is a review of a specific data flow: assets, boundaries, STRIDE, LINDDUN where people are in the data, abuse cases as invariants, controls that live in named components, and residuals a person accepts. For TechLearningPro's partner API the decisive controls are a tenant bound into the token, a second check at the service that owns the row, card data that never enters our stores, idempotency keys bound to the authorized body, and logs that allowlist their fields. The gateway authenticates. It does not become the authorization engine for every object.

    STRIDE and LINDDUN will disagree on how much identity to keep. That disagreement is a decision, with security and privacy each able to veto their own fact. A model that only asks whether the JWT signature verifies will approve a cross-tenant join and a platform-wide partner client. The customer's letter about a copied integration token is answered by blast radius: one tenant, rotatable, unable to name another tenant's rows.

    Disaster Recovery and Business Continuity is the next practice. Backups and failover copy data, including the personal data and the secrets this lesson tried to keep few and short-lived. The recovery design has to preserve the same boundaries under a worse clock.

    Further Reading

    Ready to mark this lesson complete?Track your journey across the entire course.