Private access with Tailscale
Cloud Agent Fleet is reachable only over your tailnet. The browser reaches the
Hub through Tailscale Serve on two HTTPS ports: 443 for the dashboard and 8443
for proxied host UIs (a separate origin, so backend pages can't use your Hub
session against the Hub API). The Hub reaches hosts over the tailnet. Nothing
is published with Funnel, and Cloud Agent Fleet never binds host-local :80
or :443.
Browser → Serve :443 → Hub dashboard on 127.0.0.1:8787
Browser → Serve :8443 → Hub host UIs on 127.0.0.1:8788 → tailnet → hosts
Hub UI via Tailscale Serve#
Prerequisites#
- Tailscale installed and connected on the Hub node (
tailscale status). - Hub bound to loopback (
CLOUDAGENTFLEET_HOST=127.0.0.1, the default). - Funnel disabled.
- Operators reach only the Hub tag; the Hub reaches Codeman hosts. Operators get no direct Codeman grant.
Serve publishes MagicDNS HTTPS on the node's Tailscale endpoint (typically 443 on the tailnet name). That is separate from the machine's ordinary 80/443 services, but it shares the node's Serve configuration with any other Serve routes.
Start Serve#
cloudagentfleet-hub # or, from a clone: pnpm --filter @cloudagentfleet/hub dev
cloudagentfleet hub serve
hub serve checks the Tailscale CLI and local Hub health, then serves only the
loopback Hub. Equivalent Serve command:
tailscale serve --bg http://127.0.0.1:8787
Custom Hub port:
CLOUDAGENTFLEET_HOST=127.0.0.1 CLOUDAGENTFLEET_PORT=9000 cloudagentfleet-hub
cloudagentfleet hub serve --port 9000
The command rejects non-loopback Hub binds.
Hub password#
On first start with a durable data directory and no password env vars, the
Hub generates a random UI password, stores it in hub-ui-auth.json in the Hub's data folder (~/.cloudagentfleet/),
and prints the plaintext on every start until you replace it.
# Optional override — stops printing the bootstrap password
export CLOUDAGENTFLEET_UI_PASSWORD='use-a-long-random-password'
# or
export CLOUDAGENTFLEET_UI_PASSWORD_HASH='scrypt$N$r$p$salt$hash'
Worker token#
Workers authenticate with a bearer token, separate from the UI password. When
CLOUDAGENTFLEET_AUTH_TOKEN is unset and the Hub has a durable data
directory, the Hub generates one in <data dir>/hub-worker-token (owner-only)
and logs only that path. Give its contents to cloudagentfleet setup (it asks
for the Hub Worker token) or set CLOUDAGENTFLEET_AUTH_TOKEN on both sides.
Delete the file and restart the Hub to rotate it, then rerun setup on each
Worker.
Codeman Basic credentials stay server-side (CLOUDAGENTFLEET_CODEMAN_CREDENTIALS
or enroll-time --username / --password). They are never returned to the
browser.
Grants#
Tailscale ACLs apply only to tailnet traffic; they do not close LAN or loopback ports. Once you replace the default allow-all policy, unlisted tailnet ports are deny-by-default, so list only what you intend.
{
"tagOwners": {
"tag:cloudagentfleet-hub": ["autogroup:admin"],
"tag:codeman-host": ["autogroup:admin"]
},
"grants": [
{
"src": ["group:cloudagentfleet-operators"],
"dst": ["tag:cloudagentfleet-hub"],
"ip": ["tcp:443", "tcp:8443"]
},
{
"src": ["tag:cloudagentfleet-hub"],
"dst": ["tag:codeman-host"],
"ip": ["tcp:3000"]
}
]
}
Change 3000 if Codeman listens elsewhere.
Inspect or stop Serve#
cloudagentfleet hub status
cloudagentfleet hub stop
hub stop runs tailscale serve reset on this node. Use it only when this
node's Serve routes are dedicated to Cloud Agent Fleet. It does not stop SSH,
the Hub process, or Codeman.
Optional: operator SSH on a dedicated port#
For shell access to fleet machines, run sshd on a dedicated port (example
2222) and grant only that port. Add this grant next to the Hub grants above.
{
"tagOwners": {
"tag:cloudagentfleet-host": ["autogroup:admin"]
},
"grants": [
{
"src": ["group:cloudagentfleet-operators"],
"dst": ["tag:cloudagentfleet-host"],
"ip": ["tcp:2222", "icmp:*"]
}
]
}
Equivalent ACL shape:
{
"tagOwners": {
"tag:cloudagentfleet-host": ["autogroup:admin"]
},
"acls": [
{
"action": "accept",
"src": ["group:cloudagentfleet-operators"],
"dst": ["tag:cloudagentfleet-host:2222"]
}
]
}
Tag each machine that should accept operator SSH as tag:cloudagentfleet-host.
To use standard SSH, grant tcp:22 instead.
ssh -p 2222 user@machine-name.tailnet-name.ts.net
Security checklist#
- Hub bound to
127.0.0.1; only Serve reaches it. - Hub password set (or the bootstrap password stored and rotated).
- Funnel off on every fleet node.
- Operators granted the Hub tag only, never Codeman ports.
- Codeman hosts accept the Codeman port only from
tag:cloudagentfleet-hub.