Skip to content

_selinux

_selinux

SELinux helpers for socket labeling and policy management.

Terok services listen on Unix sockets that rootless Podman containers must connect() to. SELinux blocks this by default — the kernel's connectto check uses the socket object's SID (inherited from the creating process, typically unconfined_t), not the file inode's label.

To work around this without disabling confinement:

  1. A custom policy module defines terok_socket_t and grants container_t → terok_socket_t:unix_stream_socket connectto.
  2. Services call setsockcreatecon before socket() so the kernel assigns terok_socket_t to the socket object.
  3. After bind(), the socket object carries terok_socket_t and containers can connect.

The sock_file { write } check (file-level access) is separately handled by Podman's :z volume relabeling.

libselinux is loaded via ctypes at call time, so this module has no runtime dependency on the python3-libselinux distribution package — libselinux.so.1 from the base libselinux package is sufficient. All functions degrade gracefully on non-SELinux systems.

SELINUX_MODULE_NAME = 'terok_socket' module-attribute

Policy module the installer compiles and loads (semodule -l name).

SELINUX_SOCKET_TYPE = f'{SELINUX_MODULE_NAME}_t' module-attribute

Custom SELinux type applied to terok service sockets.

SelinuxStatus

Bases: Enum

Outcome of check_status — the single decision tree behind both terok setup's prereq check and terok sickbay's health check.

NOT_APPLICABLE_TCP_MODE = 'not_applicable_tcp_mode' class-attribute instance-attribute

Transport is tcp; the terok_socket_t policy is irrelevant.

NOT_APPLICABLE_PERMISSIVE = 'not_applicable_permissive' class-attribute instance-attribute

Socket transport, but SELinux is disabled or permissive.

POLICY_MISSING = 'policy_missing' class-attribute instance-attribute

Enforcing host, socket transport, but terok_socket module is not loaded.

POLICY_OUTDATED = 'policy_outdated' class-attribute instance-attribute

Enforcing host, socket transport, terok_socket loaded — but an older revision missing the container_runtime_t rule the per-container supervisor needs. Re-running the installer rebuilds + upgrades it.

LIBSELINUX_MISSING = 'libselinux_missing' class-attribute instance-attribute

Policy is loaded but libselinux.so.1 cannot be dlopen'd — silent- failure case where sockets would bind as unconfined_t regardless.

OK = 'ok' class-attribute instance-attribute

Enforcing, policy installed, libselinux loadable — all good.

action_needed property

Whether this status calls for an operator install.

SelinuxCheckResult(status, missing_policy_tools=tuple()) dataclass

Structured outcome of check_status.

Callers decide how to present the result; this struct only carries the decision tree's output so that terok setup (printed multi- line warnings) and terok sickbay (tuple-based check result) can share one source of truth for the branching.

status instance-attribute

Which branch of the decision tree fired.

missing_policy_tools = field(default_factory=tuple) class-attribute instance-attribute

Names of missing compile tools (only populated for POLICY_MISSING).

is_selinux_enforcing()

Return True if SELinux is in enforcing mode.

Reads /sys/fs/selinux/enforce directly — no external commands. Returns False on non-SELinux systems or if the file is unreadable.

Source code in src/terok_sandbox/_util/_selinux.py
def is_selinux_enforcing() -> bool:
    """Return ``True`` if SELinux is in enforcing mode.

    Reads ``/sys/fs/selinux/enforce`` directly — no external commands.
    Returns ``False`` on non-SELinux systems or if the file is unreadable.
    """
    try:
        return _ENFORCE_PATH.read_text().strip() == "1"
    except (FileNotFoundError, PermissionError, OSError):
        return False

is_selinux_enabled()

Return True if SELinux is active (enforcing or permissive).

Source code in src/terok_sandbox/_util/_selinux.py
def is_selinux_enabled() -> bool:
    """Return ``True`` if SELinux is active (enforcing or permissive)."""
    return _ENFORCE_PATH.is_file()

is_policy_installed()

Return True if terok_socket_t is a valid type in the loaded policy.

Uses libselinux's security_check_context(), which succeeds iff the context (and therefore the custom type) is known to the currently loaded policy — a pure userspace query requiring no subprocess and no privileges.

The previous semodule -l subprocess approach silently failed for non-root callers on Fedora, where /var/lib/selinux/.../active/ is root-readable only. terok sickbay and terok setup both run as the user, so they would always report the policy as missing even right after a successful install.

Source code in src/terok_sandbox/_util/_selinux.py
def is_policy_installed() -> bool:
    """Return ``True`` if ``terok_socket_t`` is a valid type in the loaded policy.

    Uses ``libselinux``'s ``security_check_context()``, which succeeds
    iff the context (and therefore the custom type) is known to the
    currently loaded policy — a pure userspace query requiring no
    subprocess and no privileges.

    The previous ``semodule -l`` subprocess approach silently failed
    for non-root callers on Fedora, where ``/var/lib/selinux/.../active/``
    is root-readable only.  ``terok sickbay`` and ``terok setup``
    both run as the user, so they would always report the policy as
    missing even right after a successful install.
    """
    lib = _load_libselinux()
    if lib is None:
        return False
    return lib.security_check_context(_SELINUX_CONTEXT.encode()) == 0

is_supervisor_socket_rule_loaded()

Whether the loaded policy lets container_runtime_t create terok_socket_t sockets.

The per-container supervisor binds its sockets from container_runtime_t (crun's OCI-hook domain), so this rule — added in policy v1.1 — must be in the loaded policy or the supervisor dies with EACCES on its first socket(). Probing it (via libselinux's unprivileged selinux_check_access query against the kernel AVC) distinguishes a stale v1.0 install (type present, rule absent) from a current one — which the bare type-presence check in is_policy_installed cannot see.

Returns True / False when the policy can be queried, or None when it can't be determined (libselinux absent or too old, or container_runtime_t undefined) — callers must treat None as "no opinion", never as "stale".

Source code in src/terok_sandbox/_util/_selinux.py
def is_supervisor_socket_rule_loaded() -> bool | None:
    """Whether the loaded policy lets ``container_runtime_t`` create ``terok_socket_t`` sockets.

    The per-container supervisor binds its sockets from
    ``container_runtime_t`` (crun's OCI-hook domain), so this rule —
    added in policy v1.1 — must be in the *loaded* policy or the
    supervisor dies with ``EACCES`` on its first ``socket()``.  Probing
    it (via libselinux's unprivileged ``selinux_check_access`` query
    against the kernel AVC) distinguishes a stale v1.0 install (type
    present, rule absent) from a current one — which the bare
    type-presence check in
    [`is_policy_installed`][terok_sandbox._util._selinux.is_policy_installed]
    cannot see.

    Returns ``True`` / ``False`` when the policy can be queried, or
    ``None`` when it can't be determined (libselinux absent or too old,
    or ``container_runtime_t`` undefined) — callers must treat ``None``
    as "no opinion", never as "stale".
    """
    lib = _load_libselinux()
    if lib is None:
        return None
    try:
        allowed = lib.selinux_check_access(
            _CONTAINER_RUNTIME_CONTEXT,
            _SELINUX_CONTEXT.encode(),
            b"unix_stream_socket",
            b"create",
            None,
        )
    except (AttributeError, ctypes.ArgumentError, OSError):
        return None
    if allowed == 0:
        return True
    # EACCES is a definitive "denied by policy" → the rule is absent.  Any
    # other errno (e.g. EINVAL: container_runtime_t undefined) means we
    # can't tell, so we decline to call it stale.
    return False if ctypes.get_errno() == errno.EACCES else None

is_libselinux_available()

Return True if libselinux.so.1 can be loaded via ctypes.

On SELinux-enforcing hosts, a False return is a silent-failure risk: service sockets would bind without terok_socket_t labeling, and container clients would be denied connectto even when the terok_socket policy module is installed.

Source code in src/terok_sandbox/_util/_selinux.py
def is_libselinux_available() -> bool:
    """Return ``True`` if ``libselinux.so.1`` can be loaded via ctypes.

    On SELinux-enforcing hosts, a ``False`` return is a silent-failure
    risk: service sockets would bind without ``terok_socket_t`` labeling,
    and container clients would be denied ``connectto`` even when the
    ``terok_socket`` policy module is installed.
    """
    return _load_libselinux() is not None

missing_policy_tools()

Return names of policy-compilation tools not found on PATH.

The terok_socket policy is compiled from its .te source at install time by install_policy, which requires all three of checkmodule, semodule_package, and semodule. An empty list means install_policy() will not fail with SystemExit for missing tools. Names are returned in invocation order so callers can surface the first one a user would hit.

Source code in src/terok_sandbox/_util/_selinux.py
def missing_policy_tools() -> list[str]:
    """Return names of policy-compilation tools not found on ``PATH``.

    The ``terok_socket`` policy is compiled from its ``.te`` source at
    install time by `install_policy`, which requires all three of
    ``checkmodule``, ``semodule_package``, and ``semodule``.  An empty
    list means ``install_policy()`` will not fail with ``SystemExit`` for
    missing tools.  Names are returned in invocation order so callers
    can surface the first one a user would hit.
    """
    return [t for t in ("checkmodule", "semodule_package", "semodule") if not find_host_tool(t)]

policy_source_path()

Return the path to the bundled terok_socket.te policy source.

Source code in src/terok_sandbox/_util/_selinux.py
def policy_source_path() -> Path:
    """Return the path to the bundled ``terok_socket.te`` policy source."""
    return Path(str(_resource_files("terok_sandbox.resources.selinux") / "terok_socket.te"))

install_script_path() cached

Return the path to the bundled install_policy.sh installer.

Installation is delegated to this short, inspectable shell script — which users run with sudo bash <path> — rather than a Python wrapper. Running Python as root imports a large dependency graph; a dedicated shell script can be cat-ed and audited in seconds before the privilege escalation.

Source code in src/terok_sandbox/_util/_selinux.py
@lru_cache(maxsize=1)
def install_script_path() -> Path:
    """Return the path to the bundled ``install_policy.sh`` installer.

    Installation is delegated to this short, inspectable shell script —
    which users run with ``sudo bash <path>`` — rather than a Python
    wrapper.  Running Python as root imports a large dependency graph;
    a dedicated shell script can be ``cat``-ed and audited in seconds
    before the privilege escalation.
    """
    return Path(str(_resource_files("terok_sandbox.resources.selinux") / "install_policy.sh"))

socket_selinux_context(selinux_type=SELINUX_SOCKET_TYPE)

Apply selinux_type as the creation context for sockets bound in this block.

Any socket() call within the with body produces a socket whose kernel SID is selinux_type, enabling container_t clients to connectto it once the matching policy is installed. The previous context is restored on exit.

No-op on non-SELinux systems or when libselinux.so.1 is absent.

Usage::

with socket_selinux_context():
    sock = socket.socket(AF_UNIX, SOCK_STREAM)
    sock.bind(str(path))
# socket object now carries terok_socket_t
Source code in src/terok_sandbox/_util/_selinux.py
@contextmanager
def socket_selinux_context(
    selinux_type: str = SELINUX_SOCKET_TYPE,
) -> Iterator[None]:
    """Apply *selinux_type* as the creation context for sockets bound in this block.

    Any ``socket()`` call within the ``with`` body produces a socket
    whose kernel SID is *selinux_type*, enabling ``container_t`` clients
    to ``connectto`` it once the matching policy is installed.  The
    previous context is restored on exit.

    No-op on non-SELinux systems or when ``libselinux.so.1`` is absent.

    Usage::

        with socket_selinux_context():
            sock = socket.socket(AF_UNIX, SOCK_STREAM)
            sock.bind(str(path))
        # socket object now carries terok_socket_t
    """
    if not is_selinux_enabled():
        yield
        return

    context = f"system_u:object_r:{selinux_type}:s0"
    old = _try_getsockcreatecon()
    _try_setsockcreatecon(context)
    try:
        yield
    finally:
        _try_setsockcreatecon(old)

check_status(*, services_mode)

Evaluate SELinux readiness for socket-transport services.

services_mode is the caller's configured transport (tcp or socket) — passed in rather than read from sandbox config so the helper stays free of cross-package config plumbing. Consumers (terok setup, terok sickbay) call terok_sandbox.config.services_mode themselves.

Source code in src/terok_sandbox/_util/_selinux.py
def check_status(*, services_mode: str) -> SelinuxCheckResult:
    """Evaluate SELinux readiness for socket-transport services.

    *services_mode* is the caller's configured transport (``tcp`` or
    ``socket``) — passed in rather than read from sandbox config so the
    helper stays free of cross-package config plumbing.  Consumers
    (``terok setup``, ``terok sickbay``) call
    [`terok_sandbox.config.services_mode`][terok_sandbox.config.services_mode] themselves.
    """
    if services_mode != "socket":
        return SelinuxCheckResult(SelinuxStatus.NOT_APPLICABLE_TCP_MODE)
    if not is_selinux_enforcing():
        return SelinuxCheckResult(SelinuxStatus.NOT_APPLICABLE_PERMISSIVE)
    if not is_policy_installed():
        return SelinuxCheckResult(
            SelinuxStatus.POLICY_MISSING,
            missing_policy_tools=tuple(missing_policy_tools()),
        )
    if not is_libselinux_available():
        return SelinuxCheckResult(SelinuxStatus.LIBSELINUX_MISSING)
    if is_supervisor_socket_rule_loaded() is False:
        # Type is present but the supervisor's container_runtime_t rule
        # isn't — a pre-supervisor (v1.0) policy still loaded.  Re-running
        # the installer rebuilds it; surface the same tool prerequisites.
        return SelinuxCheckResult(
            SelinuxStatus.POLICY_OUTDATED,
            missing_policy_tools=tuple(missing_policy_tools()),
        )
    return SelinuxCheckResult(SelinuxStatus.OK)