---
title: 'Network Isolation and Private Endpoints'
source: 'https://academia.sh/en/courses/cloud-governance/network-isolation-and-private-endpoints'
course: 'Cloud Governance, Security and Cost'
language: en
updated: '2026-09-13T05:07:36+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts the paths to a managed service and measures how many paths remain per access once the public endpoint is closed'
    - 'Separates how name resolution changes by scenario: the same name resolving to a private or public address decides access independently of the path'
    - "Counts broken legitimate access by location and derives the private endpoint's cost per service and per network"
---

# Network Isolation and Private Endpoints

Measuring whether a managed service is reached through the public endpoint or a private one: closing the public endpoint on 25 access records breaks 12 legitimate ones, drops 12 to zero paths, moves the same name from resolving to a private address in 11 places to 14, and closing the gaps costs 12 new private endpoints at 180 fictional units a month.

The previous lesson read whether a resource is open to public access with a single flag: Y1 fired
in eight places, six got flagged. That flag is a resource's own setting. This lesson's question
sits one stage earlier — when a workload reaches a managed service, which path does the packet
take? Network isolation and private connectivity themselves were measured in M23/K02; that is not
repeated here. The object here is individually rented services: does access to an object store, a
queue, or a secret storage service go through the service's **public endpoint**, or through a
**private endpoint** placed inside the private network?

The reason this distinction is measurable is this: a managed service comes by default with a
public name and a public address. A private endpoint changes two things at once — it adds a new
path to the network, and it changes the address the name resolves to. Each of the two can break
separately, and this lesson counts them separately.

## Service, Network, and Access Table

The example course is the fictional **regional measurement network**. **CS31 — managed service
catalog:** seven services; six support a private endpoint, one only offers a public endpoint.
Every supported service has a monthly private-endpoint rent (fictional unit). **CS32 — three
private networks and a single peering record:** the production and staging networks are peered,
the shared network is not. **CS33 — installed private endpoints:** all six of six services are
installed only in the production network. **CS34 — name-resolution zone:** the zone carrying the
private records is attached to the production and shared networks, not to the staging network.
**CS35 — access table:** twenty-five access records; twenty-two are legitimate, three are not,
three can be converted into a jump point. **CS36 — three scenarios:** A public endpoint open, B
public endpoint closed, C public endpoint closed and gaps closed.

The file below builds these tables and runs the three scenarios. It is a process-internal model;
no real network, name record, or service exists.

```js
// isolation/endpoint.mjs — whether a managed service is reached through the public endpoint or a private one.
// Process-internal model: the regional measurement network (fictional); no real network, name record, or service exists.

const parseRows = (s) => s.trim().split("\n").map((l) => l.trim().split("|"));
const row = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

// Managed service: name|private endpoint support|private endpoint monthly rent (fictional unit)
const SERVICE = Object.fromEntries(parseRows(`
  object-store|1|18
  relational-store|1|18
  queue|1|14
  secret-storage|1|12
  key-management|1|12
  log-collection|1|16
  notification-service|0|0`).map(([name, d, k]) => [name, { supports: d === "1", rent: +k }]));

// Private networks and peering (model input). shared-network is not peered.
const NETWORK = ["production-network", "staging-network", "shared-network"];
const PEERING = [["production-network", "staging-network"]];
const peered = (a, b) => a === b || PEERING.some(([x, y]) =>
  (x === a && y === b) || (x === b && y === a));

// Installed private endpoints: service|network. All six of six services are only in the production network.
const INSTALLED_A = parseRows(`
  object-store|production-network
  relational-store|production-network
  queue|production-network
  secret-storage|production-network
  key-management|production-network
  log-collection|production-network`).map((x) => x.join("|"));

// Networks the name-resolution zone is attached to: staging-network is not attached.
const ZONE_A = ["production-network", "shared-network"];

// Access: client|service|location (network or external)|legit|can be converted into a jump point
const ACCESS = parseRows(`
  measurement-collector|object-store|production-network|1|0
  measurement-collector|queue|production-network|1|0
  stream-processor|queue|production-network|1|0
  stream-processor|relational-store|production-network|1|0
  stream-processor|key-management|production-network|1|0
  catalog-service|relational-store|production-network|1|0
  catalog-service|secret-storage|production-network|1|0
  gateway-service|log-collection|production-network|1|0
  gateway-service|notification-service|production-network|1|0
  staging-runner|object-store|staging-network|1|0
  staging-runner|relational-store|staging-network|1|0
  staging-runner|queue|staging-network|1|0
  shared-reporter|object-store|shared-network|1|0
  shared-reporter|relational-store|shared-network|1|0
  shared-reporter|log-collection|shared-network|1|0
  management-tool|relational-store|external|1|1
  management-tool|secret-storage|external|1|1
  management-tool|key-management|external|1|1
  nightly-job|object-store|external|1|0
  nightly-job|queue|external|1|0
  edge-uploader|object-store|external|1|0
  edge-uploader|notification-service|external|1|0
  developer-laptop|object-store|external|0|0
  developer-laptop|relational-store|external|0|0
  unknown-client|object-store|external|0|0`)
  .map(([client, service, location, legit, jump]) =>
    ({ client, service, location, legit: legit === "1", jump: jump === "1" }));

// The paths of one access record and its name resolution. publicOpen: is the service's public endpoint still open.
const measure = (e, { installed, zone, publicOpen, jumpAvailable }) => {
  const hasPrivate = (net) => installed.includes(`${e.service}|${net}`);
  const privatePath = e.location !== "external" && NETWORK.some((a) => hasPrivate(a) && peered(a, e.location));
  const publicPath = publicOpen(e.service);
  // Name resolution: seen from a network the internal zone is attached to, with a record for that service, resolves private
  const recordExists = NETWORK.some((a) => hasPrivate(a));
  const resolution = e.location !== "external" && zone.includes(e.location) && recordExists ? "private" : "public";
  // Jump point: an external client passes through the internal network, the name resolves from the internal zone
  const jumpPath = jumpAvailable && e.jump && recordExists;
  const paths = [];
  if (privatePath) paths.push("private");
  if (publicPath) paths.push("public");
  if (jumpPath) paths.push("jump");
  const works = (resolution === "private" && privatePath) || (resolution === "public" && publicPath) || jumpPath;
  const status = works
    ? (jumpPath && !(resolution === "private" && privatePath) && !(resolution === "public" && publicPath)
        ? "jump" : resolution === "private" ? "private endpoint" : "public endpoint")
    : (resolution === "private" ? "name private, no path" : "name public, endpoint closed");
  return { paths: paths.length, resolution, works, status };
};

// Three scenarios
const supportsPrivate = (h) => SERVICE[h].supports;
const INSTALLED_C = NETWORK.flatMap((a) => Object.keys(SERVICE).filter(supportsPrivate).map((h) => `${h}|${a}`));
const SCENARIO = {
  "A public endpoint open": { installed: INSTALLED_A, zone: ZONE_A, jumpAvailable: false,
    publicOpen: () => true },
  "B public endpoint closed": { installed: INSTALLED_A, zone: ZONE_A, jumpAvailable: false,
    publicOpen: (h) => !SERVICE[h].supports },
  "C gaps closed": { installed: INSTALLED_C, zone: NETWORK, jumpAvailable: true,
    publicOpen: (h) => !SERVICE[h].supports },
};

const S = [-27, 10, 12, 10, 16, 10, 15];
row(S, "scenario", "private", "public", "jump", "broken legit", "cut", "from internet");
const R = {};
for (const [name, s] of Object.entries(SCENARIO)) {
  const c = ACCESS.map((e) => ({ e, ...measure(e, s) }));
  const count = (d) => c.filter((x) => x.status === d).length;
  R[name] = {
    priv: count("private endpoint"), pub: count("public endpoint"), jump: count("jump"),
    broken: c.filter((x) => !x.works && x.e.legit).length,
    cut: c.filter((x) => !x.works && !x.e.legit).length,
    internet: c.filter((x) => x.works && x.status === "public endpoint").length,
    c,
  };
  row(S, name, R[name].priv, R[name].pub, R[name].jump, R[name].broken, R[name].cut, R[name].internet);
}

// Name resolution: which address the same name resolves to in each scenario
const C = [-27, 18, 17];
console.log();
row(C, "scenario", "to private addr", "to public addr");
for (const name of Object.keys(SCENARIO))
  row(C, name, R[name].c.filter((x) => x.resolution === "private").length,
    R[name].c.filter((x) => x.resolution === "public").length);

// How many paths remain once the public endpoint closes: distribution of path count per access
const Y = [-27, 9, 9, 9, 14];
console.log();
row(Y, "scenario", "0 paths", "1 path", "2 paths", "total access");
for (const name of Object.keys(SCENARIO)) {
  const d = [0, 1, 2].map((n) => R[name].c.filter((x) => x.paths === n).length);
  row(Y, name, ...d, ACCESS.length);
}

// Why legitimate access breaks in scenario B
const B = [-31, 10];
console.log();
row(B, "B: break reason", "access");
for (const n of ["name private, no path", "name public, endpoint closed"])
  row(B, n, R["B public endpoint closed"].c.filter((x) => !x.works && x.e.legit
    && x.status === n).length);
const locations = [...new Set(ACCESS.map((e) => e.location))];
const KN = [-27, 10];
console.log();
row(KN, "B: broken legit, location", "access");
for (const k of locations) {
  const n = R["B public endpoint closed"].c.filter((x) => !x.works && x.e.legit && x.e.location === k);
  if (n.length) row(KN, k, n.length);
}

// The private endpoint's cost: separate per service and per network (fictional unit)
const monthlyRent = (installed) => installed.reduce((t, x) => t + SERVICE[x.split("|")[0]].rent, 0);
const supported = Object.keys(SERVICE).filter(supportsPrivate).length;
console.log(`\nprivate endpoints: A/B ${INSTALLED_A.length} installed, ${monthlyRent(INSTALLED_A)} units/mo; ` +
  `C ${INSTALLED_C.length} installed (${supported} services x ${NETWORK.length} networks), ${monthlyRent(INSTALLED_C)} units/mo`);
console.log(`C adds ${INSTALLED_C.length - INSTALLED_A.length} new private endpoints and ` +
  `${monthlyRent(INSTALLED_C) - monthlyRent(INSTALLED_A)} units/mo over B; ` +
  `broken legitimate access ${R["B public endpoint closed"].broken} -> ${R["C gaps closed"].broken}`);
const cannotClose = Object.keys(SERVICE).filter((h) => !SERVICE[h].supports);
console.log(`${cannotClose.length} service does not support private endpoints: ` +
  `${ACCESS.filter((e) => !SERVICE[e.service].supports).length} access records go through the public ` +
  `endpoint in every scenario; average rent ${(monthlyRent(INSTALLED_C) / INSTALLED_C.length).toFixed(1)} ` +
  `units per endpoint`);

// Every installed private endpoint is for one service and one network; how many are actually used
const NEEDED = [...new Set(ACCESS.filter((e) => e.legit && e.location !== "external"
  && SERVICE[e.service].supports).map((e) => `${e.service}|${e.location}`))];
const idle = INSTALLED_C.filter((x) => !NEEDED.includes(x));
console.log(`of the ${INSTALLED_C.length} private endpoints in C, ${NEEDED.length} are used by a ` +
  `legitimate access record; ${idle.length} sit idle and eat ${monthlyRent(idle)} units/mo`);
console.log(`installing only the needed ${NEEDED.length} would run ${monthlyRent(NEEDED)} units/mo: ` +
  `the ceiling is ${supported} services x ${NETWORK.length} networks = ${supported * NETWORK.length}, ` +
  `${supported * NETWORK.length - NEEDED.length} installations remaining`);
```

```
scenario                      private      public      jump    broken legit       cut  from internet
A public endpoint open              8          14         0               3         0             14
B public endpoint closed            8           2         0              12         3              2
C gaps closed                      14           2         3               3         3              2

scenario                      to private addr   to public addr
A public endpoint open                     11               14
B public endpoint closed                   11               14
C gaps closed                              14               11

scenario                     0 paths   1 path  2 paths  total access
A public endpoint open             0       14       11            25
B public endpoint closed          12       13        0            25
C gaps closed                      6       19        0            25

B: break reason                    access
name private, no path                   3
name public, endpoint closed            9

B: broken legit, location      access
staging-network                     3
shared-network                      3
external                            6

private endpoints: A/B 6 installed, 90 units/mo; C 18 installed (6 services x 3 networks), 270 units/mo
C adds 12 new private endpoints and 180 units/mo over B; broken legitimate access 12 -> 3
1 service does not support private endpoints: 2 access records go through the public endpoint in every scenario; average rent 15.0 units per endpoint
of the 18 private endpoints in C, 12 are used by a legitimate access record; 6 sit idle and eat 78 units/mo
installing only the needed 12 would run 192 units/mo: the ceiling is 6 services x 3 networks = 18, 6 installations remaining
```
The numbers are of the measurement class; their inputs are the assumptions above. The first row
worth reading is scenario A, and it is unsettling: private endpoints are installed, the public
endpoint is still open, and **fourteen of twenty-five access records go through the open
internet.** A private endpoint being installed does not mean traffic goes through it. In the same
row, even in scenario A, three legitimate access records already sit broken.

## Separating Access by Path Count

The path-distribution table carries this distinction. In scenario A, no access record sits at zero
paths: fourteen sit at one path, eleven at two paths. That means both the private and the public
endpoint are open for the eleven access records in the production and staging networks. Even so,
three access records in the staging network go through the public endpoint, because the
name-resolution zone is not attached to the staging network. The path exists; the name does not
point to it. This is what the measure separates: installing a private endpoint opens a path,
attaching the name record makes that path usable, and the two are separate jobs.

The three access records in the shared network sit on the opposite side. The name zone is attached
to the shared network, so the name resolves to the private address; but the shared network is not
peered with the production network, and there is no path to the private address. Even in scenario
A, these three access records are broken, and the reason is not the public endpoint being closed.
Name resolution produces its result before routing does; once the address resolves to the wrong
side, even a public endpoint left open does not save it.

## What's Left When the Public Endpoint Closes

In scenario B, the public endpoint closes on the six supported services. Twelve of the
twenty-five access records drop to zero paths, thirteen stay at one path, and none remain at two
paths. Broken legitimate access rises from three to twelve; in exchange, three access records that
are not legitimate — the developer laptop's two direct reads of the production store and one
client of unknown origin — get cut off. This is the gain column, and it is three items.

The reasons behind the twelve broken records fall into two groups. Three are in the "name private,
no path" state and were already broken in A; nine are in the "name public, endpoint closed" state
and are the direct result of closing the endpoint. By location, three break in the staging network,
three in the shared network, and six from external. The six from external are this lesson's real
warning: the nightly job, the edge uploader, and the management tool are not inside the private
network, and never were. Closing the public endpoint cuts them without seeing them, because the
decision to close is made on the service side, and the service side does not know where the client
is.

## The Bill for Closing the Gaps

In scenario C, three things happen at once: a private endpoint is installed in every network for
every supported service, the name zone is attached to all three networks, and a jump point is
added for the management tool. The result drops broken legitimate access from twelve to three;
access going through a private endpoint rises from eight to fourteen, three access records go
through the jump point, and the count of names resolving to a public address drops from fourteen
to eleven.

The remaining three broken access records are the ones that cannot be closed: the nightly job's
two access records and the edge uploader's store access. These run from external, cannot be
converted into a jump point, and cannot reach the service by a private endpoint's very definition.
A private endpoint resolves access from inside the network; for a client sitting outside the
network, it has nothing to resolve. These three jobs will either have to move inside the network,
or the public endpoint will have to stay open.

The bill itself is a separate line item, and it is not a one-time cost. **A private endpoint is
needed separately per service and per network:** six supported services times three networks,
eighteen installations, at 270 fictional units a month. That is twelve new private endpoints and
180 units/mo more than B — the cost of rescuing nine legitimate access records. Of the eighteen
installations, only twelve are used by a legitimate access record; the remaining six sit idle and
eat 78 units a month. Installing only the needed twelve would have made the bill 192 units. The
ceiling is eighteen and twelve of it is full; every new service or new network multiplies this
product, because a single private endpoint does not serve more than one service.

One line item does not close under any scenario. The notification service does not support a
private endpoint; the two access records reaching it go through the public endpoint in all three
scenarios. The rented layer's limit becomes visible here: how far isolation can be pushed is
decided by the capability the provider gives per service, and that item cannot be negotiated.

## Summary

- Private endpoints being installed does not mean traffic goes through them: in scenario A,
  fourteen of twenty-five access records go through the open internet, and three are already
  broken.
- Path and name resolution are measured separately. Two paths are open in the staging network, but
  three access records go through the public endpoint because the name zone is not attached; in
  the shared network the name resolves to private, but there is no path because there is no
  peering.
- Once the public endpoint closes, twelve of twenty-five access records drop to zero paths; broken
  legitimate access rises from three to twelve (staging 3, shared 3, external 6), and three
  non-legitimate access records get cut off.
- Closing the gaps drops broken access from twelve to three; the cost is twelve new private
  endpoints and 180 fictional units a month. The remaining three access records are outside the
  network and cannot be resolved with a private endpoint.
- A private endpoint is needed separately per service and per network: six services times three
  networks, twelve of the eighteen-installation ceiling are used, six sit idle and eat 78 units a
  month.
- One service does not support a private endpoint; the two access records reaching it go through
  the public endpoint in every scenario, and this limit is not a decision the tenant can make.

## Next Step

The public endpoint was closed, paths were counted, and the three access records left outside were
named. But the closed path was meant to keep out not only the expected clients but also the
unexpected ones; the three cut non-legitimate access records showed that. The problem is that
those three could only be counted because they were written into the table. An event that is not
in the table, that comes from inside the private network, and that carries valid credentials never
showed up in any table. The next lesson measures anomalous activity: in a known set of events, it
counts how many the detection rules catch, how many they miss, and how many false alarms they
produce.
