Linux/systemd colibri-bridge packaging + domedog network facts #203
4 changed files with 208 additions and 0 deletions
96
packaging/linux/README.md
Normal file
96
packaging/linux/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Colibri bridge — Linux (systemd)
|
||||
|
||||
Linux/systemd peer of `packaging/freebsd/colibri_bridge.in`. Exposes the
|
||||
colibri-daemon control-plane Unix socket as a TCP port **on the Tailscale
|
||||
interface only**, so other mesh hosts can drive the control plane.
|
||||
|
||||
The FreeBSD bridge already runs on hermes (binds hermes's own tailnet address);
|
||||
this is the matching domedog side. The network gate (ufw) is **already applied
|
||||
on domedog** (see "domedog host facts"); the systemd unit here is the proposed
|
||||
service — review the open questions with hermes before `enable --now`.
|
||||
|
||||
> Real `100.x` Tailscale addresses are never committed to git. Each host's
|
||||
> address is supplied at deploy time (`tailscale ip -4`) via
|
||||
> `/etc/colibri/bridge.env` — see `COLIBRI_BRIDGE_LISTEN_ADDR=TAILSCALE_IP_REQUIRED`.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `colibri-bridge.service` | systemd unit running socat (peer of the rc.d script) |
|
||||
| `colibri-bridge.env.example` | tunables — install to `/etc/colibri/bridge.env` |
|
||||
| `colibri-bridge.nft` | nftables ruleset **for hosts without ufw** (see facts below) |
|
||||
|
||||
## Install (one-time, as root)
|
||||
|
||||
```sh
|
||||
install -D -m 0644 packaging/linux/colibri-bridge.service \
|
||||
/etc/systemd/system/colibri-bridge.service
|
||||
install -D -m 0644 packaging/linux/colibri-bridge.env.example \
|
||||
/etc/colibri/bridge.env # then edit for this host
|
||||
|
||||
# Firewall: scope port 9190 to tailscale0 (peer of the hermes pf rule).
|
||||
# domedog runs ufw (active, default-deny input) — the allow goes in ufw, and
|
||||
# ufw's default-deny already blocks 9190 on every other interface:
|
||||
ufw allow in on tailscale0 to any port 9190 proto tcp comment 'colibri-bridge'
|
||||
# On a host WITHOUT ufw / with a default-accept input, use the nft table instead:
|
||||
# install -D -m 0644 packaging/linux/colibri-bridge.nft /etc/colibri/colibri-bridge.nft
|
||||
# nft -f /etc/colibri/colibri-bridge.nft
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now colibri-bridge.service
|
||||
systemctl status colibri-bridge.service
|
||||
```
|
||||
|
||||
Quick check from another tailnet host:
|
||||
|
||||
```sh
|
||||
printf '{"cmd":"status"}\n' | nc -w2 "$(tailscale ip -4)" 9190 # run on/against this host
|
||||
```
|
||||
|
||||
## domedog host facts (verified 26.jun.2026)
|
||||
|
||||
The networking reality this packaging assumes, recorded so the next person
|
||||
doesn't have to re-derive it:
|
||||
|
||||
- **Tailnet IPv4:** each host has a distinct `100.x` address — get it with
|
||||
`tailscale ip -4` (not committed to git per policy).
|
||||
- **Init / service mgr:** systemd (Ubuntu 24.04). FreeBSD/hermes uses rc.d.
|
||||
- **Daemon user:** `clawdija` (hermes: `clawdie`). No dedicated `colibri`
|
||||
group exists on domedog; the bridge runs as the daemon's own user so it can
|
||||
reach the 0770 socket.
|
||||
- **Firewall:** **ufw active + enabled**, default **deny (incoming)** /
|
||||
allow (outgoing), backed by nftables, with fail2ban on top. Live packet
|
||||
counters confirm it's enforcing, not just configured.
|
||||
- **Currently-allowed inbound ports:** 22/tcp (ssh), 80/tcp, 443 (tcp+udp),
|
||||
8433–8443/tcp, and now **9190/tcp scoped to `tailscale0`** (the bridge, added
|
||||
for this work).
|
||||
- **Port 8443 = CloudPanel** server control panel (nginx vhost
|
||||
`cloudpanel.conf`, run by the `clp` user from `/home/clp/services/nginx/`).
|
||||
Only 8443 actually listens; 8433–8442 are allowed but unused (range wider
|
||||
than needed). ⚠️ CloudPanel's admin login is exposed to **Anywhere** (public),
|
||||
not tailnet-scoped — the opposite posture from this bridge; flagged for the
|
||||
host-exposure review.
|
||||
|
||||
## Open questions for the hermes discussion
|
||||
|
||||
1. **The socket has no auth — the tailnet boundary is the auth.** With both
|
||||
hosts bridging the control plane, any tailnet peer that can reach either host
|
||||
can issue full control-plane commands (`spawn-agent`, `kill-agent`,
|
||||
`terminal-*`). Consider a Tailscale ACL scoping `:9190` to specific peers.
|
||||
2. **Socket path parity.** Both sides assume `/run/colibri/colibri.sock`
|
||||
(FreeBSD: `/var/run/colibri/colibri.sock`). domedog's daemon must be started
|
||||
with a matching `COLIBRI_DAEMON_SOCKET`; the daemon's default is under
|
||||
`$XDG_DATA_HOME`, which the bridge can't see under `ProtectHome=yes`.
|
||||
3. **CloudPanel public exposure** (8443) — decide whether to keep it public or
|
||||
move it behind the tailnet like the bridge.
|
||||
|
||||
## Notes
|
||||
|
||||
- The FreeBSD `colibri_bridge.in` `health`/`status` block bug (scrambled
|
||||
function definitions) was **fixed and pushed** on the hermes side during this
|
||||
work.
|
||||
- `colibri-bridge.nft` is retained for hosts with a default-accept input chain;
|
||||
on a ufw host its `accept` is not reliable (ufw's `drop` is terminal across
|
||||
base chains) and its `drop` is redundant (ufw already default-denies). Kept as
|
||||
documentation of intent and for the no-ufw case.
|
||||
18
packaging/linux/colibri-bridge.env.example
Normal file
18
packaging/linux/colibri-bridge.env.example
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Colibri bridge configuration — systemd EnvironmentFile.
|
||||
# Install to /etc/colibri/bridge.env (root:root, 0644).
|
||||
# This is the systemd parallel to the sysrc vars in the FreeBSD rc.d script
|
||||
# (packaging/freebsd/colibri_bridge.in).
|
||||
|
||||
# Tailscale IPv4 of THIS host. socat binds here — never 0.0.0.0 — so the control
|
||||
# plane is not exposed on any other interface even if the firewall rule is
|
||||
# removed. Fill in with this host's own address from `tailscale ip -4`.
|
||||
# (Real 100.x addresses are never committed to git — set it at deploy time.)
|
||||
COLIBRI_BRIDGE_LISTEN_ADDR=TAILSCALE_IP_REQUIRED
|
||||
|
||||
# TCP port. MUST match the firewall allow-rule that scopes 9190 to tailscale0.
|
||||
COLIBRI_BRIDGE_LISTEN_PORT=9190
|
||||
|
||||
# colibri-daemon Unix socket. MUST equal the daemon's COLIBRI_DAEMON_SOCKET.
|
||||
# The daemon default is under $XDG_DATA_HOME; for a system bridge point both at
|
||||
# a stable /run path (and keep it off /home so ProtectHome=yes can stay on).
|
||||
COLIBRI_BRIDGE_SOCKET=/run/colibri/colibri.sock
|
||||
28
packaging/linux/colibri-bridge.nft
Normal file
28
packaging/linux/colibri-bridge.nft
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/sbin/nft -f
|
||||
#
|
||||
# Colibri bridge firewall — restrict the control-plane TCP port to the Tailscale
|
||||
# interface only. Linux/nftables peer of the hermes pf rule:
|
||||
#
|
||||
# pass in quick on tailscale0 proto tcp to port 9190 flags S/SA keep state
|
||||
#
|
||||
# Load standalone: sudo nft -f packaging/linux/colibri-bridge.nft
|
||||
# Verify: sudo nft list table inet colibri
|
||||
# Persist (Debian/Ubuntu): `include "/etc/colibri/colibri-bridge.nft"` from
|
||||
# /etc/nftables.conf, then `systemctl enable --now nftables`.
|
||||
|
||||
table inet colibri {
|
||||
chain input {
|
||||
# Base-chain declaration — REQUIRED. Without `type … hook input …` the
|
||||
# chain is inert and filters nothing. policy accept keeps this table
|
||||
# surgical: it governs ONLY port 9190; all other traffic falls through
|
||||
# to the host's existing rules.
|
||||
type filter hook input priority 0; policy accept;
|
||||
|
||||
# Reach the bridge only over the tailnet…
|
||||
iifname "tailscale0" tcp dport 9190 accept
|
||||
# …and drop it on every other interface. `drop` is a terminal verdict,
|
||||
# so this reliably blocks 9190 from the public side even if another
|
||||
# table would otherwise accept it.
|
||||
tcp dport 9190 drop
|
||||
}
|
||||
}
|
||||
66
packaging/linux/colibri-bridge.service
Normal file
66
packaging/linux/colibri-bridge.service
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
[Unit]
|
||||
# Colibri control-plane TCP bridge — Linux/systemd peer of the hermes rc.d
|
||||
# service in packaging/freebsd/colibri_bridge.in.
|
||||
#
|
||||
# Bridges the colibri-daemon Unix socket to a TCP port on the Tailscale
|
||||
# interface so other mesh hosts can reach the Colibri control plane. socat runs
|
||||
# in the foreground; systemd supervises it (restart on crash, journald logs) —
|
||||
# the systemd equivalent of FreeBSD running socat under daemon(8).
|
||||
Description=Colibri control-plane TCP bridge (Tailscale -> Unix socket)
|
||||
Documentation=https://code.smilepowered.org/clawdie/colibri
|
||||
After=network-online.target tailscaled.service colibri-daemon.service
|
||||
Wants=network-online.target
|
||||
# Ordering + lifecycle: the bridge is useless without the daemon, and the
|
||||
# socket vanishes if the daemon stops — so bind our lifecycle to it.
|
||||
Requires=colibri-daemon.service
|
||||
BindsTo=colibri-daemon.service
|
||||
# Keep retrying while we wait for tailscaled to assign the address at boot
|
||||
# (paired with freebind below, this race is largely moot, but be forgiving).
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
# Must be a user in the colibri-daemon socket's group (the daemon chmods its
|
||||
# socket 0770 owner+group). On domedog that is the daemon's own user.
|
||||
User=clawdija
|
||||
Group=clawdija
|
||||
|
||||
# Tunables live here (systemd parallel to the rc.d sysrc vars). See
|
||||
# colibri-bridge.env.example.
|
||||
EnvironmentFile=/etc/colibri/bridge.env
|
||||
|
||||
# Refuse to start until the daemon socket actually exists, mirroring the rc.d
|
||||
# prestart check (clearer failure than a socat connect error).
|
||||
ExecStartPre=/usr/bin/test -S ${COLIBRI_BRIDGE_SOCKET}
|
||||
|
||||
# bind=<tailscale-ip> keeps us off every other interface even if the firewall
|
||||
# is flushed. freebind (Linux IP_FREEBIND, no privilege needed) lets socat bind
|
||||
# the tailnet address before tailscaled has finished bringing it up, avoiding a
|
||||
# boot-order race — the one place this unit improves on the FreeBSD version.
|
||||
ExecStart=/usr/bin/socat -d \
|
||||
TCP-LISTEN:${COLIBRI_BRIDGE_LISTEN_PORT},bind=${COLIBRI_BRIDGE_LISTEN_ADDR},freebind,fork,reuseaddr \
|
||||
UNIX-CONNECT:${COLIBRI_BRIDGE_SOCKET}
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
|
||||
# --- hardening: socat needs only TCP + Unix sockets and no privileges ---
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
# ProtectHome=yes is safe ONLY while the socket lives outside /home (it does, by
|
||||
# default, under /run). If you repoint COLIBRI_BRIDGE_SOCKET into a home dir,
|
||||
# relax this to read-only or the connect will fail.
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
RestrictAddressFamilies=AF_INET AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallErrorNumber=EPERM
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Loading…
Add table
Reference in a new issue