fix(docker): remove --read-only and allow exec on /tmp for package installs

The Docker sandbox previously used --read-only on the root filesystem and
noexec on /tmp. This broke 30+ skills that need to install packages:
- npm install -g (codex, claude-code, mcporter, powerpoint)
- pip install (20+ mlops/media/productivity skills)
- apt install (minecraft-modpack-server, ml-paper-writing)
- Build tools that compile in /tmp (pip wheels, node-gyp)

The container is already fully isolated from the host. Industry standard
(E2B, Docker Sandboxes, OpenAI Codex) does not use --read-only — the
container itself is the security boundary.

Retained security hardening:
- --cap-drop ALL (zero capabilities)
- --security-opt no-new-privileges (no escalation)
- --pids-limit 256 (no fork bombs)
- Size-limited tmpfs for /tmp, /var/tmp, /run
- nosuid on all tmpfs mounts
- noexec on /var/tmp and /run (rarely need exec there)
- Resource limits (CPU, memory, disk)
- Ephemeral containers (destroyed after use)

Fixes #189.
This commit is contained in:
teknium1 2026-03-02 01:09:34 -08:00
parent e265006fd6
commit 866fd9476b
3 changed files with 19 additions and 15 deletions

View file

@ -411,7 +411,7 @@ Hermes has terminal access. Security matters.
| **Write deny list** | Protected paths (`~/.ssh/authorized_keys`, `/etc/shadow`) resolved via `os.path.realpath()` to prevent symlink bypass | | **Write deny list** | Protected paths (`~/.ssh/authorized_keys`, `/etc/shadow`) resolved via `os.path.realpath()` to prevent symlink bypass |
| **Skills guard** | Security scanner for hub-installed skills (`tools/skills_guard.py`) | | **Skills guard** | Security scanner for hub-installed skills (`tools/skills_guard.py`) |
| **Code execution sandbox** | `execute_code` child process runs with API keys stripped from environment | | **Code execution sandbox** | `execute_code` child process runs with API keys stripped from environment |
| **Container hardening** | Docker: read-only root, all capabilities dropped, no privilege escalation, PID limits | | **Container hardening** | Docker: all capabilities dropped, no privilege escalation, PID limits, size-limited tmpfs |
### When contributing security-sensitive code ### When contributing security-sensitive code

View file

@ -769,7 +769,7 @@ Hermes includes multiple layers of security beyond sandboxed terminals and exec
| **Write deny list with symlink resolution** | Protected paths (`~/.ssh/authorized_keys`, `/etc/shadow`, etc.) are resolved via `os.path.realpath()` before comparison, preventing symlink bypass | | **Write deny list with symlink resolution** | Protected paths (`~/.ssh/authorized_keys`, `/etc/shadow`, etc.) are resolved via `os.path.realpath()` before comparison, preventing symlink bypass |
| **Recursive delete false-positive fix** | Dangerous command detection uses precise flag-matching to avoid blocking safe commands | | **Recursive delete false-positive fix** | Dangerous command detection uses precise flag-matching to avoid blocking safe commands |
| **Code execution sandbox** | `execute_code` scripts run in a child process with API keys and credentials stripped from the environment | | **Code execution sandbox** | `execute_code` scripts run in a child process with API keys and credentials stripped from the environment |
| **Container hardening** | Docker containers run with read-only root, all capabilities dropped, no privilege escalation, PID limits | | **Container hardening** | Docker containers run with all capabilities dropped, no privilege escalation, PID limits, size-limited tmpfs |
| **DM pairing** | Cryptographically random pairing codes with 1-hour expiry and rate limiting | | **DM pairing** | Cryptographically random pairing codes with 1-hour expiry and rate limiting |
| **User allowlists** | Default deny-all for messaging platforms; explicit allowlists or DM pairing required | | **User allowlists** | Default deny-all for messaging platforms; explicit allowlists or DM pairing required |

View file

@ -1,7 +1,8 @@
"""Docker execution environment wrapping mini-swe-agent's DockerEnvironment. """Docker execution environment wrapping mini-swe-agent's DockerEnvironment.
Adds security hardening, configurable resource limits (CPU, memory, disk), Adds security hardening (cap-drop ALL, no-new-privileges, PID limits),
and optional filesystem persistence via `docker commit`/`docker create --image`. configurable resource limits (CPU, memory, disk), and optional filesystem
persistence via bind mounts.
""" """
import logging import logging
@ -19,13 +20,15 @@ logger = logging.getLogger(__name__)
# Security flags applied to every container # Security flags applied to every container.
# The container itself is the security boundary (isolated from host).
# We drop all capabilities, block privilege escalation, and limit PIDs.
# /tmp is size-limited and nosuid but allows exec (needed by pip/npm builds).
_SECURITY_ARGS = [ _SECURITY_ARGS = [
"--read-only",
"--cap-drop", "ALL", "--cap-drop", "ALL",
"--security-opt", "no-new-privileges", "--security-opt", "no-new-privileges",
"--pids-limit", "256", "--pids-limit", "256",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=512m", "--tmpfs", "/tmp:rw,nosuid,size=512m",
"--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m", "--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",
"--tmpfs", "/run:rw,noexec,nosuid,size=64m", "--tmpfs", "/run:rw,noexec,nosuid,size=64m",
] ]
@ -37,12 +40,13 @@ _storage_opt_ok: Optional[bool] = None # cached result across instances
class DockerEnvironment(BaseEnvironment): class DockerEnvironment(BaseEnvironment):
"""Hardened Docker container execution with resource limits and persistence. """Hardened Docker container execution with resource limits and persistence.
Security: read-only root, all capabilities dropped, no privilege escalation, Security: all capabilities dropped, no privilege escalation, PID limits,
PID limits, tmpfs for writable scratch. Writable overlay for /home and cwd size-limited tmpfs for scratch dirs. The container itself is the security
via tmpfs or bind mounts. boundary the filesystem inside is writable so agents can install packages
(pip, npm, apt) as needed. Writable workspace via tmpfs or bind mounts.
Persistence: when enabled, `docker commit` saves the container state on Persistence: when enabled, bind mounts preserve /workspace and /root
cleanup, and the next creation restores from that image. across container restarts.
""" """
def __init__( def __init__(
@ -114,9 +118,9 @@ class DockerEnvironment(BaseEnvironment):
"--tmpfs", "/root:rw,exec,size=1g", "--tmpfs", "/root:rw,exec,size=1g",
] ]
# All containers get full security hardening (read-only root + writable # All containers get security hardening (capabilities dropped, no privilege
# mounts for the workspace). Persistence uses Docker volumes, not # escalation, PID limits). The container filesystem is writable so agents
# filesystem layer commits, so --read-only is always safe. # can install packages as needed.
# User-configured volume mounts (from config.yaml docker_volumes) # User-configured volume mounts (from config.yaml docker_volumes)
volume_args = [] volume_args = []
for vol in (volumes or []): for vol in (volumes or []):