Skip to content

Hosts and Ports

An allow_http block is one grant in two levels. The label says which destination may be reached; the body says which requests may be sent there.

allow_http "api.example.com" {
allow_requests ["GET /v1/**"]
}

Keeping them together is what makes a diff readable: delete the host and its rules go with it.

The label may be a list, and then every host in it shares the one body:

allow_http ["a.example.com", "b.example.com:9123"] {
allow_requests ["GET /health", "* /v1/**"]
}

This is exactly the same as writing a separate block for each host with the same rules — it resolves to one grant per host — but the rules live in one place, so they cannot drift out of step between the hosts. Reach for it when several destinations are governed by one rule set; keep the single-string form for a host with rules of its own. The list is an option, never a requirement: a one-host grant stays allow_http "api.example.com" { … }, no brackets.

WrittenMatches
"api.example.com"that host, ports 80 and 443 only
"api.example.com:8443"that host on 8443 only
"api.example.com:*"that host on every port
"*.example.com"one subdomain label — api.example.com, not a.b.example.com and not the apex
"**.example.com"any subdomain depth — api.example.com and a.b.example.com, still not the apex
"registry-*.npmjs.org"a generated-name family, in one label
"10.0.0.0/8:*"a network, matched against literal addresses only
"[2001:db8::1]:443"an IPv6 address — bracketed, so the port is unambiguous
"*"any host, still on 80 and 443

A bare host never means every port. Granting https should not silently grant ssh on the same machine.

A label can carry wildcards — the same * / ** the filesystem globs use:

  • * matches any run of characters within one label and never crosses a dot. registry-*.npmjs.org matches registry-1.npmjs.org but not registry.cdn.npmjs.org. A whole-label * fixes the depth: console.*.aws.com is console.<one label>.aws.com, and *.*.aws.com is exactly two subdomain labels.
  • ** matches one or more whole labels, so any-depth reach is written **.example.com — every subdomain, however deep, but never the apex.

The rightmost label must be literal. A wildcard there would open a whole top-level domain, so foo.* and *.* are refused, and “any host at all” keeps its one spelling, the bare *. (*.com is accepted — without a public-suffix list the resolver cannot tell a public suffix from a private one like .internal — but it is rarely what you mean.)

This grammar lives in the destination, not the verb, so every net rule reads it the same: allow_http, allow_tcp, allow_udp, and the deny_* / mute_hosts forms alike.

Two labels for the same host on different ports are two separate grants, and neither rules on the other’s requests:

allow_http "api.example.com:443" { allow_requests ["GET /safe"] }
allow_http "api.example.com:8443" { allow_requests ["* /**"] }

A request to :443 is decided by the first block only.

allow_http "api.example.com" { } # ← error

Not “allow everything”. To grant a host completely, say so:

allow_http "api.example.com" { allow_requests ["* /**"] }
deny_hosts ["ads.example.com", "*.ads.example:*"]

Beats every allow, from any fragment, and covers every protocol. There is no deny_host block form — a deny needs no body.

mute_hosts ["telemetry.example.com"]

Keeps an expected-blocked host’s denials out of the logs without deciding anything — the host stays blocked by the absence of a grant, and unlike deny_hosts a mute is overridable by a more local fragment. See Muting a host for when to use it over deny_hosts.

allow_http "*" { allow_requests ["* /**"] }

Still routed through the proxy and still logged. Note it is every host on 80 and 443 — every port anywhere is allow_tcp ["*:*"], in the verb whose grants are documented as weaker.

An entry written as a name matches a name; an entry written as an address matches an address. They never cross, because a name’s resolution is checked separately: after DNS, the proxy vets where the name actually landed, so a name resolving into a denied range is refused. That closes DNS rebinding without you writing anything. A wildcard name is still a name: 127.*.0.1 never matches the literal address 127.0.0.1, however its dots line up — name a network with a CIDR (127.0.0.0/8) instead.

Reaching a service on the host: host.internal

Section titled “Reaching a service on the host: host.internal”

host.internal is a reserved name for a service the developer is running on the host machine’s own loopback — a local Postgres, a dev gateway. Grant it like any other target:

net {
allow_tcp ["host.internal:5432"] # raw splice to the host's Postgres
allow_http "host.internal" { # or inspected, with per-request rules
allow_requests ["* /**"]
}
}

It means the same thing on macOS and Linux, so one policy — and one client connection string — works on both. The sandbox cannot dial the host’s 127.0.0.1 itself (on Linux it has its own isolated loopback; on macOS it shares the host’s), so the proxy dials 127.0.0.1 on the sandbox’s behalf, after the grant is checked. A deny_hosts ["127.0.0.1:*"] still wins.

Use host.internal for anything on the host, and keep 127.0.0.1 for the sandbox’s own processes — the two differ by platform, so see macOS and Linux. .internal is reserved for private use, so the name cannot collide with a real one.