OpenHands default setup runs its worker containers with `privileged: true` and mounts `/var/run/docker.sock` from the host. This is a full breakout risk.
If you're trying to lock it down, you'll hit permission errors. The container's user (`openhands-worker`, UID 1001) isn't in the host's `docker` group. Even if you add it, the group ID inside the container won't match the host's `docker` group GID (typically 998).
Quick fix is to run the worker as root (bad) or set the host docker.sock to world-readable (worse).
Proper fix requires building a custom worker image:
* Ensure the `openhands-worker` user has GID matching host's `docker` group (e.g., 998).
* Add the user to the `docker` group inside the image.
* Run as non-root `USER openhands-worker`.
* Mount socket read-only.
Example Dockerfile addition:
```dockerfile
RUN groupadd -g 998 docker &&
usermod -aG docker openhands-worker
USER openhands-worker
```
Then update your compose to mount socket read-only and drop `privileged: true`.
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
securityContext:
privileged: false
```
Their default posture is wide open. You have to rebuild to secure it.
/root
USER nobody
Good catch on the GID mismatch. It's a classic side effect of not treating the container as part of the host's security domain.
You can also manage this by modifying the host, creating a docker group with the same GID you use inside your custom image. That keeps everything synchronized if you're deploying across multiple nodes.
But honestly, needing a custom build just to avoid a privileged container is a big red flag for their default configuration. It pushes the security burden entirely onto the user.
Isolate everything.
That's exactly the kind of default config that makes me wonder what their threat model even is. They've basically shipped the 'exploit me' preset. Building a custom image is the right path, but it feels like a workaround for a design flaw.
I tried the group sync approach on a small cluster and it's brittle. If the host docker GID differs, which it often does between fresh installs and cloud images, your container breaks again. So you're either baking a specific GID into your image, which won't be portable, or you're forcing a specific GID on the host, which might conflict with something else.
Why didn't they just use a socket proxy like docker-socket-proxy? It lets you expose specific API endpoints to the container without giving it the whole socket. Then you could run it completely unprivileged.