PLATINUM DOCS

Networking & ports

Expose sandbox ports with signed preview URLs, revoke access, and filter outbound traffic per sandbox.

Networking controls traffic in and out of a sandbox. Expose ports through signed preview URLs. Filter outbound traffic per sandbox by address or hostname, and add headers to outbound requests.

Expose ports at create

expose accepts up to 8 ports. Ports open when the sandbox reaches running.

const sbx = await client.sandboxes.create(
  { expose: [{ port: 8000 }, { port: 3000, public: true }] },
  { waitForRunning: true },
);
sbx.exposedUrl(8000);
sbx = client.sandboxes.create(
    expose=[{"port": 8000}, {"port": 3000, "public": True}],
    wait_for_running=True,
)
sbx.exposed_url(8000)
# no create flag in the CLI — expose after create: pt sandbox expose <id> <port>
curl -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"template": "pt-base", "expose": [{"port": 8000}, {"port": 3000, "public": true}]}'

Expose a port after create

The sandbox must be running.

const { url, token } = await sbx.expose(8000, { ttlSeconds: 3600 });
res = sbx.expose(8000, ttl_seconds=3600)
print(res["url"])
pt sandbox expose $ID 8000
# open the preview URL in your browser:
pt sandbox open $ID 8000
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/expose" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"port": 8000, "ttl_seconds": 3600}'

The response carries the preview url. For private exposures, it also carries the token.

RuleBehavior
Default token TTL1 day
public: trueDrops the token; anyone with the URL can reach the port
Re-expose a portMints a fresh token with a new expiry

Use preview URLs

https://<port>-<sbxid-short>.sbx.platinum.dev/<path>?t=<hmac>
  • <sbxid-short> is the lowercase ULID tail of sbx_<ULID>. Drop the sbx_ prefix.
  • Private exposures need the token, as ?t= or the x-pt-preview-token header. Public exposures need none.
  • The proxy passes WebSocket upgrades end-to-end.
  • A request to a stopped or archived sandbox auto-resumes it, then forwards the request.

Revoke one token

Revoke a leaked token without removing the exposure.

await sbx.revokeExposeToken(token, "leaked in a screenshot");
sbx.revoke_expose_token(token, reason="leaked in a screenshot")
pt sandbox revoke $ID "<the t= value>" --reason "leaked in a screenshot"
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/expose/revoke" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"token": "<the t= value>", "reason": "leaked"}'

Unexpose a port

Warning: unexpose kills every token ever minted for the port. Tokens stay dead after a later re-expose.

await sbx.unexpose(8000);
sbx.unexpose(8000)
pt sandbox unexpose $ID 8000
curl -X DELETE "$PT_API_URL/v1/sandboxes/$ID/expose/8000" \
  -H "Authorization: Bearer $PT_TOKEN"

Block internet access

Set allowInternetAccess to false at create. The policy applies to the sandbox interface before the sandbox boots. A blocked sandbox never has a route out.

const sbx = await client.sandboxes.create(
  { allowInternetAccess: false },
  { waitForRunning: true },
);
sbx = client.sandboxes.create(allow_internet_access=False, wait_for_running=True)
pt sandbox network $ID --no-internet
curl -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"template": "pt-base", "allow_internet_access": false}'

allowInternetAccess: false is the same policy as denyOut: ["0.0.0.0/0"].

Allow only some destinations

allowOut accepts IP addresses, CIDR blocks, hostnames and *. wildcards. denyOut accepts addresses only. Allow rules always win over deny rules.

Warning: a hostname allow-list needs denyOut to block everything else. The API rejects the combination without it.

const sbx = await client.sandboxes.create({
  network: {
    allowOut: ["api.example.com", "*.github.com", "8.8.8.8"],
    denyOut: ({ allTraffic }) => [allTraffic],
  },
});
sbx = client.sandboxes.create(network={
    "allow_out": ["api.example.com", "*.github.com", "8.8.8.8"],
    "deny_out": lambda ctx: [ctx.all_traffic],
})
pt sandbox network $ID --allow-out api.example.com,*.github.com,8.8.8.8 --deny-out all
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"allow_out": ["api.example.com", "*.github.com", "8.8.8.8"], "deny_out": ["0.0.0.0/0"]}'

*.github.com matches any subdomain of github.com. It does not match github.com itself.

Platinum reads the hostname from the TLS server name on port 443 and the Host header on port 80. Traffic on other ports is filtered by address only. A hostname allow-list also permits DNS to 1.1.1.1 and 8.8.8.8 on port 53.

Platinum resolves an allowed hostname itself and connects to that result. A sandbox cannot reach a different address by claiming an allowed name.

Update the policy on a running sandbox

updateNetwork replaces the whole policy. It does not merge. An empty object clears every rule.

await sbx.updateNetwork({ denyOut: ["8.8.8.8"] });
await sbx.updateNetwork({});
sbx.update_network({"deny_out": ["8.8.8.8"]})
sbx.update_network({})
pt sandbox network $ID --deny-out 8.8.8.8
pt sandbox network $ID --show
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"deny_out": ["8.8.8.8"]}'

The policy survives stop and start. Read it back with GET /v1/sandboxes/$ID/network.

Add headers to outbound requests

A rule injects headers into requests to one host. Register rules under network.rules.

Warning: a rule grants no access. List the host in allowOut as well when you also block other traffic.

await sbx.updateNetwork({
  rules: { "api.example.com": [{ transform: { headers: { "X-Tenant": "acme" } } }] },
});
sbx.update_network({
    "rules": {"api.example.com": [{"transform": {"headers": {"X-Tenant": "acme"}}}]},
})
pt sandbox network $ID --header 'api.example.com:X-Tenant:acme'
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"rules": {"api.example.com": [{"transform": {"headers": {"X-Tenant": "acme"}}}]}}'

Platinum terminates TLS for a host that has a rule. Header injection into HTTPS is impossible otherwise. Platinum generates a certificate authority for that sandbox alone, trusts it inside the sandbox, and verifies the origin certificate on the outbound connection. Every host without a rule passes through untouched.

Headers that control message framing are rejected. That list covers Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Keep-Alive, TE, Trailer, Expect and the Proxy- headers.

Understand a blocked connection

A blocked TCP connection can still look open inside the sandbox. Platinum accepts the connection before it reads the destination hostname.

Check for an application-level response instead. A blocked HTTP request returns 403 with the header X-Platinum-Egress: blocked. A blocked HTTPS connection closes without a TLS handshake.

Set an address policy (original API)

setEgressPolicy filters by CIDR only. It remains supported and maps onto the same enforcement.

Caveat: the egress policy is not a hard security boundary. A malicious process in the sandbox can spoof its source IP to evade the rules.

169.254.0.0/16 (and its IPv6 equivalents) is handled host-wide on the forward path, before any per-sandbox policy is evaluated. Unlike the per-sandbox rules, that guard matches on destination only, so it cannot be evaded by spoofing the source address. Keeping 169.254.169.254/32 in your own policy is harmless and still recommended as defense in depth. Operators can extend the host-wide list with PT_EGRESS_BLOCK_CIDRS in /etc/platinum/host.env (comma-separated CIDRs): worth adding 168.63.129.16/32 on Azure and 100.100.100.200/32 on Alibaba, since those are ordinary routable addresses the link-local range does not cover.

await sbx.setEgressPolicy({ mode: "allow", cidrs: ["10.0.0.0/8", "169.254.169.254/32"] });
sbx.set_egress_policy("allow", ["10.0.0.0/8", "169.254.169.254/32"])
pt sandbox egress $ID --mode allow --cidrs 10.0.0.0/8,169.254.169.254/32
# clear it (allow all outbound again):
pt sandbox egress-clear $ID
curl -X POST "$PT_API_URL/v1/sandboxes/$ID/egress-policy" \
  -H "Authorization: Bearer $PT_TOKEN" \
  -d '{"mode": "allow", "cidrs": ["10.0.0.0/8", "169.254.169.254/32"]}'

The mode names the list's role, not the default verdict.

modeListed CIDRsEverything else
allowdroppedaccepted
denyaccepteddropped

Block all egress with {"mode":"deny","cidrs":[]} or {"mode":"allow","cidrs":["0.0.0.0/0"]}.

Know what is reachable

From inside a sandbox:

DestinationReachable?
InternetYes, subject to the network policy
Own ports, via 127.0.0.1Yes

Use the dashboard

The sandbox detail view has a Ports tab. Expose, unexpose, and copy preview URLs there.\n\nThe same view has a network panel. Edit the reachable and blocked lists, add request headers, and read one sentence describing what the policy does. Changes apply immediately without a restart.

See also

  • Templates — bake services into the rootfs that start on boot
  • Snapshots — exposed ports survive stop and resume
  • API reference — full request and response schemas