ronly — read-only sandbox for untrustworthy agents

Drop into a shell session where everything looks normal but
destructive operations are blocked at the kernel level.

Linux only. Runs rootless via user namespaces on any
kernel 4.6+ (Debian 11+, RHEL 8+, Ubuntu 16.04+). Falls
back to privileged mode if user namespaces are unavailable.

  $ kubectl exec -it debug-pod -- ronly
  ronly: rootless (user namespaces)
  root@debug-pod:~# top -bn1 | head -5
  top - 14:23:01 up 42 days, load average: 2.31, 1.87
  Tasks: 312 total,   1 running, 311 sleeping
  %Cpu(s):  8.3 us,  2.1 sy,  0.0 ni, 89.1 id
  MiB Mem:  32168.0 total,  12042.3 free

  root@debug-pod:~# cat /var/log/syslog | grep error
  Mar 24 09:14:02 prod payment-svc: connection error

  root@debug-pod:~# rm /etc/hosts
  rm: cannot remove '/etc/hosts': Read-only file system

  root@debug-pod:~# kill 1
  bash: kill: (1) - Operation not permitted

  root@debug-pod:~# docker run nginx
  ronly: docker run is blocked (read-only session)

HOW IT WORKS

  1. Read-only filesystem — mount namespace with root
     bind-mounted read-only. Writes fail with EROFS.
     Writable tmpfs at /tmp for scratch space.

  2. PID namespace — host /proc mounted read-only. ps and
     top show real host processes. kill fails because
     target PIDs don't exist in the agent's namespace.

  3. seccomp-bpf — blocks kill, unlink, rename, truncate,
     mount, reboot. ptrace write ops blocked but read ops
     allowed (so strace works). Defense in depth.

  4. Tool shims — ronly copies itself into /tmp under
     names like "docker" and "kubectl" (one copy, rest are
     hard links) and prepends that dir to PATH. The shell
     finds the shim, ronly checks argv[0], and either execs
     the real binary (read-only subcommands) or exits with
     an error (write subcommands).

Network is not restricted. The agent can make outbound
connections. Layer your own network policy if exfiltration
is a concern.

USAGE

  ronly [OPTIONS] [COMMAND...]

With no arguments, execs $SHELL (or /bin/bash).

  --rootless           force rootless mode (user namespaces)
  --privileged         force privileged mode (requires root)
  --tmpfs-size SIZE    writable /tmp size (default: 64M)
  --extra-shims DIR    additional shim directory
  --no-shims           disable all shims
  --writable PATH      empty writable tmpfs at PATH
  --help               show this help
  --version            show version

By default, ronly tries rootless first and falls back to
privileged. It prints which mode it's using to stderr.

BUILT-IN SHIMS

Some tools talk to sockets/APIs rather than the filesystem,
so a read-only mount alone doesn't stop them.

  docker — ps, logs, inspect, stats, top, images, info,
    version, events, diff, network ls/inspect,
    volume ls/inspect

  kubectl — get, describe, logs, top, explain, version,
    cluster-info, api-resources, api-versions,
    config view/current-context/get-contexts,
    auth can-i/whoami

Everything not listed is blocked. Add custom shims with
--extra-shims DIR.
