# Networking and access You reach a sandbox in two ways: over SSH, which goes through the workspace API, and through HTTP ports, which the workspace serves on public URLs. In the other direction, the sandbox's network mode and egress rules decide what the sandbox itself can reach: the internet, your Zerops project, or nothing. ## SSH `zbox ssh` opens an SSH session to a sandbox. The connection is carried through the workspace API, so you need no SSH key, no VPN and no SSH server in the image: the CLI's access to the workspace ([API key or Zerops login](/zbox/access)) is the proof. It works for every base and in every network mode, airgapped included, and wakes a parked sandbox first. ```bash zbox ssh app # run one command instead of a shell zbox ssh app -- uname -a # forward local port 3000 to port 3000 in the sandbox zbox ssh app -- -L 3000:localhost:3000 ``` Sessions run as `root`. Local port forwards and agent forwarding work, remote forwards (`-R`) are refused. To use plain `ssh`, `scp`, `sftp`, `sshfs` or a remote SSH editor (VS Code Remote - SSH, JetBrains Gateway), install the SSH config block once: ```bash zbox ssh-config --install ssh zbox- scp ./data.csv zbox-:/workspace/ ``` `--install` writes a `Host zbox-*` block next to the zbox config file and adds an `Include` line for it at the top of `~/.ssh/config`. Without `--install`, `zbox ssh-config` only prints the block. The block always follows the current workspace, and the host name is `zbox-` plus the sandbox ID, not its name. `rsync` works too when the image has `rsync` installed (the default base doesn't). Every sandbox has its own host key, generated by the workspace when the sandbox is created, forked or restored. The CLI fetches it from the API and pins it in its own `known_hosts` file, so host key checking stays strict. :::caution An SSH connection ends when the sandbox is parked and when a snapshot with memory is taken. It can't be resumed, reconnect instead. ::: ## Ports A sandbox exposes nothing until you declare a port. A port is written as `port[/access]`, for example `3000`, `3000/public` or `5432/project`, where access is one of the [access modes](#access-modes). Add `/wake` or `/no-wake` to override [wake on request](#wake-on-request). Ports are HTTP: the workspace proxies HTTP requests to them. ```bash # declare ports at create zbox new app -p 3000 -p 8080/public # add or change ports on an existing sandbox, nothing restarts zbox port open app 5432/project 9000/token zbox port close app 9000 zbox port list app ``` Port changes apply to a running or parked sandbox on the next request. In the console, a sandbox's ports are edited in its Policy tab. A sandbox created from a [template](/zbox/images#templates) starts with the template's default ports unless you declare your own. The application has to listen on all interfaces (`0.0.0.0`), not only on `localhost`: requests arrive on the sandbox's network interface. If nothing answers on the port, the request fails with `UNREACHABLE`. The workspace forwards the request with the original `Host` header and `X-Forwarded-*` headers. It removes the `Authorization` header, its own session cookie and the `ticket` and `token` query parameters before the request reaches the sandbox, on every access mode. :::note Coming soon Raw TCP ports, and per-port ingress limits: maximum connections, held requests, request rate and body size. ::: ### Port hostnames Each port gets one URL per workspace domain, built from the port number and the sandbox ID, for example `https://3000-l8mvceq5-zbox-6da-8081.prg1.zerops.app` on the Zerops subdomain or `https://3000-l8mvceq5.box.example.com` on your own domain. See [domains](/zbox/workspace-settings#domains) for the two forms and how to add a domain. The URLs are listed by `zbox port list` and in the sandbox's `hosts` in the API, in the order of the workspace domains. A workspace without a domain gives ports no public URL. All of them are served by the workspace's port 8081 ([subdomain access](/zbox/workspace-service#subdomain-access)). ## Access modes The access mode of a port decides who gets through: | Mode | Who can reach the port | How | |---|---|---| | `members` (default) | Whoever can use the workspace API, with the API key or a Zerops login | A one-time ticket from `zbox open` or the console's port links, exchanged for a session cookie | | `ticket` | Anyone you give a ticket | A ticket from `zbox credential issue`, exchanged for a session cookie | | `token` | Clients holding a token | `Authorization: Bearer `, or `?token=` in a link | | `public` | Anyone | No credential | | `project` | Services in your Zerops project | Requests from the project network only ([how](#reach-a-sandbox-from-the-project)) | In the API and in policy JSON, the modes are written in uppercase, for example `MEMBERS`. The workspace API key itself never opens a port, in any form. A request without a valid credential gets `UNAUTHORIZED`, a `project` port reached through the public URL gets `PROJECT_ONLY`. To open a port in the browser, signed in: ```bash zbox open app 3000 # under another workspace domain than the first one zbox open app 3000 --domain box.example.com ``` `zbox open` issues a credential valid for 60 seconds for this one opening and opens the port URL with it: a ticket for a `members` or `ticket` port, a token for a `token` port. A `public` port opens bare. The workspace exchanges the credential for a session cookie and redirects to the clean URL, so the credential never stays in the address bar or reaches your application. The port links at the top of a sandbox's page in the console work the same way. ### Ingress credentials **Ingress credentials** are tickets and tokens for one port of one sandbox. A **ticket** is used once and works for `members` and `ticket` ports. A **token** can be used repeatedly and works for `token` ports. The secret is shown only when the credential is issued. ```bash # a ticket for port 3000, valid 15 minutes unless --ttl says otherwise zbox credential issue app --port 3000 --name review # a token for a token port, valid 30 days unless --ttl says otherwise zbox credential issue app --port 9000 --kind token --ttl 24h zbox credential list app zbox credential revoke ``` `issue` also prints an Open URL with the secret as a query parameter. Opening it signs the browser in with a session cookie. A client can send a token as a bearer header instead, on every request. Revoking a credential ends its sessions on their next request. Removing a sandbox revokes all of its credentials. The console lists and issues them in the sandbox's Access tab. :::caution A session lasts only as long as the credential it was opened with, and 12 hours at most. A session started by `zbox open` or a console port link ends after about a minute, when its 60-second credential expires, and the next request gets `UNAUTHORIZED`. Open the port again, or open it with a longer-lived ticket from `zbox credential issue --ttl`. ::: ## Wake on request A request to a port of a parked sandbox can wake it. The request is held while the sandbox wakes and is then passed on. Wake on request is on by default for every access mode except `public`, and `/wake` or `/no-wake` on the port spec overrides it. A request that fails the access check never wakes anything. A `public` port without wake on request answers `PARKED`. A request is held for 30 seconds at most, also while the sandbox is being created or is in another transition. Set `ingress.holdTimeoutSeconds` in the sandbox's policy to change it, the same way as the [egress rules](#egress-rules). A sandbox with [stale secrets](/zbox/secrets#stale-secrets) isn't woken by a request, it answers `SECRETS_STALE` until you wake it explicitly. ### Error responses When the workspace can't pass a request on, it answers with a JSON body like the API's, for example `{"error":{"code":"PARKED","message":"..."}}`: | Status | Code | Meaning | |---|---|---| | 404 | `NO_ROUTE` | No sandbox with this ID, or the port isn't declared | | 401 | `UNAUTHORIZED` | The port needs a valid ticket, token or session | | 403 | `PROJECT_ONLY` | A `project` port was reached from outside the project network | | 410 | `GONE` | The sandbox was removed | | 503 | `PARKED` | The sandbox is parked and the port doesn't wake it | | 503 | `SECRETS_STALE` | The sandbox has stale secrets and needs an explicit wake | | 504 | `HOLD_TIMEOUT` | The sandbox didn't come up within the hold timeout, `Retry-After` is set | | 502 | `FAILED` | The sandbox is in the failed status | | 502 | `UNREACHABLE`, `UPSTREAM` | Nothing listens on the port, or the application closed the connection | On the Zerops subdomain, 502 answers show the Zerops error page instead of the JSON body. ## Egress What a sandbox can reach is set by its **network mode**, fixed when the sandbox is created: | Mode | Reaches | |---|---| | `project_internet` (default) | The internet and your Zerops project's private network | | `internet` | The internet, and the project's DNS resolver but nothing else of the project | | `project` | The project's private network only | | `airgapped` | Nothing: the sandbox has no network interface | ```bash zbox new worker --network project ``` In the API and in policy JSON, the modes are written in uppercase, for example `PROJECT_INTERNET`. The workspace policy can limit which modes are allowed ([workspace policy](/zbox/workspace-settings#workspace-policy)). In every mode, a sandbox can't reach other sandboxes or its own workspace directly. It can reach another sandbox's port through the port's public URL when its mode includes the internet. Every mode except `airgapped` reaches the [credentials proxy](/zbox/secrets#credentials-proxy). `zbox ssh`, `zbox exec` and the other commands work in every mode, airgapped included: they don't use the sandbox's network. Ports of an airgapped sandbox never answer. ### Egress rules Inside its mode, a sandbox can be restricted further by the `egress` part of its policy: ```json { "egress": { "allow": [ { "cidr": "project", "ports": [5432] }, { "cidr": "203.0.113.0/24", "ports": [443] } ], "deny": [], "dns": "PROJECT" } } ``` - `deny` rules are checked first and drop matching traffic. - `allow` rules let matching traffic through, still within the network mode. With an `egress` block present, everything not allowed is dropped, so an empty `allow` list leaves only DNS and the credentials proxy. - A rule's `cidr` is an IPv4 address or prefix, or `project` for the project's private network. `ports` lists destination ports (TCP and UDP), an empty list means every port. Domain names and IPv6 prefixes are refused. - `dns` is `PROJECT` (the sandbox uses the project's DNS resolver) or `NONE` (no DNS at all). Without an `egress` block, the network mode alone applies. A sandbox created from a template gets the template's default egress unless its own policy sets one. Set the rules at create with `zbox new -f policy.json`, or change them on a running or parked sandbox, which applies at once: ```bash zbox policy app > policy.json # edit the "egress" block, then: zbox policy app -f policy.json ``` :::note Coming soon Egress filtering by domain name, and bandwidth limits. ::: ## Project services A sandbox whose network mode includes the project reaches the other services of your Zerops project by hostname with the `.zerops` suffix, for example `http://api.zerops:3000` or `db.zerops:5432`. The bare hostname doesn't resolve inside a sandbox. ### Reach a sandbox from the project Services in the project reach a sandbox's `project` port through the workspace's port 8081 on the project network, with the port's host in the `Host` header: ```bash # from a service in the same project, workspace hostname "zbox" curl -H "Host: 3000-l8mvceq5-zbox-6da-8081.prg1.zerops.app" http://zbox.zerops:8081/ ``` Requests that come through the public URL are refused with `PROJECT_ONLY`.