Security model
A web terminal is a remote shell, so cloudflare-tmux assumes any single layer can fail. A stranger would have to get through every layer below to reach a shell.
Layers#
1. Nothing listens on the internet#
The app binds to 127.0.0.1:7681. cloudflared makes outbound connections to Cloudflare. There's no
inbound port to scan or brute-force, and your server's IP never appears in DNS.
2. Cloudflare Access in front#
Every request to the hostname passes through Cloudflare Access first.
- The only login method is Sign in with Cloudflare, restricted to members of your Cloudflare account. There are no one-time email codes and no other providers.
- An Allow policy lists exactly the emails in
IDENTITIES. Nobody else gets past Cloudflare's edge. - The session cookie is
HttpOnlyandSameSite=Lax, and expires afterACCESS_SESSION, by default 24 hours.
3. The app verifies the login again#
Access attaches a signed token, a JWT, to each request, in the Cf-Access-Jwt-Assertion header and the
CF_Authorization cookie. The app doesn't trust Access blindly. On every HTTP request and every WebSocket
upgrade it:
- verifies the signature against your team's public keys,
- checks the token was issued by your team domain, for this application's AUD tag,
- checks the email is in its own
IDENTITIESlist.
So a misconfigured Access policy, a different Access application on the same team, or a request that reaches port 7681 locally still can't open a shell. The app refuses to start without this configuration.
4. Same-origin checks#
WebSocket upgrades and every non-GET request must carry an Origin header that matches the host. A missing
or malformed Origin is refused. This blocks cross-site WebSocket hijacking and CSRF riding on your Access
cookie.
5. Optional two-step code#
With TOTP=required, passing Access isn't enough. The app also asks for a 6-digit authenticator code before
it lists sessions or opens a terminal. Authenticators are enrolled only on the server, codes can't be reused,
and guessing is rate-limited. See Two-step code.
6. Fixed email → user mapping#
- No Linux password is ever typed into a web page.
- The mapping lives on the server. Users can't choose an account.
rootand UID 0 can never be mapped. Accounts with anologinorfalseshell are refused.- In the single-user install, emails mapped to any user other than the one running the service are refused.
7. Privilege separation in the multi-user install#
- tmux runs through
setpriv, so each person's processes have their own uid, gid and groups, and a clean environment. - The file panel works the same way. Listing, downloading and uploading run in a helper
started through
setprivas the person, never as root, so the kernel decides what they may read and write. A person can reach exactly the files their shell can reach, and nothing more. - The root service runs only root-owned code from
/opt/cloudflare-tmux. No user can change what root executes. - Its config in
/etc/cloudflare-tmux/envis readable only by root.
8. Hardened HTTP#
Strict headers: Content-Security-Policy: default-src 'self', X-Frame-Options: DENY,
Referrer-Policy: no-referrer, Cache-Control: no-store. Small request-body limits. Session names are
validated against [A-Za-z0-9_-]{1,32} and always passed to tmux as separate arguments, never through a shell.
Downloads are sent as attachments with nosniff, so a downloaded HTML file never runs on the app's origin.
Uploads are capped by MAX_UPLOAD_MB, written to a temporary file and kept only when every byte arrived, and
never replace an existing file unless the person confirms.
Hardening checklist#
Do this once after the install, and again whenever you add someone. Most steps need no API token.
1. Cloudflare accounts#
Every listed Cloudflare account is a key to a shell. Protect those accounts first.
- Turn on two-factor authentication on your own Cloudflare account, under My Profile → Authentication. Prefer a security key or an authenticator app.
- Require 2FA for all members in your account's member settings.
- Give everyone else Minimal Account Access only. An admin could change the Access policy or the tunnel and let themselves in.
- Check Manage Account → Members and remove anyone who shouldn't be there.
2. The API token#
- Scope it to one account and one zone, with the four permissions and nothing more.
- Give it a short expiry, and optionally a client IP filter for your server.
- Delete it once setup works. Nothing running uses it.
- Don't keep a copy anywhere. The scripts ask for it when they need it.
3. The Access application#
These change Cloudflare, so they need a token or the dashboard.
- Shorten the login lifetime. Set
ACCESS_SESSION=8hor less in~/.config/cloudflare-tmux/env, then re-runscripts/cloudflare.shwith a token. Change it in the config, not the dashboard:cloudflare.shoverwrites the dashboard value on every run. - Make sure the Cloudflare login method is restricted to account members.
cloudflare.shwarns if it isn't. - Optional: add device posture or country rules to the Access policy in the dashboard.
cloudflare.shreplaces the policy on every run, so re-add them afterwards, or stop re-running it.
4. The app#
- Turn on the two-step code with
TOTP=required. It's checked on your server, so it still protects you if a Cloudflare account or the Access policy is compromised. - Keep
TOTP_SESSION_HOURSshort, for example8. - Map only people you'd give an SSH login to. Never a shared or admin account.
- Never set
AUTH_DISABLED=1or aHOSTother than127.0.0.1on a real install.
5. The server#
Check that only loopback is listening:
ss -ltnp | grep 7681 # expect 127.0.0.1:7681, never 0.0.0.0 or *Check the secret files are private:
ls -ld ~/.config/cloudflare-tmux ~/.cloudflared # drwx------ ls -l ~/.config/cloudflare-tmux/env ~/.cloudflared/*.token # -rw-------If other people have accounts on the machine, make your home directory private too:
chmod 700 ~.Check the hostname is protected, from any machine:
curl -sI https://term.example.com | grep -i '^location' # expect ...cloudflareaccess.com/...Keep the OS, Node.js and tmux patched, for example with
unattended-upgradeson Debian and Ubuntu.Update
cloudflaredregularly withscripts/install-cloudflared.sh --restart. It doesn't update itself. See Maintenance.
6. Watch it#
Zero Trust → Logs → Access shows every login to the app, with email and IP.
The app logs every attach, detach and refusal:
cloudflare-tmux logs | grep -E 'attach|rejected'auth rejectedand2fa rejectedlines from people you don't expect are worth a look.cloudflare-tmux peopleshows each person's last login.
If something goes wrong#
If you think a person's Cloudflare account, a device or the API token is compromised, do these in order. The first three need no API token, so you can act right away.
- Lock them out of the app. Run
cloudflare-tmux remove their@email. It removes them fromIDENTITIESand restarts the app. The app refuses them immediately, even with a valid Cloudflare login, and the restart drops any open terminal. - End what they left running. Their tmux sessions survive the lockout. As that Linux user, check
tmux lsand runtmux kill-server. Review~/.ssh/authorized_keys, crontabs and shell startup files for anything they added. - Revoke at Cloudflare. Revoke their Access sessions under Zero Trust → Users → the person → Revoke session. Remove or downgrade them under Manage Account → Members. Delete any API token that may have leaked.
- Remove them from the Access policy. Run
scripts/cloudflare.shwith a new short-lived API token, then delete the token. - If the tunnel token leaked. Delete the tunnel under Zero Trust → Networks → Tunnels, then re-run
scripts/cloudflare.shandscripts/setup.shwith an API token. That creates a new tunnel with a new token. With the old token, someone could run their own connector for your hostname, though Access would still sit in front of it. - If a phone with the two-step code was lost. Run
scripts/totp.js remove their@email, thenscripts/totp.js addagain on the new device.
What it doesn't protect against#
- A compromised Cloudflare account that's on the list. That person gets that user's shell. Use 2FA and the two-step code.
- Other users on the same machine. They can reach
127.0.0.1:7681, though they still need a valid login for a listed email. Keep your home directory and the secret files private. - A compromised Cloudflare account with admin rights on your account. It controls the policy and the tunnel. Give other people only Minimal Account Access.
- Anything the mapped Linux user could already do. There's no sandbox beyond normal Unix permissions.
Reporting vulnerabilities#
Please report security issues privately. See SECURITY.md.