Private network
Let every Cloud Run reach internal services through Tailscale or Headscale.
A company can connect one private network. Every Cloud Run joins it before its harness starts, so agents reach internal services (databases, staging APIs, package registries, admin tools) by the names people use. Owners and admins set it up once in Settings → Network, or an integration does with an admin workspace API key; nothing is configured per Task. Every member can see which network is connected, whether it is on, and when it was last verified.
Only overlay networks that enroll a machine unattended can join a disposable sandbox. Melso supports two: Tailscale and Headscale, the self-hosted Tailscale control server.
Tailscale
For each Run, Melso uses an OAuth client to mint a single-use, ephemeral, pre-authorized device key for your tags. The key expires after five minutes, and the client secret never leaves the Melso API.
-
Create a tag for Runs. In your tailnet policy file, add a tag such as
tag:melsoand grant it exactly what agents may reach. Access control stays in the policy: a Run can reach what the tag can reach, nothing more.{ "tagOwners": { "tag:melso": ["autogroup:admin"] }, "grants": [ { "src": ["tag:melso"], "dst": ["tag:staging"], "ip": ["tcp:443", "tcp:5432"] }, { "src": ["tag:melso"], "dst": ["10.20.0.0/16"], "ip": ["tcp:443"] } ] }The second grant reaches services behind a subnet router; the router must advertise that route.
-
Create an OAuth client. In the Tailscale admin console, generate an OAuth client with the Auth Keys scope set to Write and the tag from step 1. Copy the client ID and the client secret (
tskey-client-…); the secret is shown once. -
Connect it. In Settings → Network, choose Connect, then Tailscale. Paste the client ID and secret, and enter the tags, separated by commas. Saving checks the client: Melso exchanges it for an access token with the
auth_keysscope and your tags. If Tailscale refuses, nothing is saved and Settings shows why.
Headscale
Headscale cannot mint a key per Run, so every Run receives the same pre-auth key. Scope it to a dedicated user or tags and give it an expiry. Runs on a Headscale network send no logs to Tailscale.
-
Create a user for Runs, or use tags from your Headscale policy:
headscale users create melso -
Create a reusable, ephemeral pre-auth key with an expiry. Add
--tags tag:melsoif your policy uses tags.headscale preauthkeys create --user <user> --reusable --ephemeral --expiration 90d -
Connect it. In Settings → Network, choose Connect, then Headscale. Enter the login server as an HTTPS origin with no path, such as
https://headscale.example.com, and paste the key.
Headscale cannot be verified without enrolling a device, so the first Run is the check. When the key expires, Runs fail to join: create a new key and paste it in Settings → Network → Edit.
What a Run gets
- The Run's worker joins the network before any harness, checkout step or skill runs, and leaves it when the Run completes, fails or is cancelled.
- Each Run is its own ephemeral device named
melso-followed by the first eight characters of the Run ID. On Tailscale it carries the configured tags; the control plane removes the device after the Run. - Subnet routes are accepted and MagicDNS names resolve by default. Turn off Accept subnet routes or Use network DNS in Settings to change that.
- Kernel mode is used when the sandbox allows it (a TUN device and passwordless
sudo): every program reaches the network transparently. Otherwise the worker falls back to userspace networking and points the harness at a local HTTP and SOCKS5 proxy on127.0.0.1:1055throughALL_PROXY,HTTP_PROXYandHTTPS_PROXY. Only programs that honor those variables reach the network then. The Run's brief says which mode applies. - A Run that cannot join fails before its harness starts, with
failure_reasonprivate_network_unavailableand "This task could not join the company network." It never runs half-connected. Check the credential, its tags and the tailnet policy, and the Last error in Settings.
Turning the network off keeps its settings; new Runs stop joining until it is on again. Turning it off or disconnecting it does not touch Runs already connected: they keep their device until they end.
Limits
- One network per company.
- Corporate VPNs that require interactive sign-in, MFA or device posture checks (Cisco AnyConnect, GlobalProtect, Zscaler, Fortinet) cannot enroll a sandbox. Run those tasks on a worker inside the company network instead.
- In userspace mode, programs that ignore proxy variables cannot reach the network.
Settings API
These routes accept people and admin keys: a signed-in browser session, a personal access token (mel_…) sent with X-Workspace-ID, or a workspace API key created with admin scope (see Credential types), which acts as a workspace admin here. tasks API keys, Run tokens and MCP credentials are rejected. Any member can read the settings; only owners, admins and admin keys can change them. No response ever contains a client secret or pre-auth key, and provider response bodies are never returned or logged.
The examples use the variables from Overview with MELSO_TOKEN set to a personal access token.
GET /api/workspaces/{workspaceId}/private-network
Auth: Workspace member (a person), or an admin API key.
Request: None.
Response: {private_network:PrivateNetwork|null}, where null means none is connected. PrivateNetwork is {provider:"tailscale"|"headscale",enabled:boolean,client_id:string,tags:string[],login_server:string,accept_routes:boolean,accept_dns:boolean,generation:integer,configured_by:UUID,configured_at:timestamp,verified_at:timestamp|null,last_error:string}. client_id and tags apply to Tailscale, login_server to Headscale. verified_at is the last successful check (null until one succeeds); last_error the last failure.
Status: 200; 403 this endpoint is only available to human actors for a credential that is neither a person's nor an admin API key, and workspace_key_scope for a tasks API key. Authentication and resource-access errors follow Overview.
curl -sS "$MELSO_URL/api/workspaces/$WORKSPACE_ID/private-network" \
-H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID"{
"private_network": {
"provider": "tailscale",
"enabled": true,
"client_id": "kAbc123CNTRL",
"tags": ["tag:melso"],
"login_server": "",
"accept_routes": true,
"accept_dns": true,
"generation": 3,
"configured_by": "00000000-0000-4000-8000-000000000001",
"configured_at": "2026-09-28T19:00:00Z",
"verified_at": "2026-09-28T19:00:01Z",
"last_error": ""
}
}PUT /api/workspaces/{workspaceId}/private-network
Auth: Workspace owner/admin (a person), or an admin API key. configured_by is then the key's identity.
Request: For Tailscale: provider:"tailscale", client_id:string, client_secret?:string (starts with tskey-client-), tags:string[] (1–20 tags, each matching tag:[a-zA-Z][a-zA-Z0-9-]*), accept_routes?:boolean, accept_dns?:boolean, enabled?:boolean. For Headscale: provider:"headscale", login_server:string (an https:// origin with no path, credentials, query or fragment), auth_key?:string (20–512 characters, no whitespace), accept_routes?:boolean, accept_dns?:boolean, enabled?:boolean. Leaving out client_secret or auth_key keeps the stored one while the provider stays the same; switching provider needs a new one. A left-out switch keeps its saved value, and is on for a new network. Every save increments generation.
Response: The GET shape.
Status: 200; 400 private_network_invalid (a field failed validation; error names it); 403 insufficient permissions for a member who is not an owner or admin, this endpoint is only available to human actors for a credential that is neither a person's nor an admin API key, or workspace_key_scope for a tasks API key; 422 private_network_rejected (Tailscale refused the OAuth client, or could not be reached to check it; nothing changed); 503 when the server has no Vault keyring to seal the secret. A Tailscale save that changes the provider, client ID, secret or tags is checked first: Melso mints a one-minute, single-use key for the tags with the OAuth client and deletes it at once. Saving the same credential again, for example to turn the network on or off, does not check it again. Authentication and resource-access errors follow Overview.
curl -sS -X PUT "$MELSO_URL/api/workspaces/$WORKSPACE_ID/private-network" \
-H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
-H "Content-Type: application/json" -d '{"provider":"tailscale","client_id":"kAbc123CNTRL","client_secret":"tskey-client-replace-me","tags":["tag:melso"],"accept_routes":true,"accept_dns":true,"enabled":true}'curl -sS -X PUT "$MELSO_URL/api/workspaces/$WORKSPACE_ID/private-network" \
-H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
-H "Content-Type: application/json" -d '{"provider":"headscale","login_server":"https://headscale.example.com","auth_key":"replace-with-the-pre-auth-key","accept_routes":true,"accept_dns":true,"enabled":true}'To turn the network off without sending the secret again, repeat the saved settings with "enabled":false and no client_secret or auth_key.
DELETE /api/workspaces/{workspaceId}/private-network
Auth: Workspace owner/admin (a person), or an admin API key.
Request: None.
Response: Empty.
Status: 204; 403 as for PUT. Runs already connected keep their device until they end. Authentication and resource-access errors follow Overview.
curl -sS -X DELETE "$MELSO_URL/api/workspaces/$WORKSPACE_ID/private-network" \
-H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID"