Skip to content
academia.sh

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 policy function and is not changed in this lesson; intent is the course’s constant.
  • NS58policy 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

"""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?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close