Limit where a Space connects (egress policies)

Allow and deny lists of addresses for a Space's outbound connections, enforced by the host outside the guest, from the first instruction.

An egress policy says which destinations a Space may open connections to. The machine the Space runs on enforces it outside the guest, so nothing in the guest, root included, can change it: there are no rules inside the guest to flush.

import cua
 
spaces = cua.embedded().spaces()
created = await spaces.create(cua.SpaceCreateOptions(
    on="local",
    egress=cua.EgressPolicy(allow=["1.1.1.1"], deny=["all"]),
))
space = created.space
assert space.egress_enforced == cua.EgressEnforcement.HOST
 
# Replace it while the Space runs (an empty policy removes it).
await spaces.set_egress(space.id, cua.EgressPolicy(deny=["10.0.0.0/8"]))

The policy#

  • Every deny entry blocks that destination; every allow entry is an exception that wins over deny. deny=["all"] with allow=["1.1.1.1"] leaves only 1.1.1.1 reachable.
  • An allow list alone blocks nothing. Both lists empty: no policy.
  • Entries are IPv4 or IPv6 addresses and CIDRs; all is 0.0.0.0/0 and ::/0. A hostname is refused (InvalidArgument): rules are by address.
  • The destination is the address the guest sends to. DNS works while the resolver the guest uses is allowed: on a VM that is its virtual network's own resolver (10.0.2.3 under QEMU, 172.16.0.1 in a Firecracker microVM); a container uses its engine's resolvers.
  • Never filtered: loopback inside the guest, replies to connections made to the guest (cua-spacesd, published ports), IPv6 neighbor discovery.

Where it is enforced#

SpaceEnforced by
Container, runc or gVisor (any Docker engine, also the built-in runtime on a Mac)nftables in the engine's own network namespace, on the Space's bridge port
Firecracker microVM (Linux)iptables in the network namespace pasta creates around the VM
QEMU VM (Linux)iptables in the network namespace pasta creates around QEMU
A Space one of your machines provides (on="host:...", on="auto")that machine, as above
macOS VM, Apple Containerization microVM, QEMU on macOS or Windowsrefused (Unsupported): filtering them needs root (pf, WFP)
Cua Cloud, your own cloud account (AWS, Google Cloud, Modal), a Space added by addressrefused (Unsupported): no host-side point cua controls

SpaceInfo.egress_enforced is host for a Space with a policy. A machine whose cua predates egress policies never gets a Space with one: the create fails and the Space it made is deleted.

Lifetime#

  • The policy is in place before the guest's first instruction: a container's rules exist before it starts (keyed by the MAC address it is created with), a VM's namespace holds them before the VM boots.
  • It survives stop and start, suspend and resume, a restart of the daemon, forks and checkpoints (a fork inherits its source's policy).
  • set_egress applies at once. A running QEMU VM started without a policy first moves into its own namespace: its memory is carried over to a new QEMU process in a moment, and connections through the old one end.

What changes for a Space with a policy#

  • A container created with a policy has no raw sockets (CAP_NET_RAW); ping still works through ICMP datagram sockets. Other containers on the same engine reach it only through the host (published ports), not directly over the bridge.
  • A QEMU VM with a policy reaches nothing on the host's loopback through 10.0.2.2.

Requirements#

  • Containers: nft (nftables) where the container engine runs.
  • VMs on Linux: pasta (the passt package) and unprivileged user namespaces. Ubuntu 24.04 restricts those to programs with an AppArmor profile; allow pasta with one that grants userns, or set kernel.apparmor_restrict_unprivileged_userns=0.

To check a policy from inside a Space as root: nft flush ruleset; iptables -F changes nothing, and a raw packet to a denied address gets no answer.