Skip to content

children

children

Per-service child runners — one hardened process per supervisor service.

The supervisor used to compose every service (vault proxy, SSH signer, git gate, clearance hub, verdict server) as coroutines in a single asyncio loop. That put secret-holding code (the vault's SQLCipher session key, the signer's private keys) in the same address space as convenience services (the desktop notifier), so a bug in any of them exposed all of them.

Each service now runs in its own process, launched by the parent supervisor via launch_child. A child does exactly one thing:

  1. harden_self — clear the dumpable flag, zero the core limit, lock memory — before it opens the credential store or binds a socket.
  2. Re-read the sidecar (the parent hands it the same path), rebuild the one service it owns, and run that service's asyncio loop.
  3. Await SIGTERM from the parent, then stop the service and exit 0.

The service classes are constructed and driven the standard way — only the process boundary is new. IPC is unchanged because every service already binds a per-container filesystem socket (or loopback port); a child in a separate process binds the identical path the container reaches.

The five children map onto the six former services: clearance owns the hub and the desktop notifier/subscriber (they share the clearance socket and the notifier only drives the subscriber), while verdict, vault, signer, and gate are one service each. gate only runs when the sidecar wired it.

SERVICE_NAMES = tuple(_RUNNERS) module-attribute

run_child(service, container_id, sidecar_path)

Harden, build the one service, run it until SIGTERM; return an exit code.

The synchronous entry the supervise-child CLI verb calls via asyncio.run. Arms the parent-death signal first (a supervisor that dies without teardown must never strand a running child), then loads the sidecar the parent pinned (config, not secrets), hardens the process before the runner opens the credential store or binds a socket — honouring the sidecar's debug-mode opt-out — then drives the single service's lifecycle. A start failure returns _EXIT_START_FAILED (4) so the parent can log it and carry on, degrading one service without taking the rest down.

Source code in src/terok_sandbox/supervisor/children.py
def run_child(service: str, container_id: str, sidecar_path: Path) -> int:
    """Harden, build the one *service*, run it until SIGTERM; return an exit code.

    The synchronous entry the ``supervise-child`` CLI verb calls via
    ``asyncio.run``.  Arms the parent-death signal first (a supervisor
    that dies without teardown must never strand a running child), then
    loads the sidecar the parent pinned (config, not secrets), hardens
    the process *before* the runner opens the credential store or binds
    a socket — honouring the sidecar's debug-mode opt-out — then drives
    the single service's lifecycle.  A start failure returns
    ``_EXIT_START_FAILED`` (4) so the parent can log it and carry on,
    degrading one service without taking the rest down.
    """
    if not _arm_parent_death_signal():
        _logger.error(
            "%s child: supervisor died before startup — refusing to run as a stray", service
        )
        return _EXIT_START_FAILED
    runner = _RUNNERS.get(service)
    if runner is None:
        _logger.error("unknown supervisor child service %r", service)
        return _EXIT_BAD_SIDECAR

    cfg = load_sidecar(sidecar_path)
    if cfg is None:
        _logger.error("%s child: no usable sidecar at %s", service, sidecar_path)
        return _EXIT_BAD_SIDECAR

    report = harden_self(allow_debugger=cfg.allow_debugger)
    if not cfg.allow_debugger and not report.fully_hardened:
        # Expected in a rootless container (mlockall needs CAP_IPC_LOCK);
        # log at debug so the operator can confirm the floor on hosts
        # where it should have taken.  (Debug mode drops no_dump on
        # purpose, so a partial report is not noteworthy there.)
        _logger.debug("%s child hardening partial: %s", service, report)

    paths = SupervisorPaths.for_container(
        container_id, cfg.container_name, sidecar_path, cfg.runtime_dir
    )
    return asyncio.run(_drive(service, runner, cfg, paths))