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

WILDCARDS_NEED_LIVE_TIER = 'Wildcard entries need live DNS resolution, and this host runs the {tier} tier: {names}. Use an allowlist without wildcard entries, or install dnsmasq with nftset support.' module-attribute

Launch refusal for a *. entry on a tier that resolves names once.

DnsTier

Bases: Enum

How domain allowlists reach the nft allow sets.

dnsmasq with --nftset adds every answered address to the

allow sets before the reply reaches the workload. Follows IP rotation, covers subdomains, accepts wildcard entries, names blocked domains.

DNSMASQ_STATIC: a dnsmasq built without nftset support. The allow sets are resolved once at launch; the query log still names blocked domains. LOOKUP: no dnsmasq. The allow sets are resolved once at launch with dig or drill. GETENT: no dnsmasq and no lookup tool. The allow sets are resolved once at launch with getent hosts.

DNSMASQ_LIVE = 'dnsmasq-live' class-attribute instance-attribute

DNSMASQ_STATIC = 'dnsmasq-static' class-attribute instance-attribute

LOOKUP = 'lookup' class-attribute instance-attribute

GETENT = 'getent' class-attribute instance-attribute

live property

True when the allow sets follow DNS answers as they arrive.

runs_dnsmasq property

True when a per-container dnsmasq serves the container's DNS.

hint property

What a degraded tier lacks and what restores the live one; empty for the live tier.

parse(recorded) classmethod

The tier a recorded name means; None for a name that is not a tier.

A retired name reads as the tier it named, so a container recorded under it restarts instead of being recreated.

Source code in src/terok_shield/config.py
@classmethod
def parse(cls, recorded: str) -> DnsTier | None:
    """The tier a recorded name means; ``None`` for a name that is not a tier.

    A retired name reads as the tier it named, so a container recorded under
    it restarts instead of being recreated.
    """
    try:
        return cls(_RETIRED_TIER_NAMES.get(recorded, recorded))
    except ValueError:
        return None

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: Accept-by-default posture, private-range protection retained (RFC 1918 + RFC 4193). DISENGAGED: Accept-everything posture — no deny set, no private-range or hard-deny reject. 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=(), loopback_ports=(), audit_enabled=True, profiles_dir=None, runtime=ShieldRuntime.DEFAULT, dns_cache_dir=None, dnsmasq_path=None) 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 = () class-attribute instance-attribute

Profiles to compose when a call's profiles argument is None; empty composes no profile.

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

dns_cache_dir = None class-attribute instance-attribute

Resolved-allowlist cache shared across containers.

The one deliberate exception to the state_dir-only rule: many tasks with the same allowlist share one resolve. None selects dns_cache_dir under the shield state root. Only the tiers that resolve at launch use it.

dnsmasq_path = None class-attribute instance-attribute

The dnsmasq binary to run; None finds one on the current host PATH.

Set it for a dnsmasq built outside the distro package, for example one built with nftset support in the operator's home.

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, posture transitions, and preview.

pre_start(container, profiles, *, security_deny=(), provider_allow=(), project_allow=(), override=())

Prepare for container start; return extra podman args.

security_deny / provider_allow / project_allow / override are the caller-generated t20 / t30 / t40 / t10 tiers (see Shield.pre_start).

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

    *security_deny* / *provider_allow* / *project_allow* / *override* are the
    caller-generated t20 / t30 / t40 / t10 tiers
    (see [`Shield.pre_start`][terok_shield.Shield.pre_start]).
    """
    ...

refresh(container, profiles, *, security_deny=(), provider_allow=(), project_allow=(), override=())

Recompute an existing container's policy bundle before a plain restart.

Same tier data as pre_start, no launch half — rewrites tiers, caches, and pre-applied artifacts only.

Source code in src/terok_shield/config.py
def refresh(
    self,
    container: str,
    profiles: list[str],
    *,
    security_deny: Sequence[str] = (),
    provider_allow: Sequence[str] = (),
    project_allow: Sequence[str] = (),
    override: Sequence[str] = (),
) -> None:
    """Recompute an existing container's policy bundle before a plain restart.

    Same tier data as
    [`pre_start`][terok_shield.config.ShieldModeBackend.pre_start], no
    launch half — rewrites tiers, caches, and pre-applied artifacts only.
    """
    ...

resolve(*, force=False)

Re-resolve the authored policy into its static-resolution caches.

Rewrites no tier; force re-resolves even when a cache is fresh.

Source code in src/terok_shield/config.py
def resolve(self, *, force: bool = False) -> list[str]:
    """Re-resolve the authored policy into its static-resolution caches.

    Rewrites no tier; *force* re-resolves even when a cache is fresh.
    """
    ...

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(container, domain)

Live-allow a domain (reload dnsmasq if active).

Source code in src/terok_shield/config.py
def allow_domain(self, container: str, domain: str) -> None:
    """Live-allow a domain (reload dnsmasq 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(container, domain)

Live-deny a domain (reload dnsmasq if active).

Source code in src/terok_shield/config.py
def deny_domain(self, container: str, domain: str) -> None:
    """Live-deny a domain (reload dnsmasq 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, *, disengaged=False)

Switch a container to the DOWN posture.

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

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_reset(container)

Forget learned allow-set state, keeping the authored policy seeds.

Source code in src/terok_shield/config.py
def shield_reset(self, container: str) -> None:
    """Forget learned allow-set state, keeping the authored policy seeds."""
    ...

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, disengaged=False)

Generate the ruleset without applying it.

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

detect_dns_tier(has, *, dnsmasq_usable=False, nftset=False)

The best tier the host supports.

Parameters:

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

Says whether a named tool exists on the host (dig, drill).

required
dnsmasq_usable bool

A dnsmasq binary was found and can read its config from the state directory.

False
nftset bool

That dnsmasq is built with nftset support.

False
Source code in src/terok_shield/config.py
def detect_dns_tier(
    has: Callable[[str], bool], *, dnsmasq_usable: bool = False, nftset: bool = False
) -> DnsTier:
    """The best tier the host supports.

    Args:
        has: Says whether a named tool exists on the host (``dig``, ``drill``).
        dnsmasq_usable: A dnsmasq binary was found and can read its config
            from the state directory.
        nftset: That dnsmasq is built with nftset support.
    """
    if dnsmasq_usable:
        return DnsTier.DNSMASQ_LIVE if nftset else DnsTier.DNSMASQ_STATIC
    if has("dig") or has("drill"):
        return DnsTier.LOOKUP
    return DnsTier.GETENT