Skip to content

ADR 0003 — Company tailnet owns CX23/CX43; personal access via Tailscale node sharing

SSH access to both CX23 (Supabase) and CX43 (Dokploy, Authelia, hosted services) is restricted to the Tailscale network interface. ufw drops all connections to port 3003 (SSH) from non-Tailscale source IPs. There is no public SSH exposure. This posture was chosen deliberately: key-based SSH hardened against a Tailscale-only interface is stricter than key-based SSH on a public interface, and the Tailscale control plane provides an additional authentication layer.

The initial deployment enrolled CX23 and CX43 in a personal Tailscale account (the solo founder’s personal tailnet). This created three structural problems:

1. Personal identity dependency. Server identity is tied to a personal account. If that account is compromised, closed, or needs to be rotated, the servers lose their Tailscale connectivity — and since SSH is Tailscale-only, the servers become unreachable without out-of-band access (Hetzner VNC console).

2. Device sprawl. Personal tailnets include all personal devices by default: home servers, laptops, mobile devices, home automation hubs. These devices become mesh peers of the production servers. A compromised personal device could reach production hosts directly.

3. Scaling and offboarding. If a second operator (contractor, co-founder) needs SSH access, the cleanest model is to add them to the tailnet. But adding a person to a personal tailnet gives them visibility into all personal devices — unacceptable. Node sharing is the correct mechanism, but it requires a stable “source” tailnet that does not change when operators come and go.

Tailscale supports two models for cross-identity access:

  • Tailnet membership: the user’s Tailscale account joins the same tailnet. They see all devices in the tailnet according to ACLs. This is right for operators who need full fleet visibility (e.g., a platform team).
  • Node sharing: a node in tailnet A is shared to a specific email address. It appears in that person’s device list as a shared node, without them joining tailnet A. ACLs and tags remain owned and enforced by tailnet A. The shared node is a read-only guest in the recipient’s client.

Node sharing is the correct pattern for individual access to specific servers without granting tailnet membership.

A dedicated company Tailscale tailnet (corahealth org) owns CX23 and CX43.

Server nodes are tagged at enrolment:

  • CX23: tag:supabase-prod
  • CX43: tag:ops

Tags are defined in the company tailnet’s ACL policy. Tag owners are restricted to the org admin account.

Personal devices access production servers via Tailscale node sharing, not tailnet membership.

The company tailnet admin shares CX23 and CX43 to specific personal Tailscale identities (email addresses). Shared nodes appear in the personal device list and are reachable via SSH and Tailscale Serve HTTPS, but the personal identity does not join the company tailnet and cannot see other company tailnet devices.

Access control

The company tailnet ACL policy grants the tag:supabase-prod and tag:ops tags access only to the ports they need. A shared personal identity is further restricted to only the ports needed for day-to-day work: SSH (3003), Studio (via Tailscale Serve on tailnet HTTPS), Dokploy UI (3001). Shared nodes cannot reach internal service ports (Postgres 5432, Redis, etc.) unless explicitly granted.

Offboarding

To revoke a personal operator’s access: revoke the node share from the company tailnet admin panel. The servers are unaffected; no server-side config changes are needed.

Home-lab isolation

Personal home servers, home automation hubs, and personal laptops are invisible to the production fleet. The tailnets are separate mesh networks; there is no route between them except through the deliberately shared nodes.

Migration SOP from personal to company tailnet

The migration carries a lockout risk because SSH is Tailscale-only. Perform the migration with Hetzner VNC console access confirmed beforehand. The full procedure is documented at docs/server/supabase/security/tailnet-migration.md. Key steps:

  1. Confirm Hetzner VNC console works for both CX23 and CX43 before starting.
  2. Generate a reusable auth key on the company tailnet with the appropriate tags.
  3. On the server (inside a tmux session to survive disconnect):
    tailscale logout
    tailscale up --authkey=<company-key> --advertise-tags=tag:supabase-prod
  4. Verify the node appears in the company tailnet admin panel.
  5. Re-share the node to personal identities from the company tailnet admin.
  6. Update all MagicDNS hostnames in documentation and Caddy/Traefik configs — MagicDNS names change when the tailnet changes.
  7. Re-issue tailscale serve config for Studio on CX23 — Serve config is per-tailnet.
  8. Migrate CX23 first, verify SSH and Studio access, then migrate CX43.

Ongoing operations

  • Auth key rotation: generate a new reusable auth key in the company tailnet; run tailscale up --authkey=<new-key> on the server to re-authenticate without logout.
  • New server: enrol with a tagged reusable auth key from the company tailnet, then share to personal identities as needed.
  • ACL changes: edit the company tailnet policy file in the Tailscale admin console; changes apply immediately to all tagged nodes.

Keep servers on the personal tailnet

Simplest in the short term — no migration needed. Rejected because it ties production infrastructure to a personal identity. If the personal account is closed, rotated for security reasons, or transferred to a new owner, the servers lose connectivity. It also exposes production servers to the personal device mesh, which violates the principle of least exposure.

Add a second operator as a member of the personal tailnet

Gives the second operator full tailnet membership. They would see all personal devices, which is not acceptable. Node sharing achieves the same access to specific servers without any cross-visibility. This is the pattern Tailscale explicitly recommends for this scenario.

Public SSH with key-based auth and fail2ban

Remove the Tailscale dependency for SSH. Rejected because it expands the attack surface. The current ufw-on-tailscale posture means SSH is unreachable from the public internet regardless of key configuration. Public SSH with key-based auth is a reasonable baseline but is strictly weaker than no public SSH at all. The operational cost of running Tailscale on both client and server is low; the security benefit is high.