diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fab230de4..9ce8f0f60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 | | **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 | -| **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 diff --git a/README.md b/README.md index b65121e77..018120389 100644 --- a/README.md +++ b/README.md @@ -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 | | **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 | -| **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 | | **User allowlists** | Default deny-all for messaging platforms; explicit allowlists or DM pairing required | diff --git a/tools/environments/docker.py b/tools/environments/docker.py index 8ac4f7c73..85184fde7 100644 --- a/tools/environments/docker.py +++ b/tools/environments/docker.py @@ -1,7 +1,8 @@ """Docker execution environment wrapping mini-swe-agent's DockerEnvironment. -Adds security hardening, configurable resource limits (CPU, memory, disk), -and optional filesystem persistence via `docker commit`/`docker create --image`. +Adds security hardening (cap-drop ALL, no-new-privileges, PID limits), +configurable resource limits (CPU, memory, disk), and optional filesystem +persistence via bind mounts. """ 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 = [ - "--read-only", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--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", "/run:rw,noexec,nosuid,size=64m", ] @@ -37,12 +40,13 @@ _storage_opt_ok: Optional[bool] = None # cached result across instances class DockerEnvironment(BaseEnvironment): """Hardened Docker container execution with resource limits and persistence. - Security: read-only root, all capabilities dropped, no privilege escalation, - PID limits, tmpfs for writable scratch. Writable overlay for /home and cwd - via tmpfs or bind mounts. + Security: all capabilities dropped, no privilege escalation, PID limits, + size-limited tmpfs for scratch dirs. The container itself is the security + 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 - cleanup, and the next creation restores from that image. + Persistence: when enabled, bind mounts preserve /workspace and /root + across container restarts. """ def __init__( @@ -114,9 +118,9 @@ class DockerEnvironment(BaseEnvironment): "--tmpfs", "/root:rw,exec,size=1g", ] - # All containers get full security hardening (read-only root + writable - # mounts for the workspace). Persistence uses Docker volumes, not - # filesystem layer commits, so --read-only is always safe. + # All containers get security hardening (capabilities dropped, no privilege + # escalation, PID limits). The container filesystem is writable so agents + # can install packages as needed. # User-configured volume mounts (from config.yaml docker_volumes) volume_args = [] for vol in (volumes or []):