Skip to content

hardening

hardening

Process self-hardening — shrink what a leaked address space can reveal.

A process that holds secret material (a vault DB session key, an SSH private key) wants four cheap kernel-level guarantees the moment it starts, before it opens anything sensitive:

  • No ptrace / no debugger attachprctl(PR_SET_DUMPABLE, 0) clears the dumpable flag, so another process in the same user (a compromised sibling) cannot ptrace the address space and cannot read /proc/<pid>/mem. It also stops the kernel writing a core dump for this process.
  • No core dumpssetrlimit(RLIMIT_CORE, 0) belt-and-braces the dumpable clear: even a SIGSEGV can't spill the heap (keys included) to a file on disk.
  • No swap-outmlockall(MCL_CURRENT | MCL_FUTURE) pins the pages into RAM so secret bytes never land in the swap file where they outlive the process. This one is best-effort: it needs CAP_IPC_LOCK or a generous RLIMIT_MEMLOCK and legitimately fails in a locked-down rootless container — a failure is reported, never raised.
  • No privilege gain on execprctl(PR_SET_NO_NEW_PRIVS, 1) bars this process and every descendant from ever gaining privilege through a setuid/setgid bit or file capabilities on exec. A child that is compromised while shelling out (the gate service runs git) cannot re-exec a setuid helper to climb out. It also unlocks installing an unprivileged seccomp filter, which the kernel permits only once no-new-privs is set. Unlike the dumpable clear, it never impedes a debugger attaching, so it holds even in debug mode.

harden_self applies all four to the current process and returns a HardeningReport of what took — the floor every isolated child process (terok-sandbox's split supervisor children) applies at start-up.

__all__ = ['HardeningReport', 'harden_self'] module-attribute

HardeningReport(no_dump, no_core, memory_locked, no_new_privs) dataclass

What harden_self managed to apply.

Each field is True only when the corresponding guarantee is in force for the current process. no_dump, no_core, and no_new_privs are expected to succeed; memory_locked routinely comes back False in a rootless container without CAP_IPC_LOCK and that is not an error — the caller decides whether to log it.

no_dump instance-attribute

no_core instance-attribute

memory_locked instance-attribute

no_new_privs instance-attribute

fully_hardened property

True when all four guarantees are in force.

harden_self(*, allow_debugger=False)

Apply the process-hardening floor to the current process.

Idempotent and side-effecting: clears the dumpable flag, zeroes the core-dump limit, and locks memory — each independently, so a failure of one (typically mlockall for lack of privilege) still lets the others take. Never raises; the returned HardeningReport says what held.

Call this as early as possible in a process that will hold secret material — before opening the credential store or binding a socket — so the sensitive bytes are only ever mapped under the guarantees.

allow_debugger leaves the dumpable flag set so a debugger, py-spy, or strace can attach — the escape hatch for running a task in debug mode. It trades away only the no-ptrace guarantee (no_dump reports False); the core-dump, swap-out, and no-new-privileges guarantees still apply (the last never impedes a debugger attaching).

Source code in src/terok_util/hardening.py
def harden_self(*, allow_debugger: bool = False) -> HardeningReport:
    """Apply the process-hardening floor to the current process.

    Idempotent and side-effecting: clears the dumpable flag, zeroes the
    core-dump limit, and locks memory — each independently, so a failure
    of one (typically ``mlockall`` for lack of privilege) still lets the
    others take.  Never raises; the returned
    [`HardeningReport`][terok_util.hardening.HardeningReport] says what
    held.

    Call this as early as possible in a process that will hold secret
    material — before opening the credential store or binding a socket —
    so the sensitive bytes are only ever mapped under the guarantees.

    *allow_debugger* leaves the dumpable flag set so a debugger, ``py-spy``,
    or ``strace`` can attach — the escape hatch for running a task in debug
    mode.  It trades away only the no-ptrace guarantee (``no_dump`` reports
    ``False``); the core-dump, swap-out, and no-new-privileges guarantees
    still apply (the last never impedes a debugger *attaching*).
    """
    libc = _libc()
    return HardeningReport(
        no_dump=False if allow_debugger else _clear_dumpable(libc),
        no_core=_zero_core_limit(),
        memory_locked=_lock_memory(libc),
        no_new_privs=_set_no_new_privs(libc),
    )