Skip to content

Hardening a node

For untrusted tenants (the hosting profile). Without any of this, game containers already run with:

  • all capabilities dropped and no-new-privileges
  • Docker’s seccomp profile
  • limits for memory (without swap), CPU and processes
  • one network per tenant: tenants can’t reach each other
A node's isolation settingsA node's isolation settings

Install and update jobs run as root (installers write any file of the server), but keep only the capabilities for that: changing owners and permissions of files, and switching users. No raw sockets, device nodes or chroot.

The node’s page shows under Isolation what its Docker offers; the agent reports it on every connect. The agent itself never needs more than the Docker socket: everything below is set up once by the operator. Changes apply to each server at its next start.

Root in a container is then not root on the node. In /etc/docker/daemon.json:

{ "userns-remap": "default" }

Then systemctl restart docker and recreate the agent. The agent must manage files as the node’s root, so it runs with userns_mode: host (deploy/compose.agent.yml and the Add node command already set it).

What changes:

  • Docker keeps images, containers and volumes per remapping (/var/lib/docker/100000.100000): images are pulled again, and Steam sign-ins have to be done again.
  • Server files belong to the shifted user on the node (988 → 100988). The agent detects this at start and re-owns existing servers once; it remembers the owner in /var/lib/panel-agent/file-owner.

A switch on the node’s page; on by default for new nodes with the hosting profile.

Game containers get a read-only root filesystem. Their files, /tmp (256 MB) and /var/tmp stay writable. Images that write elsewhere don’t start this way: switch it off for that node, or fix the image.

Optional, a switch on the node’s page. Game servers then run on gVisor’s own kernel: the strongest isolation, with some CPU and network overhead. Install it on the node (Debian, Ubuntu):

Terminal window
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc
sudo runsc install # adds the "runsc" runtime to /etc/docker/daemon.json
sudo systemctl restart docker

Restart the agent, so it reports the new runtime; the switch is refused while the node has none.

gVisor can’t use Docker’s own name server on the tenant networks, so the agent gives gVisor containers its own: 1.1.1.1 and 9.9.9.9, or what AGENT_SANDBOX_DNS sets (e.g. your router).

Game containers should not reach the node’s own services (SSH, databases, other published ports) or the networks around it (LAN, VPN, the cloud’s metadata service).

Tenant networks use bridge interfaces named pnl…. deploy/node-isolation.nft blocks that traffic and leaves players and replies alone:

Terminal window
sudo install -m 644 deploy/node-isolation.nft /etc/nftables.d/panel-isolation.nft
sudo nft -f /etc/nftables.d/panel-isolation.nft # and include it from /etc/nftables.conf for reboots
  • If the node’s DNS resolver is in the LAN (e.g. the router), allow it in the file first (the commented line), or game servers can’t resolve names.
  • Networks created before keep Docker’s br-… names until no server uses them; remove them then with docker network rm.