This page explains how a caller reaches a sandbox and why each path is shaped the way it is. For the settings, read Deploy.
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.
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 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 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.
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.