What Hawk taught me about trusting containers
Running other people's game servers is an exercise in assuming everything inside the box is hostile. Here is the checklist I ended up with.
Hawk gives people a button that runs arbitrary code on my infrastructure. That is the product. There is no version of it where I get to assume good intent.
A container is not a sandbox
This is the sentence I wish someone had put in front of me earlier. A container is a packaging and resource boundary that happens to provide some isolation. It is not a security boundary in the way a VM is — and every default is tuned for developer convenience, not for hosting strangers.
The practical consequences:
- Drop every capability, add back what you need.
--cap-drop=ALLfirst. Most game servers need none of them. - Never mount the Docker socket. If a feature seems to need it, the feature is wrong.
- Read-only root filesystem with explicit writable volumes for save data and configs.
- Hard resource ceilings on memory, CPU quota, pids and disk. An unbounded fork loop should hurt one tenant.
- No shared network namespace. Per-server networks, explicit port publishing, no host mode.
The console is the scariest surface
A live console over WebSockets is the feature people actually want, and it is a direct write channel into a process on my hardware. Three rules held up:
- Authorise per message, not per connection. Permissions change while a socket is open.
- The socket attaches to the process stdin, never to a shell.
- Rate limit input server-side. A pasted 200KB payload should not become the container's problem.
socket.on('stdin', async (payload) => {
const allowed = await can(session.userId, 'server:console:write', serverId)
if (!allowed) return socket.close(4403, 'forbidden')
if (payload.length > 2048) return // silently drop, log the attempt
await attachStream.write(payload)
})
Where it got fun
Output is the easy half — until you have a server logging thousands of lines a second and a browser tab that cannot keep up. Backpressure without dropping the interesting lines meant batching by frame instead of by line: coalesce on a 50ms tick, cap the buffer, and tell the client explicitly when lines were elided rather than quietly losing them.
That last part is the pattern I keep coming back to across every project. When the system has to degrade, say so out loud. Silent degradation is how you lose someone's trust permanently.