<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Engineering-Exposure-Reduction on Brewed in the Cloud by Chris Hailes</title><link>https://blog.brewedinthecloud.com/tags/engineering-exposure-reduction/</link><description>Recent content in Engineering-Exposure-Reduction on Brewed in the Cloud by Chris Hailes</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 03 Nov 2026 00:00:00 +1100</lastBuildDate><atom:link href="https://blog.brewedinthecloud.com/tags/engineering-exposure-reduction/rss.xml" rel="self" type="application/rss+xml"/><item><title>From Finding to Path: Model Azure Exposure as Reachability</title><link>https://blog.brewedinthecloud.com/p/exposure-finding-to-path-reachability/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +1100</pubDate><guid>https://blog.brewedinthecloud.com/p/exposure-finding-to-path-reachability/</guid><description>&lt;p&gt;A finding can land at the top of the queue and still leave the engineer guessing.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Restrict access to this storage account&amp;rdquo; sounds actionable. But access from where, using which identity, to do what? Without those answers, two teams can implement different fixes and both reasonably believe they have finished.&lt;/p&gt;
&lt;p&gt;The earlier &lt;a class="link" href="https://blog.brewedinthecloud.com/p/prioritizing-external-risk-with-attack-path-context/" &gt;attack-path prioritization article&lt;/a&gt; explains why consequence should outrank finding volume. This series starts after that decision: what exactly should change, and how will we know the change worked?&lt;/p&gt;
&lt;p&gt;I would not hand that finding to engineering until we can name the path and explain how the fix could fail its test. That adds work to triage. It also avoids the more expensive conversation later: the setting changed, so why is the unwanted operation still possible?&lt;/p&gt;
&lt;h2 id="the-mental-model"&gt;The Mental Model
&lt;/h2&gt;&lt;p&gt;Start with what the compromised workload can do, not where it sits on the diagram. An exposure path connects that capability to usable authority, a network path, a destination operation and its consequence.&lt;/p&gt;
&lt;p&gt;Consider an application spoke whose workload needs to read objects from an approved private Blob destination. A second controlled destination represents somewhere it should not write. DNS, telemetry and responder access must keep working when we constrain that second path. This illustrative scenario runs through the series; the testing assumptions are collected below.&lt;/p&gt;
&lt;p&gt;Can the workload connect to the second account? That is one question. Can its identity actually write there? That is another. And if we block the connection, can the workload or its deployment pipeline change a role assignment, public access setting or route to reopen it?&lt;/p&gt;
&lt;p&gt;Those questions belong in the same record. Otherwise we risk closing the door while leaving the means to unlock it untouched.&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart LR
S[Source workload capability] --&gt; I[Effective identity authority]
S --&gt; N[DNS and network path]
I --&gt; O[Destination operation]
N --&gt; O
O --&gt; C[Reachable consequence]
M[Control-plane change authority] -. can reopen .-&gt; N
M -. can widen .-&gt; I
R[Recovery and telemetry dependencies] --&gt; V[Validation boundary]
&lt;/div&gt;
&lt;p&gt;The diagram expresses dependencies, not an Azure packet-processing order. A useful record names which edge a proposed change will constrain and which edges remain.&lt;/p&gt;
&lt;h2 id="how-it-really-works"&gt;How It Really Works
&lt;/h2&gt;&lt;h3 id="make-the-finding-an-engineering-hypothesis"&gt;Make the finding an engineering hypothesis
&lt;/h3&gt;&lt;p&gt;Give the path a stable ID, then describe it plainly: this workload, using this principal, can perform this operation against this service instance. Add the DNS answer, route and authorization scope that make the operation possible. Include who can change those conditions, not just who owns the account.&lt;/p&gt;
&lt;p&gt;Then write a claim that can fail:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;After change C, source S using principal P cannot perform operation O against controlled destination D; the approved dependency and responder path still work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is less impressive than &amp;ldquo;the account is secure.&amp;rdquo; It is also something an engineer can challenge, implement and test.&lt;/p&gt;
&lt;p&gt;The &lt;a class="link" href="https://blog.brewedinthecloud.com/azure-exposure-reduction-toolkit/#1-path-record-and-read-only-inventory" &gt;path-record module in the draft Azure Exposure Reduction Toolkit&lt;/a&gt; provides the technical companion. Its minimum fields are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Decision it supports&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Path ID and scope version&lt;/td&gt;
&lt;td&gt;Comparing the same case across changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source, principal, destination, operation&lt;/td&gt;
&lt;td&gt;Establishing which capability is being constrained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS, route and enforcement context&lt;/td&gt;
&lt;td&gt;Identifying where the network claim applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role scope and control-plane owner&lt;/td&gt;
&lt;td&gt;Finding authority that can preserve or reopen access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected result and evidence references&lt;/td&gt;
&lt;td&gt;Separating a hypothesis from a supported conclusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive controls, residuals and retest triggers&lt;/td&gt;
&lt;td&gt;Preserving service and avoiding permanent assurance claims&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Inventory gives you a starting point, not a verdict. This read-only Azure Resource Graph query identifies declared Private Endpoint targets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;resources
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| where type =~ &amp;#34;microsoft.network/privateendpoints&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| mv-expand connection = properties.privateLinkServiceConnections
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| project endpointId = id,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subnetId = tostring(properties.subnet.id),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; targetId = tostring(connection.properties.privateLinkServiceId),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; groupIds = connection.properties.groupIds,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; location
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Run it in Resource Graph, not a Log Analytics workspace. It covers the automatic connection property shown; manually requested connections use a different property and need separate inspection. Empty output may mean missing scope, permissions, delayed inventory or no matching resources. It does not mean no usable path exists.&lt;/p&gt;
&lt;h3 id="test-authority-and-reachability-separately"&gt;Test authority and reachability separately
&lt;/h3&gt;&lt;p&gt;Run the checks where the workload runs. A successful CLI call from an administrator&amp;rsquo;s laptop may use a different resolver, route and identity. It answers a useful question, but not necessarily the one in the finding.&lt;/p&gt;
&lt;p&gt;Use synthetic objects and scoped data permissions. The baseline must include an authorized, harmless operation that succeeds against the controlled unintended destination if the claim is that this path currently exists. If baseline access fails, investigate why; do not celebrate the same failure after the change as newly achieved reduction.&lt;/p&gt;
&lt;p&gt;After a single approved lab change, repeat the identical operation, approved dependency probe and responder-access check. Preserve request identifiers, timestamps and configuration versions. A negative result is convincing only when the positive controls still work and the rejection can be attributed to the intended boundary.&lt;/p&gt;
&lt;h2 id="real-world-impact"&gt;Real-World Impact
&lt;/h2&gt;&lt;p&gt;The useful handoff is no longer &amp;ldquo;secure Storage.&amp;rdquo; It is &amp;ldquo;remove this write capability without breaking that read dependency.&amp;rdquo; Platform, identity and application owners can then divide the work without pretending one team owns every part of the path.&lt;/p&gt;
&lt;h3 id="choose-the-smaller-defensible-change"&gt;Choose the smaller defensible change
&lt;/h3&gt;&lt;p&gt;Suppose the application needs reads from account A but has inherited write authority to account B. Tightening the data role may be a smaller change than introducing a new centralized inspection dependency. Conversely, network restriction may be valuable where many identities share a reachable management surface.&lt;/p&gt;
&lt;p&gt;I would compare those changes by the capability they remove and the dependency they preserve, not by how much infrastructure they introduce. The record gives the reviewer a way to make that choice and see what remains.&lt;/p&gt;
&lt;h3 id="leave-responders-something-useful"&gt;Leave responders something useful
&lt;/h3&gt;&lt;p&gt;A path record should survive the remediation ticket. During an incident, it tells responders which dependency they can constrain, which identity scope matters, and which recovery operation must remain possible.&lt;/p&gt;
&lt;p&gt;That durability is worth the documentation burden when consequence is high. For a low-value isolated endpoint, a shorter record may be proportionate. The method should improve a decision, not create an exhaustive graph project before every change.&lt;/p&gt;
&lt;h2 id="gotchas-and-edge-cases"&gt;Gotchas and Edge Cases
&lt;/h2&gt;&lt;p&gt;A neat record can still describe the wrong path. The most important checks are the ones that catch a convincing explanation before it becomes a closure decision.&lt;/p&gt;
&lt;h3 id="reachable-does-not-mean-authorized"&gt;Reachable does not mean authorized
&lt;/h3&gt;&lt;p&gt;A successful TCP connection establishes transport, not Blob write authority. A service rejection might reflect authentication, authorization or a network policy; retain service error details rather than assigning every 403 to the firewall.&lt;/p&gt;
&lt;h3 id="a-snapshot-can-miss-reopening-authority"&gt;A snapshot can miss reopening authority
&lt;/h3&gt;&lt;p&gt;The principal used by the workload may not itself edit networking, while its deployment pipeline can. Record the separate automation identity and who can change that pipeline. The relevant authority graph includes deployment and recovery operations.&lt;/p&gt;
&lt;h3 id="context-changes-invalidate-comparison"&gt;Context changes invalidate comparison
&lt;/h3&gt;&lt;p&gt;Changing resolver, test principal, destination subresource or token state between runs changes the experiment. Cached tokens and sessions require explicit handling. Use fresh connections and record token-acquisition context without recording tokens.&lt;/p&gt;
&lt;h3 id="unknown-paths-are-not-blocked-paths"&gt;Unknown paths are not blocked paths
&lt;/h3&gt;&lt;p&gt;If role visibility or diagnostic access is incomplete, mark the path unknown. Removing an unknown record from the denominator does not improve security. It only improves the appearance of the report.&lt;/p&gt;
&lt;h2 id="best-practices"&gt;Best Practices
&lt;/h2&gt;&lt;p&gt;Start with the highest-consequence finding that has a controllable test boundary. Define one source, one principal, one destination operation and one proposed intervention. Expand only after the first record can be reviewed and reproduced.&lt;/p&gt;
&lt;h3 id="assumptions-and-evidence-boundary"&gt;Assumptions and evidence boundary
&lt;/h3&gt;&lt;p&gt;The proposed exercise uses synthetic objects and author-owned Blob destinations on supported general-purpose v2 Storage accounts. Service data access is separate from management-plane read access. Resource Graph needs visibility over the queried resources; the operation needs the appropriate scoped data role. Confirm CLI version, effective-route inspection support, resource API versions, region availability and diagnostic schema for the selected topology before deployment.&lt;/p&gt;
&lt;p&gt;Microsoft documents Private Endpoint connectivity and unchanged service authorization mechanisms. This article proposes a validation exercise; it does not report Azure lab results. Keep those evidence categories separate in the final record.&lt;/p&gt;
&lt;p&gt;I would close this as a removed path only when the baseline existed, the unwanted operation fails at the intended boundary, required service still works, and reopening authority is accounted for. If one of those is missing, call it a partial change or an inconclusive test. That is a useful engineering result too; it just is not the result we set out to prove.&lt;/p&gt;
&lt;div class="insight"&gt;
&lt;div class="insight-icon"&gt;🍺&lt;/div&gt;
&lt;div class="insight-content"&gt;
&lt;strong&gt;Brewed Insight:&lt;/strong&gt; A good remediation claim names the capability that disappears and the dependency that survives. A resource setting without those two statements is still an implementation task, not evidence of reduced consequence.
&lt;/div&gt;
&lt;/div&gt;
&lt;style&gt;
.insight {
display: flex;
align-items: center;
background-color: #0089e41c;
border-left: 10px solid #D69A2D;
padding: 10px;
margin: 20px 0;
border-radius: 4px;
}
.insight-icon {
font-size: 24px;
margin-right: 10px;
}
.insight-content {
flex: 1;
}
&lt;/style&gt;&lt;h2 id="learn-more"&gt;Learn More
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/governance/resource-graph/overview" target="_blank" rel="noopener"
&gt;Azure Resource Graph overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/storage/common/storage-private-endpoints" target="_blank" rel="noopener"
&gt;Use private endpoints for Azure Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/role-based-access-control/overview" target="_blank" rel="noopener"
&gt;Azure RBAC overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.brewedinthecloud.com/p/prioritizing-external-risk-with-attack-path-context/" &gt;Prioritizing External Risk with Attack Path Context&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>