Skip to content

Latest commit

 

History

History
98 lines (76 loc) · 4.31 KB

networking.md

File metadata and controls

98 lines (76 loc) · 4.31 KB

Networking and reachability

This page explains how a caller reaches a sandbox and why each path is shaped the way it is. For the settings, read Deploy.

Two paths

Each provider decides whether its hosts join the tailnet. The API response carries both addresses; which is populated depends on the provider:

  • external_ssh_host / external_ssh_port — the provider-given public path. Always present (empty for a host with no public path, such as an AWS host on the tailnet).
  • internal_ssh_host — the tailnet MagicDNS name, populated only for a host on the tailnet. The internal path is always port 22 by Tailscale convention, so there is no internal port field.

Callers pick whichever path they can reach and dial it themselves.

On the tailnet: the overlay is the security model

Per sandbox, drukbox mints an ephemeral, tag-scoped Tailscale auth key and injects it at boot. The sandbox joins the tailnet, drukbox discovers the device by hostname (one shared poller serves all concurrent provisions), stores the device ID, and ssh-keyscans over the tailnet path.

On AWS a host on the tailnet has no public surface at all: no keypair is imported, no security group is managed, and no public IP is attached — authentication and reachability are tailnet ACLs plus tailscaled-SSH, and external_ssh_host comes back empty.

Hetzner can't drop the public IP that cheaply (a server needs one to reach the tailnet), so a Hetzner box on the tailnet still has a public IPv4 — but it is key-locked with the per-VM key drukbox always mints, and external_ssh_host carries that public path alongside the tailnet one. The tailnet remains the intended path; the public port just isn't left open to passwords.

Teardown releases the Tailscale device by its stored ID — never by hostname — and treats a 404 as success, because ephemeral devices self-delete and operator cleanup can race the API.

Off the tailnet: public path, key-only auth

Off the tailnet the provider's public path is the only path.

On exe.dev, SSH terminates at exe's edge (ssh_dest), and exe owns authentication; drukbox returns no key material.

On AWS, drukbox generates a per-VM ed25519 keypair, imports the public half, and returns the private half exactly once in the create response — it is never persisted, and a later GET /hosts/{id} returns private_key: null. Ingress comes from the managed security group (drukbox-managed): SSH from the service's detected public egress IP as a /32 by default, AWS_SSH_CIDRS authoritative when set, and an operator-supplied AWS_SECURITY_GROUP_ID is never touched. If egress detection fails, ingress falls back to 0.0.0.0/0 with a warning log — the per-VM key remains the auth boundary, never the source address.

On Hetzner the per-VM keypair works the same way, but a fresh server has no firewall — port 22 is open and the key is the only boundary. There is no ingress configuration to manage.

The IP-literal invariant

The detected-/32 ingress rule is only sound under one invariant: the address callers dial must route over the same path the detector saw. That is why external_ssh_host on AWS is the sandbox's public IP literal and never its public DNS name.

EC2 public DNS is split-horizon: outside the VPC it resolves to the public IP, but inside the VPC it resolves to the sandbox's private IP. A same-VPC caller dialing the DNS name therefore arrives from its private address — which a public-/32 rule silently drops, surfacing as SSH connect timeouts that depend on which resolver answered. Dials to the IP literal hairpin through the IGW and arrive from the detected egress IP no matter where the caller sits.

The same trap applies to operators who set AWS_SSH_CIDRS: if callers share the sandboxes' VPC and you restrict ingress, include the VPC CIDR, not just a public /32.

known_hosts

The response's known_hosts field carries ssh-keyscan-style host key material, scanned against the preferred path (internal_ssh_host when present, else external_ssh_host) and keyed to whichever address was scanned.

Off the tailnet, the first-time keyscan runs over the public network and carries the same TCP-session MITM window any first-time keyscan has. Operators who need stronger guarantees should put the provider on the tailnet and let the scan run over the authenticated overlay.