The sandbox
With sandbox = TRUE, the child R process runs under
restrictions set by the operating system. It can read what R needs to
run and write to a temp directory. It can’t use the network or touch the
rest of your files. Each platform does this differently, as described
below.
macOS (Seatbelt)
securer writes a Seatbelt profile and starts R with
sandbox-exec. The profile denies everything by default,
then allows a short list of things R needs. The child can read R itself,
your R library paths, system libraries, and temp directories, but not
your home directory, so files like ~/.ssh stay out of
reach. It can write only to temp directories. TCP and UDP are blocked.
Unix domain sockets are allowed, because that’s how the child talks to
your session.
session <- SecureSession$new(sandbox = TRUE)
# Computation works normally:
session$execute("1 + 1")
#> [1] 2
# Network access is blocked:
session$execute('readLines(url("http://example.com"))')
#> Error: Operation not permitted
# Writing outside temp is blocked:
session$execute('writeLines("test", "~/file.txt")')
#> Error: Operation not permitted
session$close()Linux (bubblewrap)
securer starts R with bwrap, which gives the child its
own namespaces for processes, network, users, mounts, host name, and
IPC. System libraries and R are mounted read-only, and /tmp
is an empty, writable in-memory directory. Because the child has its own
empty network namespace, it has no network.
You need bwrap installed
(apt install bubblewrap on Debian and Ubuntu). Without it,
securer warns you and runs the code without a sandbox.
Windows
Windows has no sandbox like the ones above. securer gives the child a
clean environment: it keeps only a short list of safe variables, empties
R_LIBS_USER, and points HOME,
TMPDIR, TMP, and TEMP at a
private temp directory. It can also set memory, CPU, and process limits
with a Job Object. It can’t restrict files or the network, and it prints
a message saying so the first time you start a session. If you need
those restrictions on Windows, set
SECURER_SANDBOX_MODE=docker-spawn to run each session in
its own Docker container, or use a Linux VM.
Resource limits
Resource limits cap what the child process can use. They’re set with
ulimit and work with or without the sandbox:
The limits you can set:
| Name | Unit | ulimit flag | Description |
|---|---|---|---|
cpu |
seconds | -t |
CPU time |
memory |
bytes | -v |
Virtual address space |
fsize |
bytes | -f |
Maximum file size |
nproc |
count | -u |
Maximum processes |
nofile |
count | -n |
Maximum open files |
stack |
bytes | -s |
Stack size |
If you use the sandbox and don’t pass limits, securer
applies these defaults. Pass limits = list() to turn them
off:
Code pre-validation
validate_code() checks code for syntax errors and for
calls that look risky, before the code goes to the child process:
# Valid code
validate_code("1 + 1")
# Syntax error
validate_code("if (TRUE {")
# Risky call: a warning, not a block
validate_code("system('ls')")Checking the sandbox is available
Before you deploy, check that the sandbox tools exist on the machine.
If they’re missing, securer runs code without a sandbox, or stops with
an error if sandbox_strict = TRUE:
Strict sandbox mode
By default, if the sandbox tools aren’t there, securer warns you and
runs the code without a sandbox. In production you probably want it to
stop instead. Set sandbox_strict = TRUE along with
sandbox = TRUE and the session fails to start if it can’t
set up the sandbox:
# Error if sandbox not available (recommended for production)
session <- SecureSession$new(sandbox = TRUE, sandbox_strict = TRUE)With sandbox_strict = FALSE, the default, you get a
warning and the session runs without the OS sandbox. Resource limits,
the socket checks, and the clean environment still apply.
Strict mode doesn’t stop a session on Windows, because the clean environment there counts as the sandbox.
Architecture
securer starts the child through a wrapper script, passed to callr
with callr::r_session_options(arch = ...). The socket lives
under /tmp, because macOS limits socket paths to about 104
characters and R’s own temp directory can be longer than that.
vignette("security-model") goes into the message format and
what each part of the system trusts.
Why not just callr or Docker?
callr runs code in a separate R process, so a crash there doesn’t take down your session. But it doesn’t restrict anything. The child can read your SSH keys, make HTTP requests, and write anywhere on disk. securer uses callr to start the child and adds the restrictions.
Docker isolates the filesystem, network, and processes well, but it needs a running daemon and an image, and containers are slower to start. It also has no way for the code inside to call back into your R session. You’d have to build that yourself. securer’s docker-spawn backend does use Docker, with a new container per session, for when you need it, such as on Windows.
By default securer uses the sandbox your operating system already has (Seatbelt on macOS, bubblewrap on Linux). There’s no daemon, sessions start in under a second, and tool calls, limits, timeouts, and the audit log come with it.
| Feature | callr | Docker | securer |
|---|---|---|---|
| Process isolation | Yes | Yes | Yes |
| Filesystem restrictions | No | Yes | Yes |
| Network restrictions | No | Yes | Yes |
| Tool-call IPC | No | No | Yes |
| Resource limits | No | Yes | Yes |
| Audit logging | No | No | Yes |
| No daemon required | Yes | No | Yes |
| Sub-second startup | Yes | No | Yes |