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_SOCKET_TYPE = 'terok_socket_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.

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 shutil.which(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"))

install_command()

Return the full sudo bash <path> shell command for the installer.

Single source for the command string so the setup hint, the sickbay check, and any future caller all render the same invocation.

Source code in src/terok_sandbox/_util/_selinux.py
def install_command() -> str:
    """Return the full ``sudo bash <path>`` shell command for the installer.

    Single source for the command string so the setup hint, the sickbay
    check, and any future caller all render the same invocation.
    """
    return f"sudo bash {install_script_path()}"

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)