Skip to content

config

config

Shield configuration types, enums, and mode protocol.

Defines the vocabulary shared across the entire codebase: what a shield configuration looks like, what modes and states exist, and what contract a mode backend must satisfy.

ANNOTATION_LIST_SEP = ':' module-attribute

ANNOTATION_KEY = 'terok.shield.profiles' module-attribute

ANNOTATION_NAME_KEY = 'terok.shield.name' module-attribute

ANNOTATION_STATE_DIR_KEY = 'terok.shield.state_dir' module-attribute

ANNOTATION_VERSION_KEY = 'terok.shield.version' module-attribute

ANNOTATION_AUDIT_ENABLED_KEY = 'terok.shield.audit_enabled' module-attribute

ANNOTATION_UPSTREAM_DNS_KEY = 'terok.shield.upstream_dns' module-attribute

ANNOTATION_DNS_TIER_KEY = 'terok.shield.dns_tier' module-attribute

DnsTier

Bases: Enum

DNS resolution tier for egress control.

Determines how domain-based allowlists are enforced:

Per-container dnsmasq with --nftset auto-populates nft

allow sets on every DNS query. Handles IP rotation.

DIG: Static resolution at pre-start via dig (current fallback). GETENT: Single-IP resolution via getent hosts (minimal fallback).

DNSMASQ = 'dnsmasq' class-attribute instance-attribute

DIG = 'dig' class-attribute instance-attribute

GETENT = 'getent' class-attribute instance-attribute

ShieldMode

Bases: Enum

Operating mode for the shield firewall.

Currently only HOOK is supported. Future modes (e.g. bridge) will add members here.

HOOK = 'hook' class-attribute instance-attribute

ShieldState

Bases: Enum

Per-container shield state, derived from the live nft ruleset.

QUARANTINE: Total network blackout — all traffic dropped, dropped traffic logged. UP: Normal enforcing mode (deny-all with allowlists). DOWN: Bypass mode with private-range protection (RFC 1918 + RFC 4193). DISENGAGED: Bypass mode without private-range protection. OFFLINE: No ruleset found (container stopped or unshielded). ERROR: Ruleset present but unrecognised.

QUARANTINE = 'quarantine' class-attribute instance-attribute

UP = 'up' class-attribute instance-attribute

DOWN = 'down' class-attribute instance-attribute

DISENGAGED = 'disengaged' class-attribute instance-attribute

OFFLINE = 'offline' class-attribute instance-attribute

ERROR = 'error' class-attribute instance-attribute

ShieldRuntime

Bases: Enum

Container runtime category — drives DNS-reachability assumptions.

crun / runc / youki. The container shares the netns,

so dnsmasq on 127.0.0.1 is reachable directly.

KRUN: libkrun microVM. The guest has its own loopback isolated from the netns, so dnsmasq must bind to a link-local address on netns lo that the guest can reach via passt.

DEFAULT = 'default' class-attribute instance-attribute

KRUN = 'krun' class-attribute instance-attribute

from_runtime_name(name) classmethod

Map a podman --runtime <name> string (or None) to the enum.

Centralises the wire-format vocabulary so callers don't repeat "krun" → KRUN mappings inline. Anything other than "krun" (including None and unknown runtime names) maps to DEFAULT — the loopback-shared-with-netns assumption holds for every runtime shield has been tested against besides krun.

Source code in src/terok_shield/config.py
@classmethod
def from_runtime_name(cls, name: str | None) -> ShieldRuntime:
    """Map a podman ``--runtime <name>`` string (or ``None``) to the enum.

    Centralises the wire-format vocabulary so callers don't repeat
    ``"krun" → KRUN`` mappings inline.  Anything other than
    ``"krun"`` (including ``None`` and unknown runtime names) maps
    to ``DEFAULT`` — the loopback-shared-with-netns assumption holds
    for every runtime shield has been tested against besides krun.
    """
    return cls.KRUN if name == "krun" else cls.DEFAULT

ShieldConfig(state_dir, mode=ShieldMode.HOOK, default_profiles=('dev-standard',), loopback_ports=(), audit_enabled=True, profiles_dir=None, runtime=ShieldRuntime.DEFAULT) dataclass

Per-container shield configuration.

The library is a pure function of its inputs. Given a ShieldConfig with state_dir, it writes to that directory and nowhere else. No env-var reading, no config-file parsing.

state_dir instance-attribute

mode = ShieldMode.HOOK class-attribute instance-attribute

default_profiles = ('dev-standard',) class-attribute instance-attribute

loopback_ports = () class-attribute instance-attribute

audit_enabled = True class-attribute instance-attribute

profiles_dir = None class-attribute instance-attribute

runtime = ShieldRuntime.DEFAULT class-attribute instance-attribute

AuditFileConfig

Bases: BaseModel

Audit section of config.yml.

enabled = Field(default=True, description='Enable JSON-lines audit logging') class-attribute instance-attribute

model_config = ConfigDict(extra='forbid') class-attribute instance-attribute

ShieldFileConfig

Bases: BaseModel

Validated schema for config.yml.

Loaded by the CLI at startup. extra="forbid" rejects unknown keys so typos (e.g. mod: hook) produce a clear error instead of being silently ignored.

mode = Field(default='auto', description='Firewall mode (``auto`` selects the best available)') class-attribute instance-attribute

default_profiles = Field(default_factory=(lambda: ['dev-standard']), description='Profiles applied when no explicit list is given') class-attribute instance-attribute

audit = Field(default_factory=AuditFileConfig, description='Audit logging settings') class-attribute instance-attribute

model_config = ConfigDict(extra='forbid') class-attribute instance-attribute

ShieldModeBackend

Bases: Protocol

Strategy protocol for shield mode implementations.

Each concrete backend (e.g. HookMode) provides the full lifecycle: per-container firewalling, live allow/deny, bypass, and preview.

pre_start(container, profiles)

Prepare for container start; return extra podman args.

Source code in src/terok_shield/config.py
def pre_start(self, container: str, profiles: list[str]) -> list[str]:
    """Prepare for container start; return extra podman args."""
    ...

allow_ip(container, ip)

Live-allow an IP for a running container.

Source code in src/terok_shield/config.py
def allow_ip(self, container: str, ip: str) -> None:
    """Live-allow an IP for a running container."""
    ...

allow_domain(domain)

Live-allow a domain (update dnsmasq config if active).

Source code in src/terok_shield/config.py
def allow_domain(self, domain: str) -> None:
    """Live-allow a domain (update dnsmasq config if active)."""
    ...

deny_ip(container, ip)

Live-deny an IP for a running container.

Source code in src/terok_shield/config.py
def deny_ip(self, container: str, ip: str) -> None:
    """Live-deny an IP for a running container."""
    ...

deny_domain(domain)

Live-deny a domain (remove from dnsmasq config if active).

Source code in src/terok_shield/config.py
def deny_domain(self, domain: str) -> None:
    """Live-deny a domain (remove from dnsmasq config if active)."""
    ...

list_rules(container)

Return the current nft rules for a running container.

Source code in src/terok_shield/config.py
def list_rules(self, container: str) -> str:
    """Return the current nft rules for a running container."""
    ...

shield_down(container, *, allow_all=False)

Switch a container to bypass mode.

Source code in src/terok_shield/config.py
def shield_down(self, container: str, *, allow_all: bool = False) -> None:
    """Switch a container to bypass mode."""
    ...

shield_quarantine(container)

Total network blackout — drop all traffic.

Source code in src/terok_shield/config.py
def shield_quarantine(self, container: str) -> None:
    """Total network blackout — drop all traffic."""
    ...

shield_up(container)

Restore normal deny-all mode for a container.

Source code in src/terok_shield/config.py
def shield_up(self, container: str) -> None:
    """Restore normal deny-all mode for a container."""
    ...

shield_state(container)

Query a container's shield state from the live ruleset.

Source code in src/terok_shield/config.py
def shield_state(self, container: str) -> ShieldState:
    """Query a container's shield state from the live ruleset."""
    ...

preview(*, down=False, allow_all=False)

Generate the ruleset without applying it.

Source code in src/terok_shield/config.py
def preview(self, *, down: bool = False, allow_all: bool = False) -> str:
    """Generate the ruleset without applying it."""
    ...

detect_dns_tier(has, dnsmasq_nftset_ok=lambda: True, dnsmasq_state_readable=lambda: True)

Detect the best available DNS resolution tier.

Probes for executables in priority order: dnsmasq (with nftset support, and able to read its config) > dig > getent.

Parameters:

Name Type Description Default
has Callable[[str], bool]

Returns True if the named executable exists on PATH.

required
dnsmasq_nftset_ok Callable[[], bool]

Returns True if installed dnsmasq supports --nftset. Defaults to lambda: True (skip probe); production callers should pass a real capability check.

lambda: True
dnsmasq_state_readable Callable[[], bool]

Returns True if dnsmasq can read its config from the shield state directory. Returns False when an enforcing AppArmor profile confines dnsmasq away from it, so we fall back to dig rather than fail the launch. Defaults to lambda: True; production callers pass a real probe.

lambda: True
Source code in src/terok_shield/config.py
def detect_dns_tier(
    has: Callable[[str], bool],
    dnsmasq_nftset_ok: Callable[[], bool] = lambda: True,
    dnsmasq_state_readable: Callable[[], bool] = lambda: True,
) -> DnsTier:
    """Detect the best available DNS resolution tier.

    Probes for executables in priority order: dnsmasq (with nftset
    support, and able to read its config) > dig > getent.

    Args:
        has: Returns True if the named executable exists on PATH.
        dnsmasq_nftset_ok: Returns True if installed dnsmasq supports
            ``--nftset``.  Defaults to ``lambda: True`` (skip probe);
            production callers should pass a real capability check.
        dnsmasq_state_readable: Returns True if dnsmasq can read its
            config from the shield state directory.  Returns False when
            an enforcing AppArmor profile confines dnsmasq away from it,
            so we fall back to ``dig`` rather than fail the launch.
            Defaults to ``lambda: True``; production callers pass a real
            probe.
    """
    if has("dnsmasq") and dnsmasq_nftset_ok() and dnsmasq_state_readable():
        return DnsTier.DNSMASQ
    if has("dig"):
        return DnsTier.DIG
    return DnsTier.GETENT