The box never listens for inbound support. floo makes the box dial out to the relay, and
that single foreground process is the access grant. Kill it → access is gone. Nothing is enabled at
boot, so a reboot can't reopen it. Everything else (who may enter, recording, the live command log,
and the state-diff) layers on top of: no live dial-out → no access.
| Component | Where it runs | Role |
|---|---|---|
floo |
the client box, as the client's user | dial out, stand up a throwaway sshd gated by CA mode or quick mode, show the pairing code, render the live command log, record, tear down |
relay (install-relay.sh → dedicated sshd + gw) |
the operator box | dumb switchboard: maps a random session id → a client's live reverse socket; splices ciphertext |
floo-route |
relay, as gw (ForceCommand) |
the only thing gw can do: register / resolve / meta / route / deregister / list / bindop / getop / opconfig |
floo-authkeys |
relay (AuthorizedKeysCommand) | accept any key (so no private key is published); the Match block makes gw powerless |
bin/floo-powder |
the operator box | ca-init / list / connect by code (pin host key, mint cert or bind a quick-mode key) / exec / close / gc |
The relay route is a random 16-hex session id. The human-readable name is only a display label and operator-side ssh alias; it is not an authenticator and is not trusted for uniqueness across a fleet. Auth rests on the certificate/code proof and the pairing code.
floo resolves it via a generic fallback chain: --name / FLOO_NAME → an optional
FLOO_IDENTITY_HOOK (a script that prints the name — how a deployment plugs in its own naming, e.g. from
a provisioning manifest) → hostname -s → id -un.
Operator → box ("is this really the operator?") = an SSH certificate. The client's throwaway
sshd trusts only the operator CA public key (TrustedUserCAKeys). To connect in CA mode, the
operator mints a ≤60-minute cert, principal = the session id, signed by the CA private key (operator
box only, in no repo). The cert principal must appear in the client's AuthorizedPrincipalsFile and the
login user must be the client's Unix user (loginuser, carried in the registration). Cert expiry is the
backstop; the live process is the real revoke.
Both ends → relay ("is this the real relay, not a hijack of relay.example.com?") = a
pinned relay host key. Its public half is embedded in floo (FLOO_RELAY_HOSTKEY) and pinned
operator-side by ca-init; install-relay.sh deploys the matching private half as the relay's host
key. accept-new still onboards a genuinely-new relay, but a mismatch against the pin is refused —
without this, a MITM'd relay could forge the pairing code + client host key the operator relies on.
Box → operator ("is this my real box, not a squatter on my name?") = a human pairing code.
The client prints a code on its own screen and registers only its hash (plus its ephemeral host public
key) at the relay. floo-powder connect <code> resolves that code to exactly one live session; the host
key from the same registration is pinned (UserKnownHostsFile under HostKeyAlias=<label>), so a
confirmed code also authenticates the box's key. Cert or quick-mode code proof proves operator→box;
code+pin prove box→operator. Mutual.
- relay endpoint:
FLOO_RELAY_HOST(no default — set via saved config /--relay/ pin-bootstrap; e.g.relay.example.com),FLOO_RELAY_PORT(443),FLOO_RELAY_USER(gw), socket namespaceFLOO_RELAY_SOCK_DIR(/run/floo), pinned relay host keyFLOO_RELAY_HOSTKEY(embedded; env-overridable for tests). - relay socket:
<sockdir>/<sid>.sock(the client's reverse unix-socket forward). - relay meta:
<sockdir>/<sid>.meta(codehash,loginuser,label,quick,registered,peer,hostkey). - dispatcher commands (
$SSH_ORIGINAL_COMMANDunder thegwForceCommand):register <sid> <codehash> <loginuser> <label> [quick=1] <hostkey...>resolve <codehash>→socket=live+ the meta fields +sidmeta <sid>→socket=live|absent+ the meta fieldsroute <sid>→exec nc -U <sockdir>/<sid>.sock(the operator pivot)bindop <sid> <hmac> <operator-key...>/getop <sid>for quick modederegister <sid>→ remove socket + meta + quick binds (client teardown)list→ live/known sessions by label (dead ones GC'd)opconfig→ serve the operator's public CA (so a pin-bootstrapped client fetches it over the host-key-verified relay instead of pasting it)
- operator transport:
bin/floo-powder connectwrites~/.ssh/floo.d/<name>.confwith aProxyCommand ssh … gw@relay route <sid>; then plainssh <name>/rsync … <name>:work.
- Client (
floo): snapshot the access surface → write private workdir ($XDG_RUNTIME_DIR/floo/<label>or$HOME/.local/state/floo/<label>, mode-0700) → ephemeral ed25519 host key → throwawaysshd_config(CA-trusted certs by default, code-authorized key in quick mode) → start sshd undersetsid(own process group) → generate a throwaway client key,registerthe random sid/codehash at the relay, openssh -N -R <sockdir>/<sid>.sock:127.0.0.1:<port> gw@relayundersetsid→ print the pairing code → render the merged live console. - Operator:
connect <code>resolves the sid, pins the client host key, mints a cert in CA mode or binds an ephemeral key in quick mode, drops the ssh-config include →ssh/rsync/exec. - Recording + live view: the recorder writes raw pty bytes to a fixed-name
recording/session.raw(fixed so the live console'stail -Fcan follow it even though it starts before the first operator connects). Bash/zsh hooks and the exec path add invisible private OSC markers forprompt/cmd/out/end, each stamped with a secret per-session nonce; the client-side renderer honours a marker only if the nonce matches (so operator-controlled command output cannot forge or hide a command line), sanitizes the command label of all control sequences, and turns the stream into a clean command log above a pinned status line. The rendered view drops the inter-command region (shell prompt + the echo of the typed command, bracketed byprompt→cmd) so each command shows once as$ <cmd>; full-screen TUI apps render inline (alt-screen is NOT suppressed — suppression keyed on operator output bytes could hide real output). The bash hook captures the full typed line from history with a distilled bash-preexec state machine (armed only between the end ofPROMPT_COMMANDand the next command, so anyPROMPT_COMMANDshape — string, array, function — and pipeline stages are handled; falls back toBASH_COMMANDwhen history is disabled /ignorespace, never mislabeling as a prior command). The same renderer produces the saved readablesession.log, so the live and saved rendered views never drift — while the rawsession.rawis kept untouched as the complete record. - Teardown (Ctrl-C / window close / any exit): release the terminal scroll region →
kill -- -PGIDthe tunnel and sshd groups (reaps every forked child — no orphans) → best-effortderegisterat the relay → confirm the local port is unbound → after-snapshot + diff → save the raw recording (session.raw, complete) + a readable renderedsession.logand any change-diff to~/.floo-last-session→ wipe the workdir.
- Audit (read-only):
floo-powder exec <name> < snapshot.sh(theopenclaw-client-auditrecipe). Recorded and rendered live on the client; the operator gets the verdict. - Upgrade (mutating): the toolbox is pushed from the operator after connecting
(
rsync … <name>:), never pulled — no repo credential ever lands on a client box. The upgrade replays a versioned, validated artifact (toolbox@tag +_scripts+contract.shguards), not keystrokes; stop-on-drift if a guard fails.
AllowTcpForwarding nois a master switch that ALSO disables stream-local forwarding. To allow only the reverse unix socket while blocking TCP, thegwMatchblock usesAllowTcpForwarding remote+PermitListen none+AllowStreamLocalForwarding remote. (Verified: the unix-socket tunnel works;-Land-R-TCP are both refused.)- No
restrictin the AuthorizedKeysCommand output —restrictdisables the stream-local listen the tunnel needs, andport-forwardingdoes not re-grant it. Confinement is the server-sideMatchblock, which applies regardless of key options. StreamLocalBindUnlink yesonly clears a socket before the next bind, not on disconnect — a killed tunnel leaves a stale, unconnectable socket file. So liveness is judged by connectability (socket_liveprobes vianc -U), the client deregisters on teardown, and the dispatcher GCs dead sockets lazily. (Security holds either way: a stale socket refuses connections.)AuthorizedKeysCommandmust be root-owned on a safe path — hence it installs to/usr/local/bin.- The client runtime dir must be on a path sshd considers safe (no world-writable ancestor) — hence
$XDG_RUNTIME_DIR(=/run/user/UID, mode 0700), not/dev/shm. - The trap covers
INT TERM HUPso closing the terminal (SIGHUP) also revokes, not just Ctrl-C. - Relay-on-Fedora gotchas (verified live on the operator box): (1) under systemd the relay runs
confined as
sshd_t, which may bind only the ssh port — binding:443(http_port_t) is refused untilinstall-relay.shrelabels it (semanage port -t ssh_port_t 443), reverted on--uninstall; (2)useradd --systemlocks thegwpassword (!), and sshd refuses even a publickey login to a locked account, so the installer sets it*(not locked, no usable password); (3) Fedora's sshd warnsUsePAM no is not supportedbut still honors cert auth for a non-locked account (proven end-to-end) — the warning is cosmetic. The whole relay is reboot-safe (systemd + a tmpfiles socket dir) and removes to zero leftovers (--uninstalldrops the unit, helpers,/etc/floo,/run/floo, thegwuser, the SELinux label, and the firewall opening; only~/.config/flookeys stay).
curl|bashis pinned to a commit hash; a sha256 + signature published over a second channel, with a download→verify→read→run bootstrap, is a planned hardening (the residual risk is integrity of the fetched script, not secrecy).- Full
homenum-revelioreuse for a deep audit is operator-pushed during a session;floo's built-in state-diff is the self-contained 3-surface (keys/units/cron) disclosure. - A scrollback UI with pause/search. The saved cleaned recording covers after-the-fact review; the live view stays deliberately simple.