Ramblings of an aging IT geek
← Ramblings of an aging IT geek
linux

Two-Factor Codes You Can `cat`

rosec turns TOTP seeds into live files (cat one for the current six-digit code) reusing the same FUSE trick as the SSH agent, with a QR scanner to import seeds.

Listen to this post
A live TOTP code read from a file, with a QR seed

Back when rosec grew an SSH agent and a FUSE filesystem for keys, the pattern underneath it was the interesting bit: take a secret the daemon already holds and expose it as a file, so any tool that can read a file can use it. This is that same trick, pointed at two-factor codes.

A TOTP seed is just another secret. So if an item in any provider carries one, rosec now surfaces it as a live file under $XDG_RUNTIME_DIR/rosec/totp/, and cat-ing the file gives you the current six-digit code, regenerated every window:

cat "$XDG_RUNTIME_DIR/rosec/totp/GitHub"
# 481922

No app to open, no phone to reach for, no copy-paste dance: the code is a file, which means it's also a shell pipeline, a wofi entry, a keybind. Seeds go in either by storing them on an item directly or with the built-in QR scanner, which reads the setup QR a site shows you and stores the seed in the provider of your choice. Like the SSH filesystem it's behind a toggle (totp_fuse), off until you ask for it, and the prompt helper auto-clears the clipboard after a paste so a code doesn't linger.

Is that safe?

A live 2FA code sitting in a file sounds alarming, so it's worth spelling out, because there's a real trade-off hiding in it.

The mechanics first. The mount lives under $XDG_RUNTIME_DIR/rosec/totp/, which on Linux is /run/user/<your-uid>/: a per-user tmpfs, mode 0700, owned by you, that never touches the disk. rosec insists on XDG_RUNTIME_DIR; if it isn't set the filesystem simply doesn't mount, rather than falling back somewhere world-readable. The mount itself is read-only, nosuid, nodev, noexec, with an owner-only session ACL, so only your uid can traverse it: not other users, not root by default. The code files report mode 0400, owner-read and nothing else.

There's also nothing to steal at rest. It's a virtual filesystem, so there is no GitHub file on a disk anywhere. The six digits are generated fresh on each read, handed back, and the buffer holding them zeroed straight after; nothing is written to disk, and the code is never logged. The seed itself stays in the daemon's memory behind its provider, not in the mount.

So the code is exactly as exposed as your SSH agent socket (same directory, same rules), your keyring, or the passwords your browser has saved: anything running as you can read it, and nothing else can.

That last clause is the trade-off, stated plainly. "Readable only by your user" is the same wall every other secret on your machine already stands behind, and it stops other users and remote attackers cold. It does not stop code running as you: a malicious dependency, a dodgy package build, a hijacked shell can cat the file as easily as you can. For most secrets that's the accepted bargain. For TOTP it deserves a second's thought, because the whole point of a TOTP is to be a second factor on a separate device. Serve it from the same machine that holds the password and you've folded both factors into one trust boundary: own the machine as that user, and you have the password and the current code together.

For me, and I'd guess most people, that's still the right trade, because the threat 2FA actually earns its keep against is the remote one: the phished password, the breach, the credential-stuffing, and none of those can reach the file. But if your threat model genuinely includes "my own machine might be running something hostile", a code on your phone buys you something a code in a file cannot, and that's exactly why the whole thing stays behind the totp_fuse toggle, off until you decide it's worth it.

It's a small feature that I use constantly, and it's the clearest example of why the provider model was worth the effort: unify the secrets once, and "current 2FA code" becomes cat of a path. Repo and docs on GitHub.