rules
rules
¶
nftables ruleset generation and verification.
Generates per-container nftables rulesets as an ordered tier policy and provides set operations for runtime allow/deny/override management, plus verification of applied rulesets against security invariants.
The UP ruleset is a single output chain whose body is an ordered list of
tier rules. nft evaluates them top to bottom: an accept/reject is a
terminal verdict (short-circuit), and a non-match falls through to the next
tier (a Pass). Tier order is the authority order::
preamble accept lo / established / DNS / infra ports
t00 hard-deny reject @HARD_DENY_RANGES (link-local/IMDS — absolute)
t10 override accept @override (break-glass, above the deny)
t20 security-deny reject @deny + @PRIVATE_RANGES (vault hosts + RFC1918)
t30/40 allow accept @allow (provider + project)
bypass window accept @bypass_window (kernel-timed allow-all)
terminal reject (log BLOCKED)
Because the deny tier sits above the allow tier, an explicit deny wins over an allow; an override (t10) sits above the deny and is the only way to reach a security-denied host. The hard-deny floor (t00) sits above the override and is absolute.
Security boundary: only stdlib + nft.constants imports. All inputs are
validated before interpolation into nft commands.
RulesetBuilder(*, dns=PASTA_DNS, loopback_ports=(), gateway_v4='', gateway_v6='', set_timeout='')
¶
Builder for nftables ruleset generation and verification.
Security boundary: only stdlib + nft.constants imports. All inputs validated before interpolation.
Binds dns, loopback_ports, gateways, and the dnsmasq set timeout
once at construction so callers do not repeat them on every call.
Create a builder with validated DNS, gateway, and port config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dns
|
str
|
DNS server address (pasta default forwarder). |
PASTA_DNS
|
loopback_ports
|
tuple[int, ...]
|
TCP ports to allow on the host-loopback map address. |
()
|
gateway_v4
|
str
|
IPv4 gateway address (e.g. slirp4netns |
''
|
gateway_v6
|
str
|
IPv6 gateway address (e.g. slirp4netns |
''
|
set_timeout
|
str
|
dnsmasq-tier element timeout for the allow sets (e.g. |
''
|
Source code in src/terok_shield/nft/rules.py
build_up()
¶
Generate the UP (deny-all + ordered tiers) ruleset.
Applied by the OCI hook into the container's own netns. Dual-stack. Infra ports (DNS, host-loopback proxy, gateway) are accepted in the preamble before any tier, so the control plane survives the hard-deny of link-local space. See the module docstring for the tier order.
Source code in src/terok_shield/nft/rules.py
build_down(*, disengaged=False)
¶
Generate the DOWN-posture (manual shield down) ruleset.
Output policy is accept and every new connection is logged with
the bypass prefix. Plain DOWN still enforces the hard-deny floor and
the security-deny tier (deny set + private ranges), with the t10
override kept above the deny — a break-glass host must stay
reachable in every posture that enforces the deny.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
disengaged
|
bool
|
If True (DISENGAGED), enforce nothing: no hard-deny
floor, no deny set, no private-range rejects — every
destination is accepted and logged. The tier sets stay
declared (unreferenced) so allow-set contents survive the
round trip back to |
False
|
Source code in src/terok_shield/nft/rules.py
build_quarantine()
staticmethod
¶
Generate the quarantine-mode (total blackout) ruleset.
Drops all traffic except loopback and established connections. No DNS, no allowlists, no gateway ports. All dropped packets are tagged for the audit log.
Source code in src/terok_shield/nft/rules.py
verify_up(nft_output)
¶
Check applied UP ruleset invariants. Returns errors (empty = OK).
Expects output from nft list table inet terok_shield (scoped to the
managed table), not nft list ruleset. Verifies the table header,
policy drop, both chains, the reject type, every tier set, the
terminal deny-all rule, and both range-reject floors.
Source code in src/terok_shield/nft/rules.py
verify_down(nft_output, *, disengaged=False)
¶
Check applied DOWN-posture ruleset invariants. Returns errors (empty = OK).
Verifies the table header, policy accept on output / drop on
input, both chains, every tier set, and the bypass nflog prefix.
Plain DOWN must carry both range-reject floors; DISENGAGED must carry
neither floor nor the deny-set reject — a partially applied ruleset
that keeps any reject must not pass as DISENGAGED.
Source code in src/terok_shield/nft/rules.py
verify_quarantine(nft_output)
staticmethod
¶
Check applied quarantine ruleset invariants. Returns errors (empty = OK).
Verifies the table header, both chains with policy drop, the blocked
log prefix, and that no allow sets exist (total blackout).
Source code in src/terok_shield/nft/rules.py
add_elements_dual(ips)
¶
Add IPs to the tier-40 project-allow sets, honouring the dnsmasq permanent-element rule.
When a set_timeout is configured (live tier), profile/live IPs are
written with timeout 0s so they do not auto-expire with the
dnsmasq-learned entries.
Source code in src/terok_shield/nft/rules.py
add_elements_dual(ips, *, permanent=False)
¶
Add IPs to the tier-40 project-allow sets, split by address family.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
permanent
|
bool
|
annotate elements with |
False
|
Source code in src/terok_shield/nft/rules.py
add_deny_elements_dual(ips)
¶
Add IPs to the tier-20 security-deny sets, split by address family.
add_override_elements_dual(ips)
¶
Add IPs to the tier-10 override (break-glass) sets, split by address family.
delete_deny_elements_dual(ips)
¶
Remove IPs from the tier-20 security-deny sets, split by address family.
arm_bypass_window(timeout)
¶
Open the timed allow-all window.
Adds 0.0.0.0/0 / ::/0 to the bypass_window sets with a kernel
timeout so the window closes itself when the element expires — no
userspace timer, fail-closed (any disruption only closes it sooner).
Source code in src/terok_shield/nft/rules.py
disarm_bypass_window()
¶
Close the timed allow-all window immediately by flushing both sets.
parse_set_elements(nft_output)
¶
Parse nft list set output into (ip, timeout) pairs.
timeout is the element's printed timeout token ("30m", "0s",
compound "1h22m") or "" when the element carries none. The
remaining expires countdown is deliberately dropped — a restore via
restore_elements re-grants
the full timeout. Atoms that fail validation are skipped rather than
raised: the snapshot/restore path is best-effort by design (a dropped
learned IP is re-learned on the workload's next DNS answer), and a
parse quirk must never abort a shield state transition.
Source code in src/terok_shield/nft/rules.py
restore_elements(set_name, elements, table=NFT_TABLE)
¶
Generate an nft command re-adding dumped (ip, timeout) elements.
Counterpart of parse_set_elements:
every input is re-validated before interpolation (the snapshot crosses a
subprocess boundary, so it is treated as untrusted like any other input).
Timed elements are re-granted their full timeout. Returns "" when
there is nothing to restore.
Source code in src/terok_shield/nft/rules.py
add_elements(set_name, ips, table=NFT_TABLE, *, timeout_zero=False)
¶
Generate an nft command to add validated IPs to a set.
Both set_name and table are validated against injection.
Returns empty string if no valid IPs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_zero
|
bool
|
annotate each element with |
False
|
Source code in src/terok_shield/nft/rules.py
delete_elements(set_name, ips, table=NFT_TABLE)
¶
Generate an nft command to delete validated IPs from a set.
Both set_name and table are validated against injection.
Returns empty string if no valid IPs.
Source code in src/terok_shield/nft/rules.py
safe_ip(value)
¶
Validate and normalize an IPv4 or IPv6 address or CIDR notation.
Prevents nft command injection by ensuring the value is a valid IP address or network. Returns the canonical string form so comparisons across state files are reliable regardless of input notation.
Raises ValueError on invalid input.