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