Skip to content
academia.sh

Lesson 10 / 14

Network Segmentation and DMZ

The same intent is turned into a rule set across four zone graphs: in two zones, the narrowest derivation that never leaks passes nothing and false-denies 22 flows, while the wide derivation leaks 6; in four zones, 38 rules zero the deviation, and in six zones the rule count climbs to 88 while the deviation stays at zero.

Contents

Every rule in the previous lesson was written over four names: ext, dmz, int, mgmt. These names came from the fixture and were never questioned — as if the network’s zones were just given. Yet no rule names a machine; every one names a zone. Take the zones away, and there is no rule left that can be written.

This lesson’s question is: who chooses the zones, how many, and how much of intent does that choice make sayable? In this lesson the rule set is not written by hand; it is derived from the zone graph. This way the only thing that changes is the graph.

A Zone Is a Policy Object

The segmentation mechanism is not this course’s subject: how a broadcast domain is split, how virtual LANs are tagged, and how a switch enforces this were established in the Switching and Routing course. The zone here is not that mechanism’s product; it is the smallest name policy can speak in.

The difference shows up here: as long as two machines are in the same zone, no rule can be written between them. The zone is the rule’s resolution. To be able to say an intent, the graph must distinguish everything the intent distinguishes.

The demilitarized zone (DMZ) exists exactly for this reason. It is intent’s middle term: a place open to the outside but not belonging to the inside. Without this name, only two options remain for a flow coming from outside — either it is let into the inside, or it is not let in at all.

# taught zone graph , not executed
# the names are fictional

  ext ──80,443──> dmz ──3306──> int <──> int
                                 │
   x─────────────────────────────┴───x─── mgmt
                          (mgmt goes everywhere , nobody goes to it)

merging  :  2 zones  ext | dmz+int+mgmt
            3 zones  ext | dmz | int+mgmt
splitting:  6 zones  ext | dmz-web | dmz-pub | int-data | int-app | mgmt

The Perimeter Defense topic of the Cybersecurity curriculum covered the design of trust zones: which asset goes in which zone, how layers are arranged. That design narrative is not repeated here. What is measured is how much of policy the zone graph can meet.

The Graph Derives the Rules

Given a graph, one or more base pairs collect under every zone pair and every port. As the graph gets coarser, more than one base pair accumulates under a single pair, and a single decision has to be made for that pair. Two derivations are possible:

The narrow derivation writes the rule only if all the base pairs underneath it are permitted. It never leaks; it also cuts the permitted ones among mixed pairs.

The wide derivation writes the rule if even one of the pairs underneath it is permitted. It never cuts a permitted flow; it also passes the forbidden ones among mixed pairs.

If the graph can distinguish all the base zones, no pair has any mixture underneath it, and the two derivations give the same set. Ambiguity is not born from the graph; it is born from merging.

One warning: this derivation writes the set in expanded form, producing a separate rule for every port. The previous lesson’s seven-rule set wrote the same boundary in compressed form — an unwritten field matched any value. The two counts do not belong on the same scale; what this lesson compares is the ratio across graphs.

The measurement’s assumptions:

  • NS15 — The forty flows are taken unchanged from the shared fixture; the oracle is intent itself, and in every measurement it looks at base zone names.
  • NS16 — For the fine six-zone graph, each flow’s two ends are placed into one of their base zone’s subzones by a choice from a separate generator. An end’s base zone does not change; it only gets a finer name.
  • NS17 — The rule set is not written by hand; it is derived from the graph. The only thing that changes is the graph; policy and intent are fixed.
  • NS18 — The derivation writes rules in expanded form: a separate rule for every zone pair and every port. These rule counts are not compared with the previous lesson’s compressed sets.
  • NS19 — Every derived rule carries the permit action; the only thing that blocks is the implicit deny.
  • NS20 — The graph changes only the zone names. The port set, the flow count, and intent are the same across all four graphs.
  • NS21 — In a set of forty events, the smallest measurable difference is 1/40 = 0.025.

Measurement

"""Network segmentation: rule count and two-way error as the zone graph changes.

Part 1 - the graph derives a rule set from the same intent.
Part 2 - the effect of merging and splitting on false admit / false deny.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
PORTS = (80, 443, 22, 3306, 8080)
FINE = ("ext", "dmz-web", "dmz-pub", "int-data", "int-app", "mgmt")
BASE = {"ext": "ext", "dmz-web": "dmz", "dmz-pub": "dmz",
        "int-data": "int", "int-app": "int", "mgmt": "mgmt"}


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def flows(count=40, seed=SEED):
    r, out = generator(seed), []
    for i in range(count):
        out.append({"no": i + 1, "source": ZONES[r(4)],
                    "dest": ZONES[r(4)], "port": PORTS[r(5)]})
    return out


def subzone(ak, seed=SEED + 3):
    """Places each end into one of its base zone's subzones."""
    r = generator(seed)
    pairs = {"dmz": ("dmz-web", "dmz-pub", "dmz-web", "dmz-pub"),
             "int": ("int-data", "int-app", "int-app", "int-data")}
    for a in ak:
        for end in ("source", "dest"):
            base = a[end]
            a["fine_" + end] = pairs[base][r(4)] if base in pairs else base
    return ak


def policy(a):
    """Intent: only the DMZ's web entry from outside; nobody from outside to mgmt."""
    if a["source"] == "ext":
        return a["dest"] == "dmz" and a["port"] in (80, 443)
    if a["source"] == "dmz":
        return a["dest"] == "int" and a["port"] == 3306
    if a["source"] == "mgmt":
        return True
    return a["dest"] != "mgmt"


def diff(events, intent, device):
    d = {"correct_pass": 0, "correct_block": 0, "false_admit": 0, "false_deny": 0}
    for o in events:
        n, g = intent(o), device(o)
        if n and g:
            d["correct_pass"] += 1
        elif not n and not g:
            d["correct_block"] += 1
        elif g:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def rule_chain(rules):
    """First match wins; implicit deny at the end."""
    def run(a):
        for action, criteria in rules:
            if all(a[k] == v for k, v in criteria.items()):
                return action
        return False
    return run


GRAPHS = (
    ("2 zones", {f: ("ext" if f == "ext" else "int") for f in FINE}),
    ("3 zones", {f: ("int" if f == "mgmt" else BASE[f]) for f in FINE}),
    ("4 zones", dict(BASE)),
    ("6 zones", {f: f for f in FINE}),
)


def build_rules(graph, loose):
    """The rule set that meets intent as far as the graph can tell things apart."""
    names = sorted(set(graph.values()))
    rules = []
    for z1 in names:
        for z2 in names:
            for port in PORTS:
                allowed = [policy({"source": BASE[f1], "dest": BASE[f2],
                                   "port": port})
                           for f1 in FINE if graph[f1] == z1
                           for f2 in FINE if graph[f2] == z2]
                if all(allowed) or (loose and any(allowed)):
                    rules.append((True, {"source": z1, "dest": z2,
                                         "port": port}))
    return rules


def device(graph, rules):
    k = rule_chain(rules)
    return lambda a: k({"source": graph[a["fine_source"]],
                        "dest": graph[a["fine_dest"]],
                        "port": a["port"]})


AK = subzone(flows())
print(f"flow {len(AK)} | intent passes {sum(1 for a in AK if policy(a))} "
      f"| fine zones {len(FINE)}")
print()
print(f"{'zone graph':<12s} {'zones':>5s} {'derivation':<10s} {'rules':>5s} "
      f"{'correct':>7s} {'false admit':>11s} {'false deny':>11s}")
for name, g in GRAPHS:
    for kind, loose in (("narrow", False), ("wide", True)):
        k = build_rules(g, loose)
        d = diff(AK, policy, device(g, k))
        print(f"{name:<12s} {len(set(g.values())):5d} {kind:<10s} {len(k):5d} "
              f"{d['correct_pass'] + d['correct_block']:7d} {d['false_admit']:11d} "
              f"{d['false_deny']:11d}")

print()
for name, g in GRAPHS[:2]:
    f = device(g, build_rules(g, True))
    print(f"{name} wide derivation's false admits:",
          [(a["no"], a["source"], a["dest"], a["port"])
           for a in AK if not policy(a) and f(a)])

FINE_GRAPH = GRAPHS[3][1]
MISSING = [(e, o) for e, o in build_rules(FINE_GRAPH, False) if o["dest"] != "int-app"]
INCOMPLETE = device(FINE_GRAPH, MISSING)
d = diff(AK, policy, INCOMPLETE)
print()
print(f"6 zones, rules targeting 'int-app' left unwritten: rules {len(MISSING)}, "
      f"correct {d['correct_pass'] + d['correct_block']}, "
      f"false admit {d['false_admit']}, false deny {d['false_deny']}")
print("flows cut:",
      [(a["no"], a["fine_source"], a["fine_dest"], a["port"])
       for a in AK if policy(a) and not INCOMPLETE(a)])
flow 40 | intent passes 22 | fine zones 6

zone graph   zones derivation rules correct false admit  false deny
2 zones          2 narrow         0      18           0          22
2 zones          2 wide          12      34           6           0
3 zones          3 narrow        12      29           0          11
3 zones          3 wide          18      38           2           0
4 zones          4 narrow        38      40           0           0
4 zones          4 wide          38      40           0           0
6 zones          6 narrow        88      40           0           0
6 zones          6 wide          88      40           0           0

2 zones wide derivation's false admits: [(23, 'dmz', 'mgmt', 8080), (26, 'int', 'mgmt', 8080), (27, 'int', 'mgmt', 8080), (32, 'ext', 'int', 443), (36, 'ext', 'int', 443), (39, 'dmz', 'mgmt', 8080)]
3 zones wide derivation's false admits: [(26, 'int', 'mgmt', 8080), (27, 'int', 'mgmt', 8080)]

6 zones, rules targeting 'int-app' left unwritten: rules 71, correct 38, false admit 0, false deny 2
flows cut: [(4, 'int-data', 'int-app', 443), (38, 'mgmt', 'int-app', 443)]

What Merging Costs

The two-zone graph’s narrow derivation produces zero rules. This is not a writing error; it is the answer the derivation gives: in a two-zone space, no pair is permitted across all of its underlying base pairs. The entire boundary falls to the implicit deny. False admit 0 — flawless on paper. False deny 22: all twenty-two flows intent would pass are cut.

The wide derivation of the same graph writes 12 rules and flips the direction of the error: false deny 0, false admit 6. Four of the six leaking flows go to the management zone, two go straight from outside to the inside. The graph did not change, intent did not change; the only thing that changed was how a mixed pair gets read. When a graph collapses to two zones, the direction of the error is no longer the design’s choice but the derivation’s. This is the DMZ’s rationale in one line: without that name, the sentence “open to the outside but not belonging to the inside” cannot be said.

The three-zone graph separates the outside and the DMZ, leaving management inside. The narrow derivation false-denies 11 flows with 12 rules; the wide derivation, with 18 rules, leaks only 2 flows, and both go from int to management. Putting management under the same name as the inside makes the sentence “nobody goes from the inside to management” unsayable — one zone merge directly erases a prohibition.

In the four-zone graph, the two derivations give the same 38 rules, and the deviation is zero in both directions. This is the expected outcome: the graph distinguishes everything intent distinguishes, no mixed pair remains, there is nothing left to choose between.

What Splitting Does Not Pay For

The six-zone graph splits the DMZ and the inside into two pieces each. The zone count rises from 4 to 6 — 1.5 times. The rule count rises from 38 to 88 — 2.3 times; as long as the derivation writes a rule for every zone pair, this growth tracks the square of the zone count. The deviation from intent, meanwhile, goes from 0 to 0. The decision was already correct on all forty flows, and it stays correct.

What the measure says is this: adding zones does not move you closer to intent. Four zones could already say intent; six zones say it at greater length. This does not mean splitting is useless — it opens up other intents that can be said. But nothing is gained on this measure, and the cost is fifty rules.

Splitting carries a risk of its own, and it shows up in the lower table. When a zone is split in two, rules must be rewritten separately for both halves; when one half is forgotten, the set drops to 71 rules, false admit stays at 0, and false deny climbs to 2. Both flows go to the forgotten half of the split. The splitting error is one-directional: because the new zone name appears in no rule, it opens nothing — it only closes.

Surfaces Left Open, and Narrowings

First surface — the wide derivation’s mixed pairs. Six flows in the two-zone graph, two flows in the three-zone graph, pass although intent forbids them. These are not a configuration mistake; they are things the graph cannot say. Narrowing: a new zone name. Moving management to its own name zeroes the two leaks in the three-zone graph; separating out the DMZ closes four of the six in the two-zone graph. What gets added is not a rule but a name.

Second surface — the narrow derivation’s silent cutting. 22 flows in the two-zone graph, 11 in the three-zone graph, fall to the implicit deny, and none of them has a rule name standing against it. An audit that only counts leaks would declare the two-zone graph flawless. Narrowing: build the measurement two-directionally, and write a countable deny rule at the end of the chain — the previous lesson’s narrowing applies here too.

Third surface — the forgotten half of a split zone. Splitting a zone in two doubles every rule that touches it. In the measurement, the forgotten half is visible because it only produces false denies — the user complains. The reverse direction is quieter: if the new name falls under a wide rule that was never meant to touch it, it goes entirely unnoticed. Narrowing: never separate the split from the rule-writing; rerun the derivation from scratch for every new zone name, and compute the expected growth in rule count ahead of time. Fewer rules than expected means an unwritten rule.

Summary

  • A zone is not a segmentation mechanism; it is the smallest name policy can speak in. If two machines are in the same zone, no rule can be written between them.
  • In a coarse graph, mixed base pairs accumulate under every zone pair, giving rise to two derivations: the narrow derivation never leaks but false-denies, the wide derivation never false-denies but leaks.
  • In the two-zone graph, the narrow derivation writes 0 rules and false-denies 22 flows; the wide derivation leaks 6 flows with 12 rules. This is the DMZ’s rationale — without that name, intent’s middle term cannot be said.
  • In four zones, the two derivations give the same 38 rules and the deviation is zero in both directions; ambiguity is born from merging, not from the graph.
  • As zones rise from 4 to 6, rules rise from 38 to 88 while the deviation stays at 0: adding zones does not move you closer to intent, it only says the same intent at greater length.
  • Splitting a zone and leaving one half’s rules unwritten produces 0 false admits and 2 false denies; the splitting error is one-directional.

Next Step

The three lessons so far each treated the boundary as a mechanism that decides once: a flow arrives, runs through the rule chain, passes or blocks. The decision is read from the flow’s own fields and ends there. But whether a flow matches intent is sometimes not told by its fields but by its behavior — and behavior is read through a threshold. The next lesson sets the rule chain aside and measures the threshold: as the threshold tightens, the number of missed flows falls, the number of pointlessly blocked flows rises, and the two numbers cannot be zeroed at the same time. The same measure gives the same number on a detecting mechanism and a preventing one, but it does not charge the same cost.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close