Skip to content
All writing
April 2, 20262 min read

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.

DockerSecurityHawk

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=ALL first. 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:

  1. Authorise per message, not per connection. Permissions change while a socket is open.
  2. The socket attaches to the process stdin, never to a shell.
  3. 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.