A security boundary is where the authority, trust level or data owner can change, not where a directory begins. To find boundaries in a monorepo, start with one sensitive action, trace its entry point to the protected object and output, and mark every handoff that changes who can ask, what can be trusted or who owns the data. Confirm each handoff against deployment and configuration, then record the check that authorizes the precise action on the precise object.
The map changes when the same library is used in different deployments, a queue has multiple producers, or a route is private in one environment and public in another. Source code can reveal a possible path; it cannot by itself prove an ingress rule is active or that a request was successfully exploited. OWASP's architecture documentation guidance starts with components, data flows and trust boundaries. Use that as a working map, then challenge it with source and deployment evidence.
Start with an action, not a directory tree
Suppose the repository contains apps/, packages/, services/ and infra/. Those names tell you where authors put files. They do not tell you whether a job can read another customer's data, whether a service has its own identity, or whether a package runs inside several services. The OWASP attack surface analysis cheat sheet recommends identifying exposed functions together with their protective controls.
Pick a sentence with an actor, verb and object: “An account member downloads an invoice belonging to their account.” Then collect six inputs at one pinned revision:
- Entry: routes, message consumers, command handlers and scheduled triggers that can start the action.
- Identity: how the human or machine principal is established, and who can issue any delegated token.
- Selection: request IDs, tenant IDs, queue fields and database keys used to choose the object.
- Authority: the code or policy deciding that this principal may perform this action on that selected object.
- Data movement: service calls, queues, stores, caches and exports that carry the request or result.
- Delivery: every place the result becomes visible, including direct download, search, reports and notifications.
Look up deploy manifests, gateway routes and environment policy with the source. Ask a platform owner to confirm which version is active. A design diagram can narrow the search; it is evidence of intent until reconciled with deployed routes and code. OWASP's authorization guidance emphasizes validating authorization for each request and avoiding reliance on predictable object identifiers as a control.
Illustrative case: one export, four trust changes
Illustrative case. Acorn Ledger is a fictional multi-tenant billing product. Its monorepo has apps/web, services/export-api, workers/pdf, packages/auth-context, packages/invoice-store, and infra/routes. A staff engineer reviews whether a member of tenant Elm could obtain tenant Pine's invoice. Names and paths are invented; no live attack or source review occurred.
The web app calls POST /exports with an invoice_id. The gateway verifies a session and passes a subject to the API. The API writes a queue message. A PDF worker loads the invoice, writes a document, and a separate download endpoint gives it to the requesting member. Four boundaries matter:
| Boundary | Change in trust or ownership | Required question | Evidence to collect |
|---|---|---|---|
| Browser → gateway/API | User input becomes an authenticated request | Is the session valid, and which user/tenant does it represent? | Route and session validation version. |
| API → invoice store | Supplied ID selects a tenant-owned object | Is this user allowed to export this particular invoice? | Lookup key, authoritative owner field and authorization branch. |
| API → queue → worker | A message becomes worker authority | Who can produce or replay it, and does the worker rebind invoice to the authorized job? | Producers, queue policy, message schema and worker read. |
| Worker → download | File becomes visible to a recipient | Does current access to the job also authorize the generated invoice? | Output binding, delivery route, cache and link policy. |
The gateway session check is valuable, but it does not answer the object question. OWASP API1:2023 describes this distinction for API object IDs. Likewise, a private queue controls who sends messages but does not automatically validate the invoice owner inside an authorized producer's message.
Replace the folder diagram with a principal map
Acorn's deployment manifests add a complication: the same workers/pdf folder builds two processes. The ordinary renderer runs as pdf-worker; an operator recovery command runs as support-repair. They can read different stores. Meanwhile packages/auth-context is copied into both the API and download builds; it is not a separate service or an extra network boundary.
The useful map is therefore:
member Elm → export-api principal → stored job J9 → pdf-worker principal
support operator → retry API principal ──────────┘
pdf-worker → private artifact A9 → download-api principal → current recipient
support-repair → separate recovery operation (outside J9's normal consumer)
These are fictional application principals, not literal cloud policy names. Each arrow needs a versioned enforcement point. The boundary register records who chooses the object as well as who can send the request:
| Edge | Identity trusted by receiver | Object selector actually used | Result of this source trace |
|---|---|---|---|
| Member → ordinary API | Validated member u-17, tenant Elm |
Request invoice i-42, checked against stored owner Elm |
API stores job J9 with authorized invoice i-42 and recipient u-17. |
| Support → retry API | Operator with retry permission | Operator submits J9 plus replacement i-88 owned by Pine |
Producer accepts the replacement field; suspicious lead. |
| Retry message → worker | Approved producer, job ID J9 |
Worker reloads J9 and uses stored i-42; ignores replacement field |
The proposed i-88 substitution does not reach the read. |
| Worker → artifact | Job J9 and selected invoice i-42 | Output metadata binds A9 to J9 and Elm |
Artifact remains private; the renderer does not deliver it. |
| Recipient → download | Current member and job entitlement | Download loads J9, then its stored artifact A9 | Source binds delivery to the job; membership freshness and deployment remain separate checks. |
A private or signed message can authenticate a producer while carrying an unauthorized object choice. Here the decisive defense is more specific: the consumer does not use that choice. The support route's permissive schema deserves cleanup because it misleads operators, but it does not establish the alleged foreign-invoice read on this path. If the worker instead trusted message.invoice_id, the earlier ordinary API check would not protect the retry producer.
Keep the recovery process out of the wrong conclusion
The support-repair command reads invoice IDs directly, but its operator authorization and output destination differ from J9's flow. It becomes its own row: “Operator can request recovery rendering; capability policy and artifact delivery not yet reviewed.” Do not borrow the worker's job binding to clear it. Do not borrow the recovery command's broad read to claim that an ordinary member can invoke it.
The final map now has one resolved source question and two important evidence requests. The replacement-ID lead is rejected for the examined J9 worker. Platform must match that worker and the download route to deployed artifacts. The support owner must identify the recovery command's allowed operators and delivery policy. The map has become smaller and more useful: it distinguishes a real downstream control, an unanswered operator boundary, and deployment facts that source cannot supply.
Search alternate producers by the shared operation they reach, not only by route names. Retries, replay tools and admin commands can cross different boundaries even when they sit in the same folder. Use CODEOWNERS to locate maintainers; GitHub describes it as a review assignment mechanism, not a runtime access policy.
Check the return path as carefully as the request path
Acorn's map does not end when the worker selects the right invoice. A correct renderer could still attach the artifact to the wrong job, notify the wrong member, or expose a download through a cache whose key omits the necessary account context. Those are distinct hypotheses to examine at the output edges. They should not be presented as observed flaws in the fictional system merely because the map makes them possible questions.
For A9, record which trusted fields bind the artifact to J9, which fields the download service reloads, and when the recipient's entitlement is checked. If the delivery mechanism is a reusable capability, identify the moment that capability leaves the service and the rule governing its lifetime. The reviewer can then state whether the map covers selection, publication and delivery, or only the first two stages.
This also clarifies incident scope. If a support operator can generate an invoice through support-repair, the meaningful follow-up is who can retrieve that output. Broad read authority in a restricted recovery process may be intended, while a shared output destination may create a different concern. Trace the actual recipient boundary instead of assuming that every read has the same exposure.
Mark the assumptions that make two edges different
The ordinary API and support retry may use the same queue yet rely on different authority. For each producer, state what the receiver is entitled to trust: an authenticated service identity, an immutable job identifier, or a claim that still requires verification. Do not collapse them into one “internal traffic” arrow. The queue's delivery role and the job's authorization role are separate parts of the explanation.
After a design change, compare these assumptions before reusing the map. If a new producer can write job records directly, the worker's stored-job defense needs another review even though the worker code is unchanged. If the new producer can only request an existing authorized job through the same admission contract, much of the earlier reasoning may still apply. The map should make that difference apparent to an engineer reviewing the change.
The output is a boundary register that can guide maintenance. It tells each owner which values must remain trusted, which operations select protected data, and which deployment facts remain outstanding. That is more durable than a diagram of folders, and more precise than declaring all communication inside the repository equally trusted.
Separate a map from a verdict
A boundary map is finished when each crossing has an evidence reference, an owner and a state: checked, needs deployment confirmation, blocked by access, or outside the declared scope. A dashed line on a diagram cannot stand in for an implemented authorization check. For a source-supported concern, state the prerequisites: can the actor reach the retry route, choose the invoice ID, and receive the generated output? If any prerequisite is unknown, keep it unknown. If evidence shows that the route is unavailable to that actor, reconsider the claim; do not hide a hardening opportunity behind the rejected claim.
If the unsafe path is supported, open a ticket with the evidence and its current confidence, then have another reviewer challenge the explanation where useful. Do not delay a proportionate repair or containment just to obtain independent signoff; reserve “verified production issue” for the evidence that supports that claim. A controlled, authorized environment can provide stronger runtime evidence, but do not describe the source trace as a performed exploit. The remediation should protect the selected invoice at the authoritative read or delivery boundary, and the follow-up review should inspect all producers and download paths at a new revision. A changed function or disappeared alert alone is insufficient.
Next step: Pick one customer-owned object in your monorepo and fill the four-column table for its most sensitive action. If the difficult edge is a library used by several services, continue with the shared middleware review. If the map is larger than the available review window, use the scoping guide to declare what remains unreviewed.