Threat Model
Hermetic is a userspace, in-process Python sandbox. It is not a security boundary. This page exists so you can decide whether hermetic fits your problem before you depend on it.
What hermetic is good for
- Yourself. Guaranteeing that a unit test really is hermetic — even on a developer laptop where the API is offline anyway, even in CI where the network policy might be looser than you assume. The test fails loudly when something tries to phone home.
- Poorly-behaved-but-not-malicious plugins. A library you
depend on that decides to fetch a remote schema, or write
.cache/into$HOME, or shell out togitwhen you didn't ask it to. Hermetic catches these and tells you which API the library reached for. - Poorly-behaved-but-not-malicious LLMs. The agent you handed a
Python interpreter that decides "I'll just
pip installthat real quick" or fetches a URL outside its allow-list. - Defense in depth alongside a real sandbox (container,
seccomp,gVisor). Hermetic catches policy violations earlier and with better error messages than the kernel, which makes debugging much faster.
What hermetic is not good for
- A determined attacker with arbitrary Python code execution inside your interpreter. They can bypass hermetic. See "known bypass classes" below.
- Sandboxing untrusted user input. If you're tempted to use
hermetic to safely
eval()user-supplied Python, don't. Use a real sandbox. - Running known-malicious code. Anything written specifically to defeat hermetic will defeat it. The escape hatches are well understood and documented.
Assumed attacker capabilities
Inside the same interpreter, hermetic assumes the attacker can:
- Call any Python API.
- Read process memory by ordinary Python means
(
gc.get_objects(),inspect,__dict__walking). - Import any installed module.
- Write files anywhere
--fs-readonlydoesn't cover.
Hermetic assumes the attacker cannot:
- Produce machine code from scratch.
- Escape Python's bytecode VM via memory corruption.
- Modify the interpreter binary on disk.
If your attacker has the second or third capability, no userspace guard helps anyway.
Known bypass classes
Honest enumeration of what hermetic does not stop. Most are mitigated to some degree but not eliminated.
Native code / FFI
- Direct
ctypescalls — fully mitigated only when--block-nativeis on. Without it, ten lines ofctypesreachsystem(3)directly. - Already-loaded native extensions — once a
.so/.pydis in memory, hermetic can't evict it.--block-nativeprevents new native loads. _ctypesprivate internals — partially patched but not exhaustive. Determined code can rebuildCDLLfrom_ctypes._SimpleCDataplus_FuncPtr.
Captured references
- Any module that did
from socket import socket(orfrom subprocess import Popen, etc.) before hermetic installed holds the original callable in its dict. Hermetic's monkey-patches replace the attribute on the source module, not every existing reference.urllib3keeps an internal pool of socket references.requestsdoesn't. - Mitigation: hermetic should be installed as early as possible
in the process. The CLI runner installs guards before
importing the target. The bootstrap-mode
sitecustomizeruns before any user code.
Patch removal
hermetic.guards.uninstall_all()is publicly importable. Attacker code can call it, restoring the originals from hermetic's own module globals.- Mitigation:
--seal/sealed=True. Once sealed, the uninstall path is a no-op for the rest of the process. Not bulletproof — a determined attacker can still walkgc.get_objects()to find the originals — but it raises the bar from one line to many. See Sealed Mode.
Network: residual surface
_socket.socketdirect construction — the C-level base class isn't patched. (Patching it causes infinite recursion insocket.socket.__init__.) An attacker who knows this gets a raw socket, but still has to do their own DNS, which--no-networkblocks viagetaddrinfo.- DNS-over-HTTPS through an allow-listed domain — out of scope. If you allow a CDN that fronts a public DoH resolver, any host is reachable by name.
- Bind for inbound exfil — partially mitigated; binds to
non-loopback interfaces are denied when
--no-networkis on.
Subprocess: residual surface
- Captured
subprocess.Popenreferences — same class as the socket case. - C extensions that call
CreateProcessW/fork(2)directly — the Python-visible primitives_winapi.CreateProcess(Windows) and_posixsubprocess.fork_exec(POSIX) are patched, but a third-party native extension that links the syscall directly bypasses Python entirely.--block-nativemitigates by preventing such an extension from loading. pty.fork— patched.os.fork— patched.os.forkpty— patched.posix.fork(the C-level alias) — not patched separately;os.forkpatching covers most callers.
Filesystem: residual surface
- Symlink TOCTOU — between
realpathandopen, an attacker with write access could swap a symlink. Not mitigated. scandir/listdiroutside the root — the open path is constrained, but directory listings may reveal the existence of out-of-root files. Their contents are still protected.
Defense-in-depth recipe
If you have an actual untrusted-code problem, layer:
- OS sandbox (container,
gVisor, Windows Sandbox, AppArmor,seccomp). This is the actual security boundary. - Hermetic with
--sealinside the sandbox. Loud failures, readable error messages, fast feedback. - A capability-passing protocol at the interface — give the tool what it needs, not access to what it might need.
If you have an "accidental side-effects" problem, hermetic alone is fine.
Reading further
spec/secure_secure.mdin the repo — full security review and hardening plan, with a phased roadmap.exploits/directory — concrete bypass demonstrations.- Security page — reporting and disclosure policy.
- Sealed Mode — opt-in irreversible install.