---
title: 'Zero Trust Network Model'
source: 'https://academia.sh/en/courses/wireless-and-security/zero-trust-network-model'
course: 'Wireless Networks and Network Security'
language: en
updated: '2026-08-17T18:07:22+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```text
# 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 `policy` function and is **not changed** in this lesson; intent is the course's
  constant.
- **NS58** — `policy` is 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 `credential` status 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

```python
"""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 `source` turns 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?
