Solitary

Restrict network access

Give a cell only the egress it needs.

By default, a cell can reach what the host can reach. Add network.allow when a task should have a default-deny network policy.

Define an allow list

network:
  allow:
    - github.com
    - objects.githubusercontent.com
    - registry.npmjs.org
    - 10.1.2.0/24

A domain covers its subdomains, so github.com reaches api.github.com. It does not cover a different domain the same site happens to use, so objects.githubusercontent.com has to be listed too. IPv4 addresses and CIDR blocks are used as written; IPv6 entries are refused because restricted cells enforce IPv4 egress and drop non-loopback IPv6 traffic.

Two pieces enforce the list, both in the machine and outside the container. A resolver answers for the listed names only — anything else gets NXDOMAIN, so a query cannot carry data out to a resolver of an agent's choosing — and it records the addresses it hands out in a set the firewall allows. A name resolves and is reachable, or does neither, and a site that changes its addresses keeps working without anyone editing a rule. The container is rootless: nothing inside it can load a firewall rule, stop the resolver, or edit either one's configuration.

Include dependencies

A restricted cell cannot pull its own image unless the registry is allowed. A build can also need package registries, source control, API endpoints, or a CDN. Missing destinations fail as DNS errors instead of silently expanding the policy:

dial tcp: lookup production.cloudfront.docker.com on 127.0.0.1:53: no such host

The name in the error is the entry to add.

Configure DNS

By default the cell's resolver forwards to 1.1.1.1 and 8.8.8.8. Public resolvers avoid granting DNS access to a host service. A resolver on the host's network would see every name the cell looks up.

That is the wrong answer on a network whose names only its own resolver knows — a corporate one, a split horizon, anything behind a proxy that intercepts DNS — and on one that refuses to carry DNS to a public resolver at all. Name the resolvers instead:

network:
  resolvers:
    - host # the resolver this machine is given, which is the host's
    - 10.0.0.53 # or name them outright

host is discovered at boot from the machine's DHCP lease, so it follows the network you are on. This grants the VM resolver access to that host service on port 53, independently of the destination allow list. The container asks the VM resolver, which answers for allowed names; direct connections to a DNS server require an address grant like any other connection.

Do not combine host with network.vpn; see why.

Debug a failure

The dashboard's traffic view shows lookups, resolved addresses, and refused connections. The refused name usually identifies the entry to add. Changing the list takes effect after restarting the machine:

solitary down my-cell
solitary up my-cell

Remove the policy

Deleting network.allow makes the cell unrestricted again from the next boot: the resolver, the firewall and the frozen resolv.conf are removed from the machine as part of starting it. The same goes for network.vpn — the interface is disabled and the configuration deleted from the machine, private key included. Until the machine restarts, a running cell keeps enforcing what it came up with, and up says so.

A domain grant authorizes the IPv4 addresses returned for that domain and its subdomains. Other sites sharing those addresses can also be reached, regardless of their hostname or TLS SNI. DNS-controlled addresses can include a host or private-network address. This is address filtering, so avoid broad grants and do not treat it as hostname-level isolation. Learned addresses expire after one hour and the set is capped at 65,536 addresses; established connections can continue. A client holding an old address may need to resolve the name again.

On this page