<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Wsus on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/wsus/</link>
    <description>Recent content in Wsus on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 07:50:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/wsus/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Shared services for isolated tenants: PrivateTGW subnets</title>
      <link>https://thenestedlab.com/posts/shared-services-for-isolated-tenants/</link>
      <pubDate>Wed, 16 Sep 2026 07:50:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/shared-services-for-isolated-tenants/</guid>
      <description>Three pods with identical private addressing all need the same WSUS, repo and AD. One shared-services VPC with a PrivateTGW subnet serves all of them over the transit gateway — and can&amp;rsquo;t reach back into any of them. The directional test, and why SNAT is what makes it work.</description>
      <content:encoded><![CDATA[<p>The pods from <a href="/posts/three-datacenters-one-ip-plan/">the last post</a> are
perfectly isolated. That&rsquo;s the requirement — and immediately the problem.
Every one of them needs Windows updates, a package repo, DNS, maybe a
domain controller. Do I really run a WSUS <em>per pod</em>?</p>
<p>No. There&rsquo;s a third subnet access mode for exactly this, and the design it
enables is hub-and-spoke with a very specific property: <strong>spokes reach the
hub; the hub cannot reach the spokes; spokes never reach each other.</strong></p>
<h2 id="the-three-access-modes">The three access modes</h2>
<p>Everything in this series comes down to one field on a VPC subnet:</p>
<table>
	<thead>
			<tr>
					<th><code>accessMode</code></th>
					<th>Advertised to</th>
					<th>Use</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Private</code></td>
					<td>nobody outside the VPC</td>
					<td>workloads — isolation <em>and</em> overlapping CIDRs</td>
			</tr>
			<tr>
					<td><code>PrivateTGW</code></td>
					<td>every VPC attached to the org&rsquo;s transit gateway</td>
					<td>shared services</td>
			</tr>
			<tr>
					<td><code>Public</code></td>
					<td>the external network</td>
					<td>internet/corp-facing endpoints</td>
			</tr>
	</tbody>
</table>
<p><code>PrivateTGW</code> subnets draw their addresses from a <strong>separate transit
block</strong> (here <code>172.31.0.0/…</code>), not from the VPC&rsquo;s own <code>privateIPs</code>. That&rsquo;s
the key: the shared range can&rsquo;t collide with the pods&rsquo; <code>172.30.0.0/16</code>
because it comes from a different pool that all VPCs agree on.</p>
<h2 id="the-build">The build</h2>
<p>One more VPC, <code>shared-svc</code>, with the same ordering rules as any other
(VPC → VPCAttachment → LoadBalancer → namespace), then a single subnet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">crd.nsx.vmware.com/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Subnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w"> </span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sn-services}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w"> </span>{<span class="nt">accessMode</span><span class="p">:</span><span class="w"> </span><span class="nt">PrivateTGW, ipv4SubnetSize</span><span class="p">:</span><span class="w"> </span><span class="m">32</span>}<span class="w">
</span></span></span></code></pre></div><p>It realized as <code>172.31.0.0/27</code>, and a VM on it — <code>svc-repo01</code>,
<code>172.31.0.2</code>, serving HTTP — became the shared repo.</p>
<p><img alt="Architecture: VPC per pod, transit gateway, shared-services VPC" loading="lazy" src="/images/product-02-architecture.jpg"></p>
<h2 id="the-test-that-matters-is-directional">The test that matters is directional</h2>
<p>Reachability <em>to</em> the service is the easy claim. From <code>esx01</code> in each of
the three pods (ESXi ships python3, so <code>urllib</code> is the test client):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">pod-a esx01 -&gt; http://172.31.0.2/   200  shared-svc repo01
</span></span><span class="line"><span class="cl">pod-b esx01 -&gt; http://172.31.0.2/   200  shared-svc repo01
</span></span><span class="line"><span class="cl">pod-c esx01 -&gt; http://172.31.0.2/   200  shared-svc repo01
</span></span><span class="line"><span class="cl">default-vpc  -&gt; http://172.31.0.2/   200  shared-svc repo01
</span></span></code></pre></div><p>Three pods with <strong>identical source addresses</strong> (<code>172.30.0.40</code>) all hit
one service and all get answers. How does the reply find its way back to
the right pod when three of them claim <code>.40</code>? Because pod traffic crosses
the transit gateway <strong>SNAT&rsquo;d to a per-VPC transit address</strong>. The service
never sees <code>172.30.0.40</code>; it sees three distinct transit IPs. Ambiguity
never arises.</p>
<p>Now the other direction — from <code>svc-repo01</code> back toward a pod:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">svc-repo01 -&gt; 172.30.0.40   unreachable
</span></span><span class="line"><span class="cl">svc-repo01 -&gt; 172.30.0.41   unreachable
</span></span></code></pre></div><p>Not &ldquo;blocked&rdquo; — <em>unroutable</em>. Pod subnets are <code>Private</code>, so they were never
advertised to the transit gateway, and even if they had been, <code>172.30.0.40</code>
would be ambiguous across three VPCs. The hub literally cannot initiate
into a spoke. For a shared service that will one day be compromised,
that&rsquo;s the property you want.</p>
<h2 id="what-goes-in-the-hub">What goes in the hub</h2>
<p>Anything that&rsquo;s <em>consumed</em> by pods and <em>stateless about which pod is
asking</em>: WSUS/patch mirrors, OS and package repos, container registries,
NTP, DNS forwarders, license servers. Domain controllers work too, with
the usual caveat that identical hostnames across pods need per-pod domains
or a naming scheme.</p>
<p>What does <strong>not</strong> go in the hub: anything that needs to <em>reach into</em> a pod
(monitoring pollers, backup agents pulling, jump hosts). Those either live
in the pod, or the pod publishes a <code>LoadBalancer</code> VIP for them — the
deliberate door from <a href="/posts/whats-a-vpc-with-pacman/">part 0</a>.</p>
<h2 id="tightening-further">Tightening further</h2>
<p>The transit gateway gives you reachability; policy gives you precision. A
<code>VPCGatewayFirewallPolicy</code> on <code>shared-svc</code> can restrict inbound to
<code>tcp/80,443</code> from the transit range and nothing else, so the repo is a
repo and not a foothold. I left it open for the test; you shouldn&rsquo;t.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>This is the pattern that makes isolated tenants <em>affordable</em>. Without it,
every isolated environment needs its own patch server, repository, DNS and
directory — cost and drift that quietly kill the idea. With it, a customer
runs one set of shared services for dozens of tenants, keeps them patched in
one place, and can still show a security reviewer that the shared service
has no path back into any tenant.</p>
<p>The same hub serves well beyond patching: central logging and monitoring
collectors, licence servers, artifact registries, build agents — anything
tenants consume but shouldn&rsquo;t be able to be reached <em>by</em>.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li><code>PrivateTGW</code> is the shared-services mode: addresses from the <strong>transit
block</strong>, advertised to every attached VPC, no collision with pod space.</li>
<li>Access is <strong>one-way by construction</strong>: pods → service works (SNAT&rsquo;d
per VPC), service → pod has no route. Test both directions and write
down both results.</li>
<li>Identical pod addressing and shared services coexist <em>because</em> of the
SNAT — the hub sees per-VPC transit addresses, never the overlapping
private ones.</li>
<li>Same ordering rules apply to the hub VPC (VPC → attachment → LB →
namespace → subnets → wait for image sync → workloads).</li>
<li>Add a gateway firewall policy on the hub. Reachability is not
authorisation.</li>
</ul>
<p><em>Previously: <a href="/posts/three-datacenters-one-ip-plan/">three datacenters, one IP plan</a>.
Next: the whole pod as a single catalog item.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Output captured live, trimmed for length,
never edited for outcome.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
