◇ OpenCyber Academy
Technical · Chapter 10 · English

Chapter 10 — Critical infrastructure and sector security: control configuration

Open, free learning for anyone who wants to protect people and technology. No account is required to read this text or its worked answers.

Chapter 10 — Critical infrastructure and sector security: control configuration

Lesson map

Lesson 10 published. Map, example, EX037.

flowchart TD
  N0["Learning outcomes and connection"]
  N1["10.1 Rule sets as programs"]
  N2["10.2 Patterns that erode a policy"]
  N3["10.3 Remote and supplier access"]
  N4["10.5 Change planning"]
  N5["10.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
  N6 --> EX["Worked example"]
  EX --> Q["Exercise EX037"]

Logical drawing

flowchart LR
  N0["Learning outcomes and connection"]
  N1["10.1 Rule sets as programs"]
  N2["10.2 Patterns that erode a policy"]
  N3["10.3 Remote and supplier access"]
  N4["10.5 Change planning"]
  N5["10.6 Practical activity"]
  N6["Chapter synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX037"]
[Learning outcomes and connection]
    |
    v
[10.1 Rule sets as programs]
    |
    v
[10.2 Patterns that erode a policy]
    |
    v
[10.3 Remote and supplier access]
    |
    v
[10.5 Change planning]
    |
    v
[10.6 Practical activity]
    |
    v
[Chapter synthesis]
    |
    v
[Worked example] --> [Exercise EX037]

Learning outcomes and connection

After this chapter you should be able to:

Chapters 6–9 described and observed the system. This chapter changes it, starting with the control that most directly limits who can reach an unauthenticated protocol: the conduit firewall.

10.1 Rule sets as programs

Most network firewalls evaluate rules top to bottom and apply the first match. NIST's firewall guide describes policy design, default deny and rule-set management (SP 800-41 Rev. 1). Thinking of a rule set as a small program clarifies three defects:

Order matters, so every change must be reviewed in context, not as a single line.

10.2 Patterns that erode a policy

CISA's primary mitigations for operational technology emphasize removing internet exposure, changing default passwords, securing remote access and segmenting IT from OT (CISA primary mitigations for OT).

10.3 Remote and supplier access

Configure supplier access so that it exists only when needed: the account or path is disabled by default; enabled for a ticketed job and a fixed window; authenticated with multi-factor authentication at the jump host; the session is logged; and an alert fires on any use outside an enabled window. CISA's advisory on PRC state-sponsored actors describes actors maintaining long-term access to critical infrastructure environments, including through valid accounts and remote access (CISA AA24-038A) — the reason standing access is a finding even when nobody is misusing it today.

10.4 Worked example: fw-01 (synthetic)

The policy on fw-01 between the KVROC zones has seven rules. Lab 10 checks it for shadowing and against five test flows, each with the outcome the conduit list expects.

Lab 10 (save as lab10.py, run with python3 lab10.py):

# Lab 10 - evaluate a first-match firewall policy and find shadowed rules (offline, stdlib)
# Synthetic conduit policy for fw-01 between Kestrel Vale zones; addresses are RFC 1918.
from ipaddress import ip_address, ip_network
rules = [  # id, action, source, destination, protocol, port (None = any)
    ("R1", "allow", "10.20.0.10/32", "10.10.0.0/16", "tcp", 502),    # SCADA server polls stations
    ("R2", "allow", "10.20.0.15/32", "10.10.0.0/16", "tcp", 502),    # engineering workstation
    ("R3", "allow", "10.90.0.20/32", "10.20.0.0/24", "tcp", 22),     # jump host to control centre
    ("R4", "allow", "10.30.0.0/16",  "10.20.0.0/24", "tcp", None),   # "temporary" corporate access
    ("R5", "deny",  "10.30.3.0/24",  "10.20.0.0/24", "tcp", 3389),   # intended: block RDP from desks
    ("R6", "allow", "10.20.0.0/24",  "10.10.4.0/24", "tcp", 502),    # duplicate of R1 for KW-04
    ("R7", "deny",  "0.0.0.0/0",     "0.0.0.0/0",    "any", None),   # default deny
]
def matches(rule, src, dst, proto, port):
    _, _, s, d, p, pt = rule
    return (ip_address(src) in ip_network(s) and ip_address(dst) in ip_network(d)
            and p in ("any", proto) and pt in (None, port))
def covers(a, b):   # does earlier rule a match everything rule b matches?
    return (ip_network(b[2]).subnet_of(ip_network(a[2])) and ip_network(b[3]).subnet_of(ip_network(a[3]))
            and a[4] in ("any", b[4]) and a[5] in (None, b[5]))
for i, r in enumerate(rules):
    for earlier in rules[:i]:
        if covers(earlier, r):
            kind = "redundant" if earlier[1] == r[1] else "SHADOWED (never takes effect)"
            print(f"{r[0]} {kind}: fully covered by {earlier[0]}"); break
tests = [("10.20.0.10", "10.10.4.2", "tcp", 502, "allow"), ("10.30.3.7", "10.20.0.5", "tcp", 3389, "deny"),
         ("10.30.3.7", "10.10.4.2", "tcp", 502, "deny"), ("10.30.8.2", "10.20.0.5", "tcp", 445, "deny"),
         ("10.20.0.12", "10.10.4.2", "tcp", 502, "deny")]
for src, dst, proto, port, expect in tests:
    hit = next(r for r in rules if matches(r, src, dst, proto, port))
    status = "PASS" if hit[1] == expect else "FAIL"
    print(f"{status} {src}->{dst} {proto}/{port}: {hit[1]} by {hit[0]} (expected {expect})")

Expected output (Python 3.13.5):

R5 SHADOWED (never takes effect): fully covered by R4
PASS 10.20.0.10->10.10.4.2 tcp/502: allow by R1 (expected allow)
FAIL 10.30.3.7->10.20.0.5 tcp/3389: allow by R4 (expected deny)
PASS 10.30.3.7->10.10.4.2 tcp/502: deny by R7 (expected deny)
FAIL 10.30.8.2->10.20.0.5 tcp/445: allow by R4 (expected deny)
FAIL 10.20.0.12->10.10.4.2 tcp/502: allow by R6 (expected deny)

Three findings:

  1. R5 is shadowed by R4. The comment says RDP from corporate desks is blocked, but R4 already allowed all TCP from 10.30.0.0/16, so R5 never applies — the "block" has never worked.
  2. R4 is the real problem. A "temporary" rule allows every TCP port from the whole corporate zone into the control centre. Its tests fail (RDP and SMB are allowed). Note what the tests do not show: corporate desk to station port 502 is denied by R7, so fw-01 does not explain the corporate-desk writes captured at KW-04 in Chapter 2. That path must exist somewhere else; record it as open (Chapter 13 finds it).
  3. R6 is not a duplicate of R1. It allows the whole control-centre subnet to write to KW-04, including 10.20.0.12, which has no business there.

A sound change: delete R4 (after confirming with the owner what still uses it — the firewall logs will show); move the RDP block above any allow if it is still needed, or rely on the default deny; replace R6 with nothing (R1 and R2 already cover the required paths). Re-run the evaluator with the new rule set before the change and the live tests afterwards (Chapter 11).

10.5 Change planning

Every configuration change in a control environment needs: the exact before and after; the reason (finding or requirement); the expected effect, written as tests; a rollback plan; the window agreed with operations; and who verifies. NIST's guide to security-focused configuration management describes baselines and change control (SP 800-128). The firewall export before and after is evidence; hash it (Chapter 5).

Export and hash the rule set before and after the change, and keep both digests in the change record. Write the expected effect as the same tests Lab 10 runs, so verification is a re-run rather than a new invention. Agree the window with operations, and name who will confirm that SCADA polling still meets the freshness requirement after the change.

10.6 Practical activity

Rewrite the fw-01 policy from the conduit list in Chapter 6: one rule per declared conduit, each with ticket and owner in its comment, then the default deny. Run Lab 10 on your version until all five tests pass and no rule is shadowed or redundant. Add two tests of your own for conduit C3.

Put each allow above the default deny, with ticket and owner in the comment. Remove R4 and R6 rather than trying to patch them. Add two tests for conduit C3: jump host to control centre on tcp/22 allowed, and a corporate desk to the same port denied. Re-run until every test passes and no rule is shadowed or redundant.

10.7 Common errors and limits

Exercises — chapter 10

EX037 — Explain the shadow. Objective: reason about order. Inputs: Lab 10. Steps: (1) move R5 above R4 and predict the test results; (2) run and compare. Expected evidence: prediction and output. Rubric: 3 = no shadowing reported; test 2 (RDP) passes (deny by R5); test 4 (SMB) still fails (allow by R4); test 5 still fails (R6); 2 = mostly; 1 = guesses; 0 = none.

EX038 — Fix the policy. Objective: produce a correct rule set. Inputs: Lab 10. Steps: remove R4 and R6 (keep R5 or remove it with justification); run. Expected evidence: new rule list and output with five PASS lines. Rubric: 3 = five PASS lines; justification for R5 (redundant with the default deny once R4 goes, so remove or keep for explicit logging); notes the need to check R4's users in logs first; 2 = passes without justification; 1 = partial; 0 = none.

EX039 — Supplier access configuration. Objective: configure time-bound access. Inputs: the SS-3 relay vendor needs remote access about twice a year. Steps: specify the account state, enablement steps, path, authentication, logging, alerting and expiry. Expected evidence: a configuration specification. Rubric: 3 = disabled by default; enabled by KVROC on-call against a ticket approved by Kestrel Grid; path via jump-01 with MFA and session recording; never directly to the relay network; alert on use outside a window; automatic expiry; next-day review; 2 = one element missing; 1 = "VPN for vendor"; 0 = none.

EX040 — Detect broadening. Objective: extend the evaluator. Inputs: Lab 10's covers. Steps: add a check that reports when a later rule with the same action partially overlaps an earlier one and is broader in source or destination. Expected evidence: code and output. Rubric: 3 = reports R6 as broadening R1 (source /24 contains R1's /32; destination /24 inside /16; same port), explains why this deserves review; 2 = detects without explanation; 1 = attempted; 0 = none.

Chapter synthesis

A firewall rule set is a first-match program, and its defects are program defects: shadowed rules that never act, redundant rules that confuse, and look-alike rules that widen access. Configure conduits one rule per declared purpose with a default deny, keep supplier access disabled until needed, and plan every change with its tests and rollback. Then verify, which is the next chapter.

Continue studying — chapter 10 of 18

← Chapter 09 · Course chapter index · Chapter 11 →

Continue to the next authored lesson.

Worked answers

EX037 — Explain the shadow. With R5 above R4: no shadowed rule is reported; tests: 1 PASS, 2 PASS (deny by R5), 3 PASS, 4 FAIL (SMB allowed by R4), 5 FAIL (R6). Reordering fixed the RDP comment's promise but not the broad rule.

EX038 — Fix the policy. Remove R4 and R6. All five tests PASS (RDP and SMB now deny by R7; 10.20.0.12 denied by R7). R5 is now redundant with the default deny; keep it only if a separate log category for RDP attempts is wanted (then it should be placed above any allow), otherwise remove it. Before removing R4, check fw-01 logs for 30 days to list what used R4 and give each legitimate use its own conduit.

EX039 — Supplier access configuration. vendor-ss3 disabled by default. Kestrel Grid raises a ticket; KVROC on-call enables the account for a fixed window (for example 4 h) in the access tool; vendor connects to jump-01 with MFA; session recorded; jump-01 permits only the vendor gateway's management address and the specific engineering port at SS-3, never the whole substation network; an alert fires on any login outside an enabled window; the account disables automatically; next working day the session log is reviewed against the ticket.

EX040 — Detect broadening. Add: def overlaps(a, b): return ip_network(a[2]).overlaps(ip_network(b[2])) and ip_network(a[3]).overlaps(ip_network(b[3])) and (a[4] in ("any", b[4]) or b[4] == "any") and (a[5] is None or b[5] is None or a[5] == b[5]), and for a later rule with the same action that overlaps but is not covered, report it when its source or destination is not a subnet of the earlier one. Verified output adds R6 broadens R1: same action, wider source 10.20.0.0/24, the same for R2, and R7 broadens R5 — the last is the intended default deny and should be excluded (for example skip rules whose source and destination are both 0.0.0.0/0). Review is needed because the rule looks like a duplicate in a quick reading but grants the whole control-centre subnet write access to KW-04.