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
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:
- Build a data-flow diagram that marks trust boundaries, data stores, and external principals for an architecture under review.
- List assets and security properties in language a control can honor: confidentiality, integrity, availability, and accountability.
- Apply STRIDE to interactions across a boundary, and state the control that belongs on the data owner rather than only at the gateway.
- Apply LINDDUN to personal data and explain the conflict between security non-repudiation and privacy.
- Write abuse cases that name a principal, a goal, and a violated invariant, including confused-deputy and cross-tenant access.
- Place secrets so application code never becomes the long-term store, and so logs and images are not accidental copies.
- Separate authentication from authorization, and require the service that owns the row to enforce the tenant and object check.
- Record residual risk with an owner and a revisit trigger, and refuse a review that ends with "no issues found."
Prerequisites
- Architectural Thinking and Quality Attributes: security scenarios need a response measure. "Secure" is not a measure. An allow-or-deny at a named component is.
- Requirements and Architecture Drivers and Staff Engineer Architecture Decision-Making: residual risk is a decision with an owner, not a shrug.
- API Design, Versioning and Contracts and API Gateway and Backend for Frontend: scopes and the edge. The gateway is a boundary, not the authorization engine for every row.
- Microservices Security and Zero Trust: tokens, workload identity, and network policy. Use them as the control catalog.
- Design a Payment Processing System and LLD Case Study: Payment Gateway: no PAN in our store. Idempotency keys are a security control as well as a correctness control.
- Design a Multi-Tenant SaaS Platform: the tenant predicate is the asset boundary for this lesson.
- Observability: Logging, Metrics and Tracing: what must never be a log field.
- You do not need exploit technique. You need to read a data-flow diagram and a token's claims.
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.
| Threat | Property harmed | Architectural shape |
|---|---|---|
| Spoofing | Authentication | Caller pretends to be a partner, a learner, or a service |
| Tampering | Integrity | Body, token, price, or return URL is altered in flight or at rest |
| Repudiation | Accountability | A refund or an enrollment cannot be tied to a principal |
| Information disclosure | Confidentiality | Roster, token, or email is read by a principal who should not see it |
| Denial of service | Availability | A public route consumes the capture budget or the pool |
| Elevation of privilege | Authorization | A 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:
javapublic 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:
mermaidflowchart TBsubgraph internet [Untrusted network]partner[Partner backend]browser[Learner browser]endsubgraph edge [Edge trust boundary]gw[API gateway]endsubgraph apps [Authenticated services]enroll[Enrollment service]dir[Directory]pay[Payment service]endsubgraph data [Data boundary]edb[(Enrollment DB)]ddb[(Directory DB)]pdb[(Payment ledger)]endacq[Acquirer]idp[Authorization server]partner -->|client credentials TLS| gwbrowser -->|auth code plus PKCE| gwgw --> idpgw --> enrollgw --> payenroll --> direnroll --> edbdir --> ddbpay --> pdbbrowser -->|payment instrument| acqpay -->|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:
mermaidsequenceDiagramactor Partnerparticipant GW as Gatewayparticipant AS as Authorization serverparticipant EN as Enrollmentparticipant DIR as DirectoryPartner->>AS: client credentials, tenant-bound clientAS-->>Partner: access token, aud=enrollment, tenant_idPartner->>GW: POST /tenants/{id}/enrollmentsGW->>GW: verify signature, iss, aud, expGW->>EN: forward token, no X-User-IdEN->>EN: tenant_id claim must equal path idEN->>DIR: employee in this tenant?DIR-->>EN: yes, under workload identityEN->>EN: insert row for that tenant onlyEN-->>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:
mermaidflowchart LRflow[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
| ID | Class | Abuse | Control | Residual |
|---|---|---|---|---|
| T1 | Spoofing | Stolen partner secret from CI | Secret manager, rotation, CI uses a sandbox client; production secret never in the job log | A customer can still leak their own secret; blast radius is one tenant |
| T2 | Spoofing | Token for another API replayed | aud checked at gateway and again at enrollment | Clock skew window of 60 seconds |
| T3 | Elevation | Path tenant id differs from claim | Deny on mismatch; repository has no unscoped find | Bug in a new endpoint that skips the helper |
| T4 | Elevation | Learner token calls partner route | Route requires partner client grant, not a user grant | Mis-issued token from the authorization server |
| T5 | Tampering | Body employee id not in tenant | Directory check under workload identity, deny by default | Directory replication lag of a few seconds, accepted |
| T6 | Disclosure | List endpoint returns another tenant | Query predicate and response review; no "admin all tenants" scope on this API | Support break-glass, separate model |
| T7 | Repudiation | Partner denies a bulk enroll | Audit of client id, tenant, enrollment id; email not in the audit | Audit retained 180 days, then gone |
| T8 | DoS | Million-row bulk enroll | Max batch size, per-tenant quota, asynchronous job above 100 rows | Quota misconfiguration |
| T9 | Linkability | Progress event carries payment customer id | Separate identifiers; join is a new decision | Warehouse owner may request a join later |
| T10 | Disclosure | Email in the error body and the log | Error code enum; structured log allowlist | A 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:
javapublic 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
audis 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.
- 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.
- List assets. State explicitly where the PAN is allowed to exist.
- Run STRIDE on the partner enroll write and on capture. Use a table: threat, control, residual.
- Run LINDDUN on email and on progress. Call out any conflict with the audit log.
- Write three abuse cases as invariants, including one cross-tenant read and one idempotency mismatch. Do not include exploit steps.
- Specify where authentication stops and authorization starts, and which claim carries the tenant.
- List secret copies and say which copies are forbidden.
- 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 area | Weight | What "good" looks like |
|---|---|---|
| Requirements and assumptions | 15% | Scope and out-of-scope are explicit; assets are concrete |
| Architecture and design quality | 25% | Boundaries include tenant and logs, not only the firewall |
| Technical correctness | 20% | Token checks and the tenant predicate are coherent; PAN stays at the acquirer |
| Scalability, reliability, and security | 20% | DoS quota and payment integrity are both present |
| Trade-off reasoning | 10% | A STRIDE versus LINDDUN conflict is resolved in writing |
| Documentation and communication | 10% | 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
- Adam Shostack, Threat Modeling: Designing for Security — the four questions and the discipline of diagramming before listing threats.
- Microsoft STRIDE — spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege, applied here per interaction.
- Deng, Wuyts, Scandariato, Preneel, and Joosen, LINDDUN — the privacy threat categories, including the non-repudiation tension with security logging.
- OWASP API Security Top 10 — BOLA and the object-level authorization failures this lesson treats as architecture.
- NIST SP 800-207 — zero trust, the control catalog lesson 44 implements and this lesson places.
- OAuth 2.0 (RFC 6749) and PKCE (RFC 7636) — confidential versus public clients, which decides where a secret may live.
- PCI DSS — the scope argument for keeping PAN at the acquirer. The standard is the obligation; the diagram is the design.
- This course: Microservices Security and Zero Trust, API Gateway and Backend for Frontend, Design a Payment Processing System, Design a Multi-Tenant SaaS Platform, Idempotency and Exactly-Once Processing, Observability: Logging, Metrics and Tracing, Non-Functional Requirements and SLO Design.