Two ideas sit at the end of most security learning paths because they tie everything else together. Zero trust says: do not grant trust based on network location; verify every request. Threat modeling says: before building something, think systematically about how it could be attacked. Both sound like enterprise programmes with consultants and diagrams. In practice, both fit into a team's normal work.
This post explains zero trust in terms of decisions you make in code and infrastructure, then walks through a STRIDE threat model of a feature we built recently: customers uploading documents to their account.
Zero trust in five decisions
The traditional model trusted anything inside the corporate network or VPN. Zero trust removes that assumption. In practice it comes down to a handful of decisions:
- Authenticate every request, including service-to-service calls inside the network. Internal APIs require tokens or mutual TLS, not just "it came from the private subnet".
- Authorise every request against the resource and the identity, at the time of the request. A valid session is not permission to access everything.
- Use least privilege for users, services and machines, and prefer short-lived credentials.
- Consider device and context: a login from a managed laptop and a login from an unknown device can get different access to sensitive tools.
- Assume breach: segment systems, log access, and design so that one compromised component cannot reach everything.
None of these needs a new product. They need consistent choices: an API gateway that requires tokens on internal routes, network policies that deny by default, identity-aware access to admin tools instead of a flat VPN.
Threat modeling: four questions
A useful framing for threat modeling is four questions:
- What are we working on?
- What can go wrong?
- What are we going to do about it?
- Did we do a good enough job?
STRIDE helps with the second question by giving categories of threats to check for each part of a system:
| Letter | Threat | Property violated | Example |
|---|---|---|---|
| S | Spoofing | Authentication | Using someone else's session token |
| T | Tampering | Integrity | Modifying a file or request in transit or storage |
| R | Repudiation | Non-repudiation | Denying an action because nothing logged it |
| I | Information disclosure | Confidentiality | Reading other customers' documents |
| D | Denial of service | Availability | Exhausting storage or processing |
| E | Elevation of privilege | Authorization | A customer performing admin actions |
Worked example: document uploads
1. What are we working on?
Customers upload documents (contracts, ID scans) to their account. Staff review them in an admin tool. We drew the data flow on a whiteboard:
[Customer browser] --HTTPS--> [API] --presigned URL--> [Object storage: uploads bucket]
| |
| (event) v
| [Scanner worker] --> [Object storage: clean bucket]
v
[Database: document records]
^
[Staff browser] --SSO--> [Admin tool]
Trust boundaries: between the browser and the API, between the API and storage, between the scanner and storage, between staff and the admin tool.
2. What can go wrong?
We walked each element and each arrow with STRIDE. A selection of what we found:
| Element | STRIDE | Threat |
|---|---|---|
| Browser to API | S | Upload request with a stolen session |
| Presigned upload URL | T | URL reused to overwrite another file, or upload a much larger file |
| Uploads bucket | I | Bucket readable publicly or by other customers' URLs |
| Uploaded file | T / E | Malicious file (script, macro document, polyglot) opened by staff |
| API document listing | I | Customer requests another customer's document id |
| Admin tool | R | Staff views or deletes a document without a record |
| Scanner worker | D | Huge or decompression-bomb files exhaust the worker |
| Admin tool | E | Support role can delete documents, which only compliance should |
3. What are we going to do about it?
Each threat became a mitigation and a ticket:
| Threat | Mitigation |
|---|---|
| Stolen session | Re-authentication for uploading ID documents; session binding to device |
| Presigned URL abuse | Short expiry (5 minutes), fixed object key per upload, content-length limit in the signed policy |
| Bucket exposure | Block public access, bucket policy allowing only the API and scanner roles |
| Malicious files | Content-type check by magic bytes, malware scan, files served to staff only after scanning, with Content-Disposition: attachment |
| Cross-customer access | Every document query scoped to the authenticated customer; authorization tests |
| Missing audit | Audit log for view, download and delete in the admin tool |
| Scanner DoS | Size limit before scan, timeouts, separate queue with concurrency limits |
| Over-privileged support role | Delete restricted to compliance role; support can only view |
Some threats were accepted, with a reason: for example, we accepted that a customer could upload many small files within rate limits, because storage cost was low and monitoring would show abuse.
4. Did we do a good enough job?
We checked that every ticket was linked to the threat model, that security acceptance tests existed for the authorization and presigned URL rules, and that the penetration test scope for the next quarter included the upload flow. The threat model document, one page plus the diagram, lives next to the code and is updated when the feature changes.
How zero trust showed up in the model
Several mitigations were zero trust decisions:
- The scanner worker authenticates to storage with its own short-lived role; it cannot read the database.
- The admin tool is reached through identity-aware access with SSO and device checks, not a VPN.
- The API does not trust the
customer_idin the request; it takes the identity from the token and scopes every query to it. - Network location grants nothing: the admin tool's backend requires a valid staff token even on internal routes.
When to threat model
Not every feature needs a session. Ours run for new external-facing features, changes to authentication or authorization, new integrations that receive our data, and anything that moves data across tenants or accounts. The trigger questions on the ticket template decide. A feature that adds a field to an existing internal screen does not need one; a feature that adds file uploads does.
Running a threat model session
- Who: the developers building the feature, a product person, a security champion. Five people at most.
- How long: 60 to 90 minutes for a feature of this size.
- Materials: a whiteboard or a shared diagram, the STRIDE table, the ticket.
- Output: diagram, threats with mitigations or acceptance, tickets.
Teams that do one a month get noticeably faster. After a few sessions, people start raising STRIDE-style questions in normal design discussions without being asked.
Key takeaways
- Zero trust means verifying identity and authorization on every request, regardless of network location.
- It is implemented through everyday choices: tokens on internal APIs, least privilege, segmentation, logging.
- Threat modeling answers four questions; STRIDE structures "what can go wrong?".
- Draw the data flow, walk every element and boundary with STRIDE, and turn threats into tickets.
- Keep threat models short, stored with the code, and updated as features change.
References
- NIST SP 800-207, Zero Trust Architecture: https://csrc.nist.gov/pubs/sp/800/207/final
- CISA Zero Trust Maturity Model: https://www.cisa.gov/zero-trust-maturity-model
- OWASP Threat Modeling Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Threat_Modeling_Cheat_Sheet.html
- Threat Modeling Manifesto: https://www.threatmodelingmanifesto.org/
- OWASP File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.