Lesson 13 / 14
Zero Trust Network Model
When the location criterion is stripped from the rule chain, one rule turns into an implicit allow, three rules get shadowed, and false admit climbs from zero to eighteen; when the identity criterion takes its place, rule count rises from seven to ten, and when the credential condition is added, false deny climbs from one to four.
Contents
The remote-access tunnel’s whole trick was making an outside endpoint look as if it came
from the inside to the rule chain. The reason that trick was necessary sat in a single
line: the rule chain reads the source criterion and expects a zone to read. The tunnel
makes up a zone, because the question the chain asks is “where are you coming from.”
Zero trust proposes dropping this question. The principle itself was established in
the Cybersecurity curriculum and is not repeated here. This lesson’s question is not the
principle; it is surgical: when the source criterion is cut out of the rule chain,
what is left in the chain, how many rules get shadowed, how does the two-way error change,
and what has to be put in the emptied place?
What the Location Criterion Represented
The course’s narrow rule set consists of seven rules, and all seven carry the source
criterion. The criterion says which zone a flow comes from, and for the rule’s author, the
zone is a shorthand: “a machine on the internal segment” and “the organization’s employee”
have been treated as the same thing.
This shorthand has no cost — as long as the shorthand is correct. If the machine at the end of the cable really is the employee’s machine, location stands in for identity, and the chain decides correctly. The previous lesson’s remote-access tunnel existed exactly to keep this shorthand standing: the remote end had no zone, the tunnel assigned it one, and the shorthand started working again.
Zero trust’s claim is that the shorthand no longer holds. What this lesson measures is not whether the claim is true — it is what removing the shorthand does to the rule chain.
Stripping the Criterion
The stripping is mechanical: the source key is removed from every rule’s criteria
dictionary. Rule count does not change, actions do not change, order does not change.
# taught dump , not executed location-criteria chain location criterion stripped 1 permit source=ext dest=dmz port=80 1 permit dest=dmz port=80 2 permit source=ext dest=dmz port=443 2 permit dest=dmz port=443 3 permit source=dmz dest=int port=3306 3 permit dest=int port=3306 4 permit source=mgmt 4 permit (no criteria left) 5 permit source=int dest=dmz 5 permit dest=dmz 6 permit source=int dest=ext 6 permit dest=ext 7 permit source=int dest=int 7 permit dest=int (implicit deny at the end) (implicit deny at the end)
The fourth rule carries the whole problem. In its location-criteria form it had a single
criterion, and that criterion was source; when stripped, no criterion is left. A
criteria-less rule matches every flow: under the “first match wins” rule, the fourth line
is now an implicit allow, and it shadows the three rules after it. Implicit deny,
meanwhile, never gets a turn at all.
What Takes Its Place
The location criterion indirectly said on whose behalf a flow was opened. When it is
removed, a criterion that says this directly has to take its place. This criterion travels
with the flow itself and is not tied to the cable; in this lesson’s fixture, its name is
identity, and its values are the classes guest, employee, service, and admin.
A second field is required: credential. Identity is a claim, and there has to be
something that verifies the claim. A flow without a credential has its identity treated as
unknown, and the chain drops it to implicit deny. This is the rule chain’s counterpart to
zero trust’s sentence “verify, do not assume” — and its cost will be measured.
The identity-criteria chain is longer than the location-criteria one. The reason is
the fourth rule: source=mgmt opened every destination in a single line, because the zone
was already a trust declaration. On the identity side there is no such declaration; every
destination the admin identity can reach is written separately, and one rule becomes
four.
The measurement’s assumptions:
- NS57 — The forty flows are produced from the course’s shared fixture. The oracle is
the
policyfunction and is not changed in this lesson; intent is the course’s constant. - NS58 —
policyis written in the language of location. Measuring an identity-criteria chain against this oracle is counting the flows where the two languages diverge; the measurement gives the size of the change, not the correctness of identity. - NS59 — An identity class is added to every flow. For most flows identity matches the zone; the fixture deliberately diverges a minority, because identity is not tied to the cable.
- NS60 — A
credentialstatus is added to every flow. The fixture leaves a minority without one; the credential field says whether the flow’s identity claim can be verified, not what the claim contains. - NS61 — Four chains are compared: location-based, location stripped, identity-based, identity and credential gated. The evaluation mechanism is the same in all four: first match wins, implicit deny sits at the end.
- NS62 — The shadowed-rule count is the number of rules fully covered by a criteria set that came before them; a rule whose criteria set has been emptied shadows every rule after it.
- NS63 — The set is forty flows; the smallest measurable difference is
1/40 = 0.025.
Measurement
"""The effect of stripping the location criterion out of the rule chain. Part 1 - source-criteria chain, stripped chain, identity-criteria chain. Part 2 - the gap the stripped criterion leaves in the rule chain. """ SEED = 20260811 ZONES = ("ext", "dmz", "int", "mgmt") IDENTITIES = ("guest", "employee", "service", "admin") MAPPING = {"ext": "guest", "dmz": "service", "int": "employee", "mgmt": "admin"} 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": (80, 443, 22, 3306, 8080)[r(5)]}) return out def add_identity(flows_list, seed=SEED + 6): """Adds an identity class and a credential status to each flow.""" r, out = generator(seed), [] for a in flows_list: divergent, candidate = r(9) == 0, IDENTITIES[r(4)] out.append({**a, "identity": candidate if divergent else MAPPING[a["source"]], "credential": r(8) != 0}) return out def policy(a): """Intent: only the DMZ 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 def shadowed_count(rules): count = 0 for i, (_, criteria) in enumerate(rules): for _, earlier in rules[:i]: if all(criteria.get(k) == v for k, v in earlier.items()): count += 1 break return count NARROW = [(True, {"source": "ext", "dest": "dmz", "port": 80}), (True, {"source": "ext", "dest": "dmz", "port": 443}), (True, {"source": "dmz", "dest": "int", "port": 3306}), (True, {"source": "mgmt"}), (True, {"source": "int", "dest": "dmz"}), (True, {"source": "int", "dest": "ext"}), (True, {"source": "int", "dest": "int"})] STRIPPED = [(e, {k: v for k, v in o.items() if k != "source"}) for e, o in NARROW] IDENTITY = [(True, {"identity": "guest", "dest": "dmz", "port": 80}), (True, {"identity": "guest", "dest": "dmz", "port": 443}), (True, {"identity": "service", "dest": "int", "port": 3306}), (True, {"identity": "admin", "dest": "ext"}), (True, {"identity": "admin", "dest": "dmz"}), (True, {"identity": "admin", "dest": "int"}), (True, {"identity": "admin", "dest": "mgmt"}), (True, {"identity": "employee", "dest": "dmz"}), (True, {"identity": "employee", "dest": "ext"}), (True, {"identity": "employee", "dest": "int"})] def credential_gated(rules): """A flow without a credential falls to implicit deny.""" chain = rule_chain(rules) return lambda a: a["credential"] and chain(a) ak = add_identity(flows()) print(f"flow {len(ak)} | intent passes {sum(1 for a in ak if policy(a))} " f"| identity diverges from location " f"{sum(1 for a in ak if a['identity'] != MAPPING[a['source']])} | no credential " f"{sum(1 for a in ak if not a['credential'])}") print() print(f"{'rule chain':<24s} {'rules':>5s} {'shadowed':>8s} {'correct':>7s} " f"{'false admit':>12s} {'false deny':>12s}") for name, rules, device in ( ("location-based", NARROW, rule_chain(NARROW)), ("location stripped", STRIPPED, rule_chain(STRIPPED)), ("identity-based", IDENTITY, rule_chain(IDENTITY)), ("identity + credential", IDENTITY, credential_gated(IDENTITY))): d = diff(ak, policy, device) print(f"{name:<24s} {len(rules):5d} {shadowed_count(rules):8d} " f"{d['correct_pass'] + d['correct_block']:7d} {d['false_admit']:12d} " f"{d['false_deny']:12d}") print() print("stripped chain's criteria:") for i, (_, o) in enumerate(STRIPPED): print(f" {i + 1}. {o if o else '(no criteria left)'}") print() print("flows whose identity diverges from location (no, location, identity, dest, intent, identity chain):") for a in ak: if a["identity"] != MAPPING[a["source"]]: print(f" {a['no']:2d} {a['source']:8s} {a['identity']:9s} {a['dest']:8s} " f"{str(policy(a)):5s} {str(rule_chain(IDENTITY)(a)):5s}") print("flows without a credential that intent would pass:", sum(1 for a in ak if not a["credential"] and policy(a)))
flow 40 | intent passes 22 | identity diverges from location 2 | no credential 5
rule chain rules shadowed correct false admit false deny
location-based 7 0 40 0 0
location stripped 7 3 22 18 0
identity-based 10 0 38 1 1
identity + credential 10 0 35 1 4
stripped chain's criteria:
1. {'dest': 'dmz', 'port': 80}
2. {'dest': 'dmz', 'port': 443}
3. {'dest': 'int', 'port': 3306}
4. (no criteria left)
5. {'dest': 'dmz'}
6. {'dest': 'ext'}
7. {'dest': 'int'}
flows whose identity diverges from location (no, location, identity, dest, intent, identity chain):
34 ext admin ext False True
40 int service ext True False
flows without a credential that intent would pass: 3
The Cost of Stripping
The second row carries the lesson’s harshest number. Rule count stayed at 7, not one
rule was added, not one was removed — and the chain fell from 40 correct to 22. False
admit climbed from 0 to 18; eighteen of the forty flows passed although intent
would have blocked them. That is a share of 0.450 in a set of forty, eighteen times the
measurement band.
False deny, meanwhile, stayed at 0, and this is not good news — it is the other face of the same flaw. Because the fourth rule matches every flow, no flow can reach the implicit deny; the chain no longer carries a path along which it can say “block.” An audit counting only one direction would have declared this chain flawless — false deny zero, no user complaints.
The shadowed-rule count is 3, and the lower dump gives the reason: the fourth line’s criteria set is empty, an empty set covers every criteria set, and so the fifth, sixth, and seventh rules are never evaluated. Rule shadowing here is born not from a writing mistake but from the removal of a field.
The pattern is this: the location criterion can, on its own, be a rule’s entire load-bearing part. Strip it from such a rule, and no rule is left — well, technically a rule remains. Rule count does not show this — the chain is still seven lines. The course already said in Access Control Lists that counting rules does not measure policy; here a sharper version of the same sentence appears: counting criteria does not measure policy either.
The Two Directions of the Identity Criterion
The third row gives the identity-criteria chain: 10 rules, 0 shadowed, 38 correct, 1 false admit, 1 false deny. Rule count rose from seven to ten, and the entire increase comes from the fourth rule — the implicit trust declaration the zone carried had to be written as four separate lines on the identity side. This is the rule cost of removing the location criterion.
The two errors can be read knowing exactly which of the forty flows they are born on; the
lower dump shows both. Flow thirty-four comes from the external zone but carries the
admin identity: intent would have blocked it, the identity chain passed it — false
admit. Flow forty comes from the internal segment but carries the service identity:
intent would have passed it, the identity chain blocked it — false deny.
These two numbers are not a flaw; they are the measure of where the two languages
diverge. The oracle is written in the language of location; the identity-criteria chain
is written in a different language, and the two languages say the same thing on
thirty-eight of the forty flows and diverge on two. The flows where they diverge are
exactly the flows where the location shorthand was already wrong. So the measurement does
not say “identity decided wrong”; it gives the number of flows whose intent itself needs
rewriting, and that number is 2/40 = 0.050.
The rule that comes out of this is clear: changing the criterion means changing the policy, not the rule. Trying to enforce an intent written in the language of location with a chain written in the language of identity produces an error on every flow where the two languages diverge. The move to zero trust is not a rule-chain edit; it is a rewriting of intent.
The Cost of the Credential Condition
The fourth row adds a single condition: a flow without a credential falls to implicit deny. False admit stays at 1 — the credential condition never touches this direction, because flow thirty-four has a credential and its identity passes. False deny, however, climbs from 1 to 4.
The source of the three-flow rise is in the bottom line: 5 of the forty flows have no
credential, and intent would have passed 3 of them. Correct drops from 38 to
35. That is a share of 3/40 = 0.075, three times the band; a measurable cost.
The two directions do not mix here, and that they do not is the lesson’s result. The credential condition only grows false deny. Making verification mandatory means not passing what cannot be verified, and some portion of what cannot be verified is always legitimate. Widening credential coverage recovers these three flows; loosening the coverage — that is, passing an uncredentialed flow by looking at location — brings the stripped criterion back through the side door and partially recalls the second row’s error.
Summary
- The location criterion was identity’s shorthand in the rule chain; as long as the shorthand is correct it costs nothing, and the narrow set gives 40 correct, 0 false admit, 0 false deny on forty flows.
- When the criterion is mechanically stripped, rule count stays at 7, but the rule
whose only criterion was
sourceturns into an implicit allow: 3 rules get shadowed, correct falls to 22, false admit becomes 18, and false deny stays at 0 — an audit counting only one direction would have declared this chain flawless. - When the identity criterion takes its place, rule count rises from 7 to 10; the entire increase comes from the zone’s implicit trust declaration being written as four separate lines.
- The identity-criteria chain gives 38 correct, 1 false admit, 1 false deny; the two errors are the two flows where identity and location diverge, and they measure the number of flows whose intent needs rewriting, not identity’s wrongness.
- The credential condition grows only one direction: false deny climbs from 1 to 4, because intent would have passed 3 of the 5 uncredentialed flows. This is the cost of making verification mandatory.
Next Step
Six controls were built across the course: generation and coverage on the wireless side, and segmentation, the rule chain, detection, the tunnel, and the criterion moving from location to identity on the security side. Each measured its own boundary in both directions at once. One question is left, and the course’s last lesson asks it: when all six controls are switched on together, which class of network attacks actually closes, how many classes remain open, and what is there in hand for the ones that stay open?
To keep your progress and take notes, Log in
My notes
Log in to take notes.