<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Egress on Brewed in the Cloud by Chris Hailes</title><link>https://blog.brewedinthecloud.com/tags/egress/</link><description>Recent content in Egress on Brewed in the Cloud by Chris Hailes</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 12 Nov 2026 00:00:00 +1100</lastBuildDate><atom:link href="https://blog.brewedinthecloud.com/tags/egress/rss.xml" rel="self" type="application/rss+xml"/><item><title>Private Access with Explicit Egress and Exfiltration Guardrails</title><link>https://blog.brewedinthecloud.com/p/exposure-private-access-egress-guardrails/</link><pubDate>Thu, 12 Nov 2026 00:00:00 +1100</pubDate><guid>https://blog.brewedinthecloud.com/p/exposure-private-access-egress-guardrails/</guid><description>&lt;p&gt;The application reads from private Storage. The dependency works, the endpoint has a private address, and the public-access setting has been reviewed.&lt;/p&gt;
&lt;p&gt;One question is still unanswered: where else can this workload write?&lt;/p&gt;
&lt;p&gt;The earlier &lt;a class="link" href="https://blog.brewedinthecloud.com/p/pe-attacks-exfiltration/" &gt;Private Link exfiltration article&lt;/a&gt; explains why private access is still a trust relationship. Here we put that argument to work: constrain one unwanted destination operation without breaking the approved dependency.&lt;/p&gt;
&lt;p&gt;I would test both sides of that decision. Can the workload still read where it should? Can it still write where it should not? &amp;ldquo;Private-only&amp;rdquo; does not answer either question on its own.&lt;/p&gt;
&lt;h2 id="the-mental-model"&gt;The Mental Model
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;A destination-bound operation needs more context than a private IP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Name the service instance and operation first. Then record the subresource, resolver, route and principal that make it work. &amp;ldquo;Access to Storage&amp;rdquo; is too broad to test usefully.&lt;/p&gt;
&lt;p&gt;For the illustrative exercise, account A holds synthetic objects the application should read. Account B is another author-owned destination the application should not write to. Both use Blob on supported general-purpose v2 Storage accounts. That gives us a safe comparison without sending data to a third party.&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart LR
W[Application workload] --&gt; D[Source DNS context]
D --&gt; A[Approved Blob endpoint]
D --&gt; B[Controlled unapproved endpoint]
I[Effective service authority] --&gt; A
I --&gt; B
C[Network and service controls] --&gt; T[Repeatable operation tests]
A --&gt; T
B --&gt; T
T --&gt; R[Residual and recovery record]
&lt;/div&gt;
&lt;p&gt;Keep the conclusion as specific as the test: this source, this identity, these operations and destinations. The exercise cannot establish that no data can leave the organization.&lt;/p&gt;
&lt;h2 id="how-it-really-works"&gt;How It Really Works
&lt;/h2&gt;&lt;h3 id="preserve-the-service-name-and-inspect-resolution"&gt;Preserve the service name and inspect resolution
&lt;/h3&gt;&lt;p&gt;Microsoft documents that Storage applications keep the normal service connection string and authorization mechanisms when using Private Endpoints. Do not replace the application endpoint with the &lt;code&gt;privatelink&lt;/code&gt; subdomain as a shortcut.&lt;/p&gt;
&lt;p&gt;From the source workload, inspect the normal Blob name and TCP reachability:&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;$account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;approved-lab-storage-account&amp;gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;$serviceName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$account&lt;/span&gt;&lt;span class="s2"&gt;.blob.core.windows.net&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;Resolve-DnsName&lt;/span&gt; &lt;span class="n"&gt;-Name&lt;/span&gt; &lt;span class="nv"&gt;$serviceName&lt;/span&gt; &lt;span class="n"&gt;-ErrorAction&lt;/span&gt; &lt;span class="n"&gt;Stop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;Test-NetConnection&lt;/span&gt; &lt;span class="n"&gt;-ComputerName&lt;/span&gt; &lt;span class="nv"&gt;$serviceName&lt;/span&gt; &lt;span class="n"&gt;-Port&lt;/span&gt; &lt;span class="mf"&gt;443&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-InformationLevel&lt;/span&gt; &lt;span class="n"&gt;Detailed&lt;/span&gt;
&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;The checks explain resolution and TCP connectivity; they do not authenticate to Storage or perform the data operation. Inspect &lt;code&gt;TcpTestSucceeded&lt;/code&gt; explicitly and retain the resolver context and timestamp. If DNS fails, investigate it. Substituting a cached address would change the experiment while hiding the failure.&lt;/p&gt;
&lt;p&gt;Repeat resolution for B and the separately approved recovery context. A central resolver may make the name visible to more workloads than intended. That is worth investigating, but a resolvable name is not yet a writable destination.&lt;/p&gt;
&lt;h3 id="separate-public-and-private-access"&gt;Separate public and private access
&lt;/h3&gt;&lt;p&gt;Creating a Storage Private Endpoint does not automatically block the public endpoint. Microsoft&amp;rsquo;s documentation treats public endpoint access controls separately; the Storage firewall governs public access rather than granting the private path&amp;rsquo;s data permission.&lt;/p&gt;
&lt;p&gt;Inspect actual public network settings and authorized private connection state. Test the public endpoint from a separately declared public context, not from a resolver that transparently sends the same hostname to a private IP.&lt;/p&gt;
&lt;p&gt;NSP does not turn this into a universal private-operation boundary. Current documentation allows Private Endpoint traffic without explicit NSP access rules. Similarly, AVNM documentation states security admin rules do not apply to Private Endpoints. Choose and verify the relevant supported source/path controls instead of assuming either product closes the private path.&lt;/p&gt;
&lt;h3 id="test-the-operation-the-finding-represents"&gt;Test the operation the finding represents
&lt;/h3&gt;&lt;p&gt;Use a precreated synthetic object and the declared workload test principal for an approved read. If the unintended consequence is a write, baseline and post-change tests must actually exercise a harmless synthetic write against B, not substitute a TCP check.&lt;/p&gt;
&lt;p&gt;The &lt;a class="link" href="https://blog.brewedinthecloud.com/azure-exposure-reduction-toolkit/#4-controlled-egress-and-residual-risk" &gt;draft toolkit&amp;rsquo;s controlled-egress module&lt;/a&gt; provides the worksheet:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Intended post-change result&lt;/th&gt;
&lt;th&gt;Evidence to retain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Workload reads synthetic object from A&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Principal, request ID, service result, resolver and path context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workload writes synthetic object to B&lt;/td&gt;
&lt;td&gt;Rejected at the chosen boundary&lt;/td&gt;
&lt;td&gt;Service/network error and corroborating control evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declared public context accesses protected endpoint&lt;/td&gt;
&lt;td&gt;Result matches public-access claim&lt;/td&gt;
&lt;td&gt;Public resolution, source context and service result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source sends telemetry and permits authorized recovery access&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Ingestion/access evidence, not only configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approved downstream service behavior&lt;/td&gt;
&lt;td&gt;Explicitly allowed or out of scope&lt;/td&gt;
&lt;td&gt;Owner, operation and residual-risk statement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If B was not reachable and writable before the change, the test cannot demonstrate removal of that operation. It can establish a current boundary only within the declared conditions.&lt;/p&gt;
&lt;h2 id="real-world-impact"&gt;Real-World Impact
&lt;/h2&gt;&lt;p&gt;The review can now move past &amp;ldquo;Private Endpoint deployed.&amp;rdquo; The engineer has two results to defend: the dependency survived, and the unwanted operation was constrained.&lt;/p&gt;
&lt;h3 id="choose-which-layer-should-reject-the-operation"&gt;Choose which layer should reject the operation
&lt;/h3&gt;&lt;p&gt;If the workload never needs authority over B, removing its data permission may be a direct remediation. If many identities can reach an unwanted destination set, supported network restrictions might also reduce opportunity.&lt;/p&gt;
&lt;p&gt;Using both layers may be sensible. Just keep their claims separate. A network failure cannot tell you whether the role change worked, and an authorization denial cannot tell you whether traffic followed the inspection route. Otherwise the team can approve two changes using evidence for only one.&lt;/p&gt;
&lt;h3 id="pay-for-the-constraint-you-can-operate"&gt;Pay for the constraint you can operate
&lt;/h3&gt;&lt;p&gt;Central egress inspection, narrower DNS links and destination inventories create ongoing work. TLS inspection can add certificate and troubleshooting overhead. Destination-specific permissions can increase application onboarding effort.&lt;/p&gt;
&lt;p&gt;I would accept those costs where they constrain a high-consequence path. I would not hide them behind &amp;ldquo;private is more secure.&amp;rdquo; The owner needs to know what happens when a destination changes, a certificate expires or the application acquires another dependency.&lt;/p&gt;
&lt;h2 id="gotchas-and-edge-cases"&gt;Gotchas and Edge Cases
&lt;/h2&gt;&lt;p&gt;A failed request is tempting evidence when you want the fix to be finished. These cases are the reason to ask one more question before accepting it.&lt;/p&gt;
&lt;h3 id="a-timeout-can-mimic-containment"&gt;A timeout can mimic containment
&lt;/h3&gt;&lt;p&gt;Broken DNS, routing, credentials or a stopped destination can all produce a failed operation. Require the approved positive control and control-attributed rejection before claiming a deliberate path break.&lt;/p&gt;
&lt;h3 id="storage-subresources-are-not-interchangeable"&gt;Storage subresources are not interchangeable
&lt;/h3&gt;&lt;p&gt;Blob, DFS, Files and other service endpoints have different requirements. Microsoft documents that Data Lake Storage operations can require both DFS and Blob endpoints. This illustrative exercise is Blob-only; expanding it requires a revised test matrix and current service documentation.&lt;/p&gt;
&lt;h3 id="service-side-movement-can-extend-the-boundary"&gt;Service-side movement can extend the boundary
&lt;/h3&gt;&lt;p&gt;An approved write can feed replication, export or a downstream consumer. Do not infer that a workload egress control constrains every subsequent service-side operation. Record the onward behavior as allowed, constrained, unknown or explicitly outside the test.&lt;/p&gt;
&lt;h3 id="recovery-can-use-another-path"&gt;Recovery can use another path
&lt;/h3&gt;&lt;p&gt;A responder may need a different source, principal or regional dependency. Test the actual authorized recovery operation separately. A diagram that preserves a management arrow is not evidence that responders can use it.&lt;/p&gt;
&lt;h2 id="best-practices"&gt;Best Practices
&lt;/h2&gt;&lt;p&gt;Start with a small destination/operation matrix. Keep source, principal and resolver comparable between baseline and post-change tests, use fresh sessions, and record any deliberate differences.&lt;/p&gt;
&lt;h3 id="assumptions-and-evidence-boundary"&gt;Assumptions and evidence boundary
&lt;/h3&gt;&lt;p&gt;The exercise uses synthetic data and author-owned destinations; it has not been run as an Azure lab. Before execution, record the supported Storage account and Blob subresource; actual Azure cloud/region; DNS resolver and zone-link model; Private Endpoint network-policy/routing configuration if path filtering is used; public-access settings; Firewall tier/rule schema where applicable; CLI and service API versions; scoped data permissions; and available diagnostic categories/tables. ARM read access is not permission to perform the data test.&lt;/p&gt;
&lt;p&gt;Keep resource configuration, documented behavior and observed operation results in separate evidence fields. That lets the reviewer see what the platform promises and what the test actually established.&lt;/p&gt;
&lt;p&gt;I would not approve the blocked write if the same change breaks the approved read, telemetry or recovery operation. Stop rollout when a required positive control fails. If rollback reopens B, record that residual plainly: service restored, remediation unfinished.&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; Private-access assurance is destination and operation assurance. Prove what still works, what no longer works, and what happens after the first service accepts the data.
&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/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/private-link/private-endpoint-dns" target="_blank" rel="noopener"
&gt;Private Endpoint DNS configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/storage/common/storage-network-security" target="_blank" rel="noopener"
&gt;Azure Storage network security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://learn.microsoft.com/azure/private-link/network-security-perimeter-concepts" target="_blank" rel="noopener"
&gt;Network Security Perimeter concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://blog.brewedinthecloud.com/p/pe-attacks-exfiltration/" &gt;Private Link Doesn&amp;rsquo;t Stop Exfiltration. It Just Changes the Route&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>