Security Engineering · Threat Modeling

Keeping a Threat Model Current When the System Changes

How to bind threats to the artifacts that encode their assumptions, what invalidates a model silently, and how diffing architecture against model finds the stale entries before an audit does.

A threat model describes a system at a moment. The system continues to change, and the model does not, and the failure that follows is not a wrong finding but a wrong premise: the model still asserts that traffic between two services is authenticated because it was when the diagram was drawn, and the service was later moved behind a different listener that terminates authentication somewhere else. Nothing in the model is internally inconsistent. It is simply describing a system that no longer exists, and it will continue to describe it with full confidence.

Drift is a property of the assumptions, not the diagram

The diagram ages visibly, in the way the trust boundary analysis predicts. Boxes appear and disappear, and someone eventually notices that the drawing has three services where the deployment has five. The assumptions age invisibly, and they are where the analysis lives. A model’s findings depend on claims of the form “the queue is only writable by the API”, “the token is validated on every request”, “the backup bucket is not publicly readable”. Each claim was true when recorded, each is invisible in the drawing, and each becomes false through a change that does not look like a security change.

The structural fix is to make each assumption a first-class entry with a binding to something that can be checked. Three binding types are practical.

The first is a configuration assertion: a query against the deployed configuration that returns true or false. Bucket policy statements, security group rules, and IAM bindings are all expressible as assertions, and they are the assumptions most likely to drift silently because they are changed by people who are not thinking about the threat model.

The second is a code reference: a file and a symbol that implement the control. A claim that the daemon checks peer credentials is verifiable by reading the function that does it, and the reference becomes stale when the function is deleted or the call site is removed. This is the binding that catches the most dangerous drift, because deleting a check is a one-line change that no configuration diff will show.

The third is a test: an assertion that fails when the property is violated. Tests are the strongest binding because they are executed, and they are the weakest in coverage because a test can pass while the property it was meant to protect has moved elsewhere.

Diffing the model against the system

The maintenance procedure that works is a diff, run on a cadence and after any change that touches a boundary.

On the architecture side, the comparison is between the model’s element set and the system’s actual components. Infrastructure declarations, service manifests, and network policy are all machine-readable in systems that manage deployment declaratively, and the element set can be extracted and compared. The comparison finds elements the model lacks and elements the model has that no longer exist. The first category is the dangerous one, because a new element inherits none of the analysis, and a new element that crosses a boundary is exactly the case the model was built to reason about.

On the assumption side, the comparison is a re-evaluation of each assertion against its binding. Configuration assertions are evaluated directly. Code references are resolved to see whether the symbol still exists and is still called on the path the model claims. Tests are run. The output is a list of assumptions that are false, unverifiable, or whose binding has disappeared, and the third category is the one that predicts future drift: an assumption whose binding is gone will not be caught next time either.

A third comparison is less mechanical and more valuable: the flow set. New flows appear when a component starts talking to something it did not talk to before, and those flows are usually visible in network telemetry or in service mesh configuration rather than in the source. A flow that the model does not contain is a boundary crossing that has never been analyzed, and its existence is evidence that the change process did not consult the model.

Binding findings to elements so the diff has meaning

A model whose findings are a flat list cannot be diffed. When an element disappears, the findings attached to it become orphans, and nobody can tell whether they are resolved, transferred, or simply lost. The repair is to make the element the key of every finding, so that the finding’s identity is a pair of element and property violation.

With that structure, a diff produces four categories, each with a different action. Findings on removed elements are closed only if the element’s removal actually eliminated the exposure, which requires checking that nothing replaced it. Findings on changed elements are reopened for review, since the change may have invalidated the mitigation or the assumption behind it. Findings on unchanged elements persist without action. Elements with no findings are the interesting fourth category: a new element with an empty finding set is either genuinely benign or unanalyzed, and the model should record which.

The discipline that makes this work is refusing to let an element exist without an owner. An element with an owner has someone who answers when its findings are reopened, and the diff’s output is a set of questions addressed to specific people rather than a document nobody reads. That is the same argument that applies to the asset inventory, where an asset without a named party bearing the harm cannot be ranked; here it applies to the element without a named party answering for it.

Change classes that require a model pass

Not every change deserves a review; a review on every commit is a review on none. The classes that do deserve one are identifiable by what they touch.

A change to a trust boundary’s enforcement is the clearest case. Any modification to the code path that authenticates, authorizes, or validates at a crossing invalidates the assumption the crossing rests on, and the model’s findings on both sides of that crossing need re-examination. The change may be small in lines and total in effect.

A new data flow between existing elements is the second case, and it is the one most often missed because no new component appears. A service that begins reading from a store it previously only wrote to has created a flow, and if the store is on the other side of a boundary, the reading service has acquired an exposure it did not have. This is the flow diff described above, applied at change time rather than on a cadence.

A change to a credential’s scope is the third. Widening a role, adding a permission to a service account, or moving a secret into a shared configuration all change the authority ceiling of every process that holds it, and the elevation findings attached to those processes are invalidated at once. The change is a configuration edit with no code review, which is why it needs a different trigger than the code path.

A change to a dependency outside the boundary is the fourth, and the least visible. An upstream provider that changes its authentication model, a certificate authority that changes its validation rules, or a package that changes its default behavior all alter assumptions the model recorded about the environment. These are the assumptions most likely to be unbound, because there is no local artifact to assert them against, and the honest treatment is to record them as external with a stated review interval rather than to pretend they are checked.

The model as a reviewable artifact

The end state is a model that lives where the system lives and is reviewed with the system’s changes. Practically, that means the model is text rather than a drawing: elements, flows, assumptions, and findings in a format that diffs cleanly and that a review can annotate line by line. A drawing can be regenerated from the text; the reverse is not true, and a drawing alone cannot be compared to a previous version in any way that shows what changed.

The benefit is not process compliance. It is that the model’s claims become falsifiable in the same way the system’s behavior is, and a claim that can be falsified can be maintained. A model that records for every boundary which principal enforces it, for every asset who bears the harm, and for every assumption what would show it false, has the property that its staleness is detectable. That property, rather than the completeness of any single analysis, is what keeps the document worth reading a year after it was written.

References

  1. Threat Modeling: Designing for Security — Adam Shostack, Wiley, 2014
  2. NIST SP 800-160 Vol. 1 Rev. 1: Engineering Trustworthy Secure Systems — NIST, 2022
  3. ISO/IEC/IEEE 42010: Software, systems and enterprise — Architecture description — ISO/IEC/IEEE, 2022
  4. NIST SP 800-154 (Draft): Guide to Data-Centric System Threat Modeling — NIST, 2016

The archive date above places this article in the library, its archive and its feed. It is an assigned date for ordering, not a historical publication record.

#threat-model-maintenance#model-drift#assumption-tracking#architecture-as-code#review-process