state
state
¶
Per-container state bundle layout contract.
Every shielded container gets an isolated state directory. This module
is the single source of truth for where files live within it — all
paths are derived from a single state_dir root through
StateBundle. Zero dependencies
beyond pathlib.
Bundle layout::
{state_dir}/
├── hooks/
│ ├── terok-shield-createRuntime.json
│ └── terok-shield-poststop.json
├── {HOOK_ENTRYPOINT_NAME} # entrypoint script (stdlib-only Python)
├── ruleset.nft # pre-generated nft ruleset (gateways baked in)
├── upstream.dns # upstream DNS address
├── dns.tier # active DNS tier (dig/getent/dnsmasq)
├── loopback.ports # per-container host-loopback TCP ports (newline-separated)
├── profile.allowed # IPs from DNS resolution
├── profile.domains # domain names for dnsmasq config
├── live.allowed # IPs from allow/deny
├── live.domains # domain overrides from allow_domain
├── deny.list # persistent deny overrides
├── denied.domains # denied domains from deny_domain
├── dnsmasq.conf # generated dnsmasq configuration
├── dnsmasq.pid # dnsmasq PID (in container netns)
├── dnsmasq.log # dnsmasq query log (for shield watch)
├── resolv.conf # bind-mounted over /etc/resolv.conf (dnsmasq tier)
├── container.id # podman container ID (short, 12-char hex)
└── audit.jsonl # per-container audit log
BUNDLE_VERSION = 14
module-attribute
¶
Integer version of the state bundle layout.
Bumped whenever the file layout changes in a backwards-incompatible way.
The OCI hook hard-fails if the annotation version does not match. The
same constant is the signal check_environment() uses to detect a
stale on-disk entrypoint — bump it whenever the entrypoint protocol
changes even if the file layout itself is unchanged, so that
terok setup rewrites the script instead of short-circuiting.
Current shape (v14): loopback.ports carries the per-container
host-loopback TCP ports (the broker / signer ports the supervisor
binds), persisted at pre_start time. Earlier shapes are recoverable
via git log -L /^BUNDLE_VERSION/:src/terok_shield/state.py.
STATE_DIR_MODE = 448
module-attribute
¶
Permission mode for state_dir and its subdirectories.
Owner-only. The OCI hook in _oci_state.py rejects state_dir if
st_mode & 0o022 (group- or world-writable), because a loose mode
would let any local peer drop a ruleset.nft for the hook to apply
with CAP_NET_ADMIN. mkdir(mode=…) is masked by umask, so
the writer side has to chmod after creation to guarantee the bit
pattern the validator demands.
StateBundle(state_dir)
dataclass
¶
File-layout contract for a single shielded container's state_dir.
Frozen so the per-task instance is safe to pass through hook
callbacks without anyone smuggling a mutated state_dir into a
later stage. Every property is a pure derivation off state_dir;
the IO methods (read_allowed_ips,
read_denied_ips,
read_effective_ips,
ensure_dirs) bundle
the small handful of read-and-merge / setup helpers that previously
floated as free functions taking state_dir repeatedly.
state_dir
instance-attribute
¶
hooks_dir
property
¶
OCI hooks directory within the state bundle.
hook_entrypoint
property
¶
Path to the hook entrypoint script.
ruleset
property
¶
Path to the pre-generated nft ruleset file.
upstream_dns
property
¶
Path to the persisted upstream DNS address.
dns_tier
property
¶
Path to the persisted DNS tier value.
loopback_ports
property
¶
Path to the per-container host-loopback TCP ports list.
Written by HookMode.pre_start from the caller-supplied
ShieldConfig.loopback_ports (the per-container triple of
gate / token-broker / ssh-signer ports the supervisor binds).
Read back by shield_up / shield_down when they rebuild
the nft ruleset — so a fresh Shield constructed without
the override still emits the correct
tcp dport <p> ip daddr 10.0.2.2 accept rules.
profile_allowed
property
¶
Path to the profile-derived allowlist file.
profile_domains
property
¶
Path to the profile domain names list (for dnsmasq config).
live_allowed
property
¶
Path to the live allow/deny allowlist file.
live_domains
property
¶
Path to the live domain overrides file (from allow_domain).
deny
property
¶
Path to the persistent denylist file.
denied_domains
property
¶
Path to the denied domains list (from deny_domain).
dnsmasq_conf
property
¶
Path to the generated dnsmasq configuration file.
dnsmasq_pid
property
¶
Path to the dnsmasq PID file (PID is in the container netns).
dnsmasq_log
property
¶
Path to the dnsmasq query log (consumed by shield watch).
resolv_conf
property
¶
Path to the resolv.conf bind-mounted over /etc/resolv.conf in dnsmasq tier.
container_id
property
¶
Path to the persisted podman container ID file.
reader_pid
property
¶
Path where the bridge hook tracks the live NFLOG reader PID.
audit
property
¶
Path to the per-container audit log.
meta_path
property
¶
Persisted-meta-path pointer file under state_dir.
Mirrors the resource-side META_PATH_FILE_NAME constant — one
filename on both sides of the hook boundary so package code that
reads it (Shield.up()/down()) and resource code that
writes it (the bridge createRuntime hook) can never drift
on path convention.
hook_json(stage)
¶
read_loopback_ports()
¶
Read persisted loopback ports; empty tuple when the file is absent.
Source code in src/terok_shield/state.py
read_allowed_ips()
¶
Merge IPs from profile.allowed and live.allowed, deduplicated.
Returns a stable-order list: profile IPs first, then live IPs, with duplicates removed (first occurrence wins).
Source code in src/terok_shield/state.py
read_denied_ips()
¶
Read IPs from deny.list (empty set when the file is missing).
read_effective_ips()
¶
Compute effective allowed IPs: (profile ∪ live) − deny.
Returns a stable-order list with denied IPs subtracted.
Source code in src/terok_shield/state.py
ensure_dirs()
¶
Create the state directory and its required subdirectories.
Both directories are forced to
STATE_DIR_MODE
(0o700) on every call — the OCI hook rejects anything
looser, and a prior run under a permissive umask (Fedora's
default 0o002 is a common offender) would otherwise leave
the bundle stranded.