Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference FAQ
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /Network Policy

Network Policy

Outbound traffic is gated by a list of rules naming protocol, destination, and port. The default is deny: only traffic that matches your policy is allowed.

Updated

The defaults

With no network rules and no HTTP ACL flags:

  • TCP connections are denied.
  • UDP, ICMP, and raw socket creation are denied.
  • No HTTP proxying or credential injection is active unless you configure HTTP ACL flags.

Unrestricted TCP and UDP egress is opt-in and explicit: --net-allow '*'. ICMP is never implied by it and needs its own --net-allow 'icmp://*'.

--net-allow and --net-deny

--net-allow and --net-deny both take repeatable network rule strings. They use the same basic syntax, but choose opposite defaults.

Flag Default Rule effect Targets
--net-allowDeny outbound trafficAllow destinations that match any ruleHostnames, IP literals, CIDRs, *, or an empty target such as :443
--net-denyAllow outbound trafficDeny destinations that match any ruleIP literals, CIDRs, *, or an empty target such as :443; hostnames are rejected

The two flags are mutually exclusive. Use --net-allow when you can name the destinations a workload needs. Use --net-deny when the workload needs general network access but specific IP ranges must be blocked.

Rule syntax

A network rule has three parts: an optional protocol scheme, a target, and an optional TCP/UDP port list.

Syntax
[scheme://]target[:port[,port,...]]
[scheme://]:port[,port,...]
Part Meaning Examples
SchemeOptional protocol selector. No scheme means TCP and UDP.tcp://, udp://, icmp://
TargetDestination host, IP, CIDR, or wildcard. An empty target means any IP.api.example.com, 10.0.0.4, 10.0.0.0/8, *, empty in :443
PortsOptional TCP/UDP port or comma-separated port list. Omitted means all ports.:443, :80,443, :*

These are the most common rule shapes:

Rule Matches
api.example.com:443TCP and UDP traffic to api.example.com on port 443.
tcp://api.example.com:443TCP traffic to api.example.com on port 443.
udp://1.1.1.1:53UDP traffic to 1.1.1.1 on port 53.
:443TCP and UDP traffic to any IP on port 443.
10.0.0.0/8:443TCP and UDP traffic to any address in that CIDR on port 443.
[2606:4700::/32]:443TCP and UDP traffic to an IPv6 CIDR on port 443. Brackets are required when an IPv6 target has a port.
github.comTCP and UDP traffic to github.com on any port.
*TCP and UDP traffic to any IP on any port.
icmp://github.comICMP echo traffic to github.com. ICMP rules do not take ports.

A few details are worth stating plainly.

A scheme-less rule covers both TCP and UDP. It expands into one rule per protocol at parse time. Naming a scheme pins the rule to that one protocol. ICMP is never implied and always needs icmp://.

A comma groups ports within one rule. host:80,443 is a single rule covering two ports. To express several independent rules, repeat the flag.

IP and CIDR targets never touch DNS. They are matched by containment, with an IP literal being a /32 or /128. Only hostnames resolve.

--net-allow rules are OR'd. A destination is permitted if some rule matches the socket's protocol, destination IP, and port. Port is not applicable for ICMP.

--net-deny uses the same matching rules, but blocks matches. Denylist targets must be literal IPs, CIDRs, *, or an empty target such as :443. Hostnames are rejected because a name-based denial can be bypassed by resolving the name yourself. For domain-level denial, use --http-deny.

Allowlist examples

Outbound allowlist
# One host, one port
$ sandlock run --net-allow api.openai.com:443 \
    -r /usr -r /lib -r /etc -- python3 agent.py

# Two ports for one host, plus a separate any-IP port
$ sandlock run --net-allow github.com:22,443 --net-allow :8080 \
    -r /usr -r /lib -r /etc -- python3 agent.py

# A bare host (or host:*) permits every port
$ sandlock run --net-allow github.com \
    -r /usr -r /lib -r /etc -- ssh user@github.com

# IP, CIDR, or bracketed IPv6 literal; matched by containment
$ sandlock run --net-allow 10.0.0.0/8:443 \
    --net-allow '[2606:4700::/32]:443' \
    -r /usr -r /lib -r /etc -- python3 agent.py

# Pin protocols: UDP DNS to one resolver, TCP HTTPS anywhere
$ sandlock run --net-allow udp://1.1.1.1:53 --net-allow tcp://:443 \
    -r /usr -r /lib -r /etc -- ./client

# Ping: uses the host's net.ipv4.ping_group_range setting
$ sandlock run --net-allow icmp://github.com \
    -r /usr -r /lib -r /etc -- ping github.com

# Fully open TCP and UDP; ICMP still needs its own rule
$ sandlock run --net-allow '*' \
    -r /usr -r /lib -r /etc -- ./client

Denylist

These denylist examples use the same rule syntax, but each matching destination is blocked instead of allowed.

Denylist forms
--net-deny 10.0.0.0/8               # all ports on a CIDR (TCP and UDP)
--net-deny 169.254.169.254:80       # one IP, one port (TCP and UDP)
--net-deny 169.254.169.254:80,443   # comma-separated ports in one rule
--net-deny '*'                      # any IP, all ports (TCP and UDP)
--net-deny 'udp://192.168.0.0/16'   # UDP only, to a CIDR
--net-deny 'tcp://10.0.0.1:22'      # TCP only, one IP and port

Denylists are a weaker posture. An allowlist fails closed when you forget something; a denylist fails open. The canonical use for --net-deny is blocking the cloud metadata endpoint and RFC 1918 space for a workload that otherwise needs general internet access. Prefer --net-allow whenever you can enumerate the destinations.

Protocol gating

Which protocols the workload can use at all falls out of which schemes appear in the rules. This is not a separate setting.

Condition Effect
No UDP ruleThe workload cannot create UDP sockets. A scheme-less rule counts as a UDP rule.
No ICMP ruleThe workload cannot create ping sockets (SOCK_DGRAM + IPPROTO_ICMP).
Raw ICMPNever exposed, under any policy. Packet crafting is out of scope.
TCPAvailable for rules that allow it; destinations are still checked against the configured policy.

Workloads that need ping should rely on the host's net.ipv4.ping_group_range and use --net-allow icmp://....

Name resolution

Only hostname targets touch DNS. They are resolved once, at sandbox start, and pinned in a synthetic /etc/hosts across every protocol. The workload therefore cannot make the sandbox reach a different address by manipulating resolution later.

The synthetic file replaces the real one only when at least one rule names a concrete hostname. A policy made entirely of IPs, CIDRs, :port rules, udp://*, or icmp://* leaves the real /etc/hosts and the host's DNS configuration visible.

Wildcards

Hostnames are matched literally. --net-allow *.example.com:443 is not supported. List each domain you need, or use a CIDR or IP target to cover an address range.

The * token is allowed in two places: as the target, where it is an alias for empty (*:port is the same as :port), and as the port for TCP and UDP rules (host:*, :*, *:*).

Because the port is optional, host and host:* are the same rule, and *, :*, and *:* are all the same rule. Mixing the wildcard with concrete ports, as in host:80,*, is rejected.

Binding ports

Server-side bind() is governed separately from outbound traffic. --net-allow-bind is a default-deny allowlist and has no relationship to --net-allow.

Each value is a comma-separated list of single ports or inclusive lo-hi ranges, and the flag repeats. The '*' wildcard allows binding any port, including an ephemeral bind(0); it cannot be mixed with port lists, though repeating the bare wildcard is fine.

Bind rules
$ sandlock run --net-allow-bind 8080,9000-9005 \
    -r /usr -r /lib -r /etc -- python3 server.py

The bind allowlist covers TCP only; UDP bind() is not separately gated. The wildcard allows any TCP bind, including bind(0).

--net-deny-bind is the inverse: binding is default-allow and the listed TCP ports are denied. Same port syntax, mutually exclusive with --net-allow-bind.

Port virtualization

With --port-remap, each sandbox gets a full virtual port space, so several sandboxes can bind the same port without colliding. When a port is already taken, Sandlock allocates a different real port transparently, and /proc/net/tcp inside the sandbox shows only that sandbox's own ports.

Two sandboxes, same port
$ sandlock run --name api.local --port-remap --net-allow-bind 6379 \
    -r /usr -r /lib -r /etc -- redis-server --port 6379 &

$ sandlock run --name web.local --port-remap --net-allow-bind 8080 \
    -r /usr -r /lib -r /etc -- python3 server.py &

$ sandlock ps
NAME          PID      UPTIME  CMD
api.local     12345        5m  redis-server --port 6379
web.local     12346        3m  python3 server.py

Naming a sandbox with --name gives it a stable virtual hostname, which is what lets an external reverse proxy such as nginx or envoy route by name to the correct real port. From Python, sb.ports() returns the {virtual_port: real_port} mapping while the sandbox is running.

UNIX sockets

Abstract AF_UNIX sockets are governed by Landlock's LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (ABI v6), which is independent of --net-allow entirely. The scope prevents a sandbox from connecting to abstract sockets belonging to processes outside it. Filesystem-backed UNIX sockets are governed by the filesystem rules, like any other path.

Policy fields

Python TOML Default Description
net_allowallow()Outbound endpoint allowlist. Empty denies all outbound.
net_allow_bindallow_bind()TCP ports the sandbox may bind. Default-deny allowlist.
net_deny_binddeny_bind()TCP ports the sandbox may not bind. Default-allow denylist.
port_remapport_remapFalseTransparent TCP port virtualization.

See HTTP ACL and Credentials for rules above the transport layer, and the reference for the complete field list.