Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference FAQ
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
FAQ

Frequently Asked Questions

Questions that come up often enough on the issue tracker to be worth answering once, in public, with the exact commands.

Containers

Running Sandlock Inside Docker

Sandlock is built to stand in for the container, so the first question is whether you need one at all. If you do, the runtime's default seccomp profile gets a vote before Sandlock does, and that is the single most common startup failure.

Q1Sandlock is meant to replace the container. Isn't running it inside one redundant?

Often, yes. Sandlock exists so that confining a process needs no image, no runtime, no daemon, and no root. Starting a container in order to run a sandbox that did not require one is paying twice: two policy layers to keep in sync, and the container's own startup cost in front of a sandbox that takes about five milliseconds.

What people usually want from the container in this situation is the filesystem rather than the isolation, and Sandlock can give them that on its own:

  • --image python:3.12-slim takes a local Docker image and uses its flattened rootfs directly. The daemon is asked to export the image once and the result is cached; the workload itself runs as an ordinary Sandlock sandbox, with no container around it.
  • --chroot ./rootfs does the same for a directory you built yourself, unprivileged and with no bind mounts.
  • sandlock-oci speaks the OCI runtime interface, so containerd, CRI-O, and Kubernetes can run your existing images through Sandlock as a namespace-less, cgroup-less low-level runtime instead of through runc.

Two cases where nesting is genuinely worth it: you do not get to choose, because a CI system or an internal platform hands you a container and Sandlock has to live inside it; or you want the layers, so that a Sandlock policy mistake still lands inside a container boundary. Both are reasonable, and the rest of this section is how to make them work.

Q2Can I run Sandlock in a stock Docker container, with no capabilities added and no seccomp changes?

No. Sandlock's supervisor duplicates the child's file descriptors with pidfd_getfd, and Docker's default seccomp profile only permits that syscall when the container holds CAP_SYS_PTRACE. A stock container therefore needs one of two things: --cap-add SYS_PTRACE, or a modified profile that allows pidfd_getfd explicitly.

This is a gate in Docker's policy, not a kernel restriction. There is no need for --privileged or seccomp=unconfined: the kernel is willing, the runtime's profile is not. See moby/moby#45622 for the upstream discussion of that profile entry.

Q3Why does sandlock run fail on pidfd_getfd when sandlock check says the host is fine?

Because the two report on different things. sandlock check inspects what the kernel supports: the Landlock ABI level, seccomp user notification, and the protections built on them. All of that is genuinely present inside the container, so the check passes.

The seccomp profile the container runtime installed around your process is a separate layer, and it is the one that refuses the call. The result is a run that dies at startup with pidfd_getfd: Operation not permitted on a host that looks fully supported. Treat a successful check inside a container as a statement about the kernel only.

Q5Adding SYS_PTRACE feels insecure. Can I take the privilege back?

Yes, on the Sandlock side. The container now holds a capability that could, in principle, be used to inspect or manipulate other processes, so deny the syscalls that capability unlocks to the sandboxed workload with --extra-deny-syscall. The supervisor runs outside the sandbox and keeps its own use of pidfd_getfd; only the child is affected.

Hand the privilege back at the sandbox boundary
$ docker run --rm --cap-add SYS_PTRACE \
    -v /usr/local/bin/sandlock:/usr/local/bin/sandlock:ro \
    -v /tmp/your_program.py:/tmp/your_program.py:ro \
    python:3.12-slim sandlock run \
      --extra-deny-syscall ptrace \
      --extra-deny-syscall pidfd_getfd \
      --extra-deny-syscall process_vm_readv \
      --extra-deny-syscall process_vm_writev \
      -r / -w /tmp -- python3 /tmp/your_program.py

The four denials sit on top of the default blocklist, which always applies and cannot be turned off. -r / makes the whole container filesystem readable, which is convenient for a one-off and worth narrowing to the paths the program actually needs once you know them.

Q6Is there a way to avoid the capability entirely?

Yes, at a cost. --no-supervisor never calls pidfd_getfd, so it runs under Docker's default profile as is. What you get is layers one and two only: Landlock plus a kernel-only deny filter, with no supervisor process.

What you give up is everything the supervisor provides: IP allowlisting, HTTP ACLs, resource limits, copy-on-write, chroot mediation, /proc virtualization, and custom handlers. If your policy is purely filesystem grants and syscall denials, that trade is fine. If it depends on any runtime decision, add the capability instead. See Architecture for what each layer covers.

Q7Does this apply to Podman and Kubernetes too?

Any runtime that applies a seccomp profile derived from Docker's inherits the same gate, so podman run --cap-add SYS_PTRACE is the equivalent fix.

Kubernetes pods run seccomp-unconfined unless the pod selects RuntimeDefault or the kubelet was started with that as the default. Where the runtime default is in force, add the capability in the container's security context:

Pod spec under a RuntimeDefault seccomp profile
securityContext:
  capabilities:
    add: ["SYS_PTRACE"]

Rather than reason about which profile a given cluster ended up with, run one sandlock run and look at the result. The failure is immediate, unambiguous, and names the syscall.

Not answered here

Ask on the Issue Tracker

Questions asked in the open get answered in the open, and the good ones end up on this page.