Floo & Homenum Revelio :: Magic in your terminal
Halloween has just passed, so here are two magic bash tools of mine (the names nod to the floo network and to a Potter spell), both open source. Floo gets you into someone else's Linux machine to do a job and leave no do
Halloween has just passed, so here are two magic bash tools of mine (the names nod to the floo network and to a Potter spell), both open source. Floo gets you into someone else's Linux machine to do a job and leave no door behind. Homenum Revelio shows who else can get into a machine that has just become yours.
A couple of weeks ago I set up my Steam Machine by turning on SSH by hand and dropping my key in. Only afterwards did I think I should have used floo. The key is still in authorized_keys, until I remember it.
This keeps happening. Someone, a person or an agent, needs to get onto a server, a VPS, a friend's laptop or a Steam Deck once, without opening a port or leaving standing access. TeamViewer gives you a desktop, not a terminal. Teleport is great for your own machines, but nobody deploys it for one evening on somebody else's Deck.
I wanted TeamViewer for the terminal: one command, a code read out loud, and the helper is in, with temporary access, a recording, and a way to cut it at any second. I found nothing like that without an install (maybe I didn't look hard enough). Floo grew out of supporting client servers for one product of mine, and later I moved it into its own repository.
The fireplace and the powder
In the books you throw Floo powder into a fire, say an address, and step out of someone else's fireplace. Here the network is a relay. The person who needs help connects their fireplace to it with floo, and the helper throws the powder with floo-powder.
The person at the machine goes first:
curl -fsSL https://raw.githubusercontent.com/interkelstar/floo/v0.8.2/floo \
| bash -s -- --public --relay relay.floo.run --pin df2b83ff925f89bd
A code appears on their screen. They read it to the helper, who runs this on their side:
bash <(curl -fsSL https://raw.githubusercontent.com/interkelstar/floo/v0.8.2/bin/floo-powder) \
--relay relay.floo.run --pin df2b83ff925f89bd connect THE-CODE-THEY-READ-OUT
After that the helper has a shell on that machine.
Nobody types that on a Steam Deck from dictation, so last week the project got a domain, and the public relay and its pin moved inside the scripts:
curl -s https://floo.run/sh | bash
bash <(curl -s https://floo.run/powder) connect CODE
floo.run serves the bytes of the v0.8.2 tag, not main. /sh is 23 lines that fetch floo from that tag and run it with --public, the only thing the person at the machine runs unread. A small Cloudflare Worker serves it all, from web/ in the repo.
The machine opens no port to the outside. floo calls the relay on 443, starts a throwaway sshd on localhost and forwards it over a reverse unix socket. Access lasts while the process is alive in that terminal; Ctrl-C or closing the window ends it.
The relay is an isolated sshd with one unprivileged account, gw, allowed to hold such a socket and nothing else. The helper's SSH ends at the client's sshd, and the relay just glues two streams of ciphertext together.
The helper presents an SSH certificate that lives an hour at most and is signed by the helper's CA (in quick mode a code replaces it). Both sides recognise the relay by a pinned host key, the --pin flag. The helper recognises the machine by the code read out to them, and the relay keeps only its SHA-256.
There are two files rather than one with subcommands. The person at the machine is running a stranger's code, and it's enough for them to read their half, with nothing in it about CAs, invites or installing a relay.
Growing up on real machines
In the first version a relay session was named after the machine's hostname, which breaks as soon as an operator with several clients meets two identical hostnames. v0.2 switched to a random session ID plus the code; two containers, both called twin, each lived happily with its own code.
The best story came from the first real server. The CA public key then travelled on the command line, in an --operator-ca flag, and Cockpit's web terminal wrapped the pasted line in the middle of the key. A broken CA landed on disk, and sshd answered every certificate with Permission denied (publickey). We dug into PAM for a while, but the client's sshd log had the answer all along: Failed publickey … CA and no Accepted certificate, so sshd didn't trust that CA.
Now the client gets the relay address and 16 hex characters of its host key fingerprint, and floo pulls the operator's public CA from the relay over the verified channel. In September this scheme turned out to have a weak spot, which I'll come back to.
Another mistake was mine alone. In v0.3.0 floo-powder became self-contained, with the whole relay and its installer embedded in the file, and I embedded it as base64 to keep heredoc delimiters from clashing. So floo-powder init ran under sudo a wall of letters. The project's motto is "Don't trust us — read us", and the part that got root was the part you couldn't read. v0.3.1 embeds plain bash in a quoted heredoc; a test fails if base64 comes back, and scripts/embed.sh --check compares the copy with relay/.
The fix went out as v0.3.1 and v0.3.0 still hangs next to it: the one-liners point at version tags, and a repo rule says v* tags can't be moved, updated or deleted, so the file you read on GitHub at a tag is the file that runs.
For the "help me right now" case, certificates and a CA are overkill. v0.4 added --public: anyone holding the code can take the session. That makes the code the password, so it's long, 13 base32 characters, about 65 bits, in groups of four; a word list would have needed a 13 KB dictionary of about 1,700 words, 40% more file to read.
Anyone who can reach the relay sees its list of sessions, so if the first bind claimed a session, a spammer could block it. The relay keeps every bind attempt, up to 64 per session, and the client picks the one whose HMAC(code, key) matches. The relay stores only the hash of the code and the HMAC, so whoever runs it can take the service down or look at metadata but can't quietly read a session. The mode is switched on separately on each side; CA stays the default.
What the person being helped sees
Early versions put the helper's commands in a second window, useless in a single-window web terminal. Since v0.5 they appear in the same window as a live log: $ command, its output, and a pinned status line with a Ctrl-C reminder.
The output is under the helper's control, so markers in the stream could be forged. The bash and zsh hooks emit invisible OSC markers signed with a per-session nonce, and the renderer trusts only those and strips control sequences. vim and top are shown as they are, because a filter that hides full-screen programs could hide real output too.
The session is recorded on the client's disk, in ~/.floo-last-session/recording/: a readable session.log and session.raw, the unfiltered byte stream. On exit floo shows whether SSH keys, enabled services or cron changed.
The helper runs as the same user and can read the nonce or remove the hooks. The live log shows what a helper who isn't hiding is doing, and won't save you from one who is.
After several rounds of reviewing the console, I connected to a live server and saw an empty window. tail -F was following recording/*.log, the glob expanded at startup before any files existed, and tail patiently waited for a file literally named *.log. Two more bugs of the same kind followed in the next two versions, and v0.6 went entirely into fixing and simplifying.
Orphans came next. If floo died badly (SIGKILL, a closed tmux), its sshd and tunnel lived on in their process group. v0.7.1 brought floo --stop to clean them up from another terminal, and v0.7.3 a watchdog that shuts sshd down when the controlling process disappears. A reboot is still the most reliable insurance.
Over the summer I wondered whether it was decent to show people about 1200 lines of bash in the client and a thousand in the operator's file. Go would look more respectable, but then the person at the machine would no longer run one file they can read top to bottom. Bash stayed, with shellcheck in CI and docs/READING.md, a map that gets you through both scripts in about twenty minutes.
September, and a morning with a routing table
I wrote and reviewed both tools with AI agents. In September two independent scanners went over the same bytes of both repos: VVAH (Visa Vulnerability Agentic Harness), an open agentic scanner from Visa, and OpenAI's codex-security (my comparison of the two on X). On floo they agreed on no line of code but on four mechanisms. Neither found a remote hole in the client, and I reproduced every finding by hand before fixing it.
One finding went back to the Cockpit story. --pin proved the relay genuine, but the CA came from that same relay, so the owner of someone else's relay could slip in their own. The pin now has a second half:
floo --relay relay.example.com --pin RELAY16:CA16
The first 16 hex characters check the relay's host key, the second 16 the operator's CA. Without the second half floo works as before and prints one line saying it trusts the relay's owner with the CA.
That and a few other fixes went into v0.7.3, tagged on the day I was finishing this article, and the first update of my public relay broke it. The installer writes the sshd config template through an unquoted heredoc so the port gets substituted, and a fresh comment in it had the word route in backticks. On a machine with net-tools, bash ran route and wrote the routing table into sshd_config. Next to it, systemctl enable --now left the already running sshd on its old config, while the new dispatcher required ExposeAuthInfo and refused every quick session.
v0.7.4 stops the install on a broken config and sends a live relay a SIGHUP so it rereads its config without dropping sessions. My own rule won't let me move v0.7.3, so it sits in the repo with the word route in backticks.
Homenum Revelio
In the books this spell reveals people nearby. revelio shows who kept a way into a machine that came to you: a bought or inherited VPS, a contractor's server, a box somebody set up for you.
It inventories persistent access paths, from SSH keys and sudoers to outbound tunnels, cron, PAM and setuid binaries, is especially loud about curl | sh in cron, and says for each entry whether a distro package or a human put it there.
It never prints the word SAFE; recognising your own entries is your job. The exit code isn't a verdict either: 0 means nothing to look at, 10 means entries worth a look, 1 means the tool broke.
curl -O https://raw.githubusercontent.com/interkelstar/homenum-revelio/v0.1.0/revelio
less revelio
bash revelio
Asking you to trust a backdoor finder blindly would be silly, so you can check it has no networking with one grep:
grep -nE 'curl|wget|/dev/tcp|/dev/udp|nc |ncat|telnet' revelio
It finds two lines with the regexes the script uses to spot curl | sh and reverse shells in cron and profiles, plus a comment above them, and no commands. The README used to claim the grep finds nothing; I fixed that with the v0.1.0 tag.
It writes only the --save file, if asked. It doesn't need root; without it, it lists what it couldn't read. Control bytes in key comments or file names are replaced with ?, so they can't draw anything in your terminal.
My favourite trick is REVELIO_ROOT=/mnt revelio, which inspects a mounted disk image, so you can check a server without running anything on it.
The September scanners read it differently: VVAH found bugs in the script, such as unsafe handling of REVELIO_ROOT and a --save write through a symlink. codex-security looked for what it would miss (renamed tunnel binaries, systemd timers, @include in sudoers, accounts outside /home), which for this kind of tool is worse than any bug.
There are no security scores or promises to find a rootkit; a userspace script can't prove that. If you need more than floo's own three-place diff on exit, the helper can run revelio inside the session.
Where floo is the wrong tool
I run the public relay relay.floo.run as a courtesy, with no guarantees: quick sessions last up to 30 minutes and concurrent ones are limited. For anything ongoing or sensitive, set up your own relay with floo-powder init (Fedora, Debian, Ubuntu, Arch, Alpine; --uninstall removes everything it put there) and use a CA.
floo gives a shell as the user who started it, not root; sudo asks for a password as usual. It needs the sshd binary installed (not running), python3 for the live console and script from util-linux for the pty recording, and works more plainly without the last two.
The recording is written as the helper's user, so the helper can forge it. floo promises that changes are visible, not that they can't be made. Access ends with the process, and certainly with a reboot.
On curl | bash, since that comment will be first: the one-liner points at an immutable tag, and the operator's one can also pin the full commit hash. Or download, read and run it by hand:
curl -fsSL https://floo.run -o floo
less floo
bash floo --public
There's no second-channel sha256 signature for releases yet; the threat model records that as an accepted residual risk.
If you have dozens of your own machines, RBAC and compliance, use Teleport or Boundary. I think floo fits best on machines that aren't yours, for one evening.
Links
Floo: github.com/interkelstar/floo, with docs/DESIGN.md, docs/THREAT-MODEL.md and docs/READING.md
Short entry point: floo.run
Homenum Revelio: github.com/interkelstar/homenum-revelio
VVAH: github.com/visa/visa-vulnerability-agentic-harness
Both are MIT. If a friend knocks on your door one night with a laptop and the words "it did it by itself", you can help and not leave a spare key in their house. Ctrl-C, and mischief managed.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.