colibri/docs/guide/architecture/control-plane-bridge.md
Sam & Claude 5e2692c063
Some checks are pending
CI / rust (pull_request) Waiting to run
CI / markdown (pull_request) Waiting to run
CI / port (pull_request) Waiting to run
CI / agent-jail-pkgs (pull_request) Waiting to run
docs(guide): add Control-Plane Bridge architecture page
Document the cross-host control-plane bridge (socat TCP on tailscale0 →
colibri-daemon Unix socket): FreeBSD rc.d vs Linux systemd parity, the
interface-scoped firewall gate (pf / ufw), the "tailnet boundary is the auth"
security model (no socket auth; scope :9190 via Tailscale ACL), and config
notes (TAILSCALE_IP_REQUIRED placeholder, socket-path parity, 0770 group).
Points at packaging/{freebsd,linux}/ for install. Linked from the architecture
index next to Control Plane. No real tailnet IPs (placeholders only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:33:53 +02:00

3.8 KiB

title description
Control-Plane Bridge Reaching the Colibri control plane across hosts over the Tailscale mesh.

Each host runs colibri-daemon listening on a Unix domain socket (local only). The control-plane bridge exposes that socket as a TCP port on the Tailscale interface so other mesh hosts can drive the control plane — create tasks, register agents, watch terminals — without the socket ever being reachable from the public internet.

operator / peer host                      bridged host
  nc <tailnet-ip> 9190  ──tailscale0──▶  socat TCP-LISTEN:9190
                                              │ (bind = this host's tailnet IP)
                                              ▼
                                         UNIX-CONNECT /run/colibri/colibri.sock
                                              │
                                              ▼
                                         colibri-daemon

Implementations

The bridge is a thin socat front-end, supervised by the host's service manager. Both sides are shipped in the repo:

Host Service Packaging
FreeBSD rc.d colibri_bridge packaging/freebsd/colibri_bridge.in
Linux systemd colibri-bridge.service packaging/linux/ (unit + env + nft + README)

Both run effectively:

socat TCP-LISTEN:9190,bind=<this-host-tailnet-ip>,fork,reuseaddr \
      UNIX-CONNECT:/run/colibri/colibri.sock

The Linux unit adds freebind so socat can bind the tailnet address before tailscaled has finished bringing it up, avoiding a boot-order race. The bind=<tailnet-ip> keeps the listener off every other interface even if the firewall is later changed — defence in depth, not the primary gate.

Network gate

The bridge port is opened only on the Tailscale interface, in the host's native firewall:

  • FreeBSD (pf): pass in quick on tailscale0 proto tcp to port 9190 keep state
  • Linux (ufw): ufw allow in on tailscale0 to any port 9190 proto tcp

On a default-deny host (e.g. ufw), the public side is already blocked, so only the interface-scoped allow is needed. The packaging/linux/colibri-bridge.nft ruleset is provided for Linux hosts that do not run ufw (a default-accept input chain); under ufw it is redundant.

Security model — the tailnet boundary is the auth

The control-plane socket has no authentication of its own. Once it is bridged, any peer that can reach the host over the tailnet can issue the full command set (spawn-agent, kill-agent, intake-task, terminal-*, …). That makes the Tailscale boundary the access control:

  • Scope the port to named peers with a Tailscale ACL on :9190 rather than relying on the firewall allow alone.
  • Treat any bridged host as granting control-plane authority to the whole tailnet unless an ACL narrows it.

Configuration notes

  • No real tailnet IPs in git. Config templates ship the placeholder TAILSCALE_IP_REQUIRED; the operator fills the host's own address at deploy time (tailscale ip -4). The FreeBSD rc.d defaults likewise refuse to start until the address is set.
  • Socket-path parity. The bridge connects to /run/colibri/colibri.sock (FreeBSD: /var/run/colibri/colibri.sock); the daemon must be started with a matching COLIBRI_DAEMON_SOCKET. The daemon's default lives under $XDG_DATA_HOME, which a sandboxed unit (ProtectHome=yes) cannot reach — point both at the /run path.
  • The bridge user must be in the daemon socket's group (the socket is 0770, owner + group).

Verify

From another tailnet host:

printf '{"cmd":"status"}\n' | nc -w2 <host-tailnet-ip> 9190

A healthy bridge returns the daemon's status JSON (including the daemon's host), confirming reachability end to end over the mesh.