Security & privacy
Transport and transfer notes updated October 11, 2026 · current service release: 2.9.133
Your phone controls an open-source service on your own machine over your LAN, without a cloud relay. Pairing grants a per-box credential. Supported API and WebSocket connections use pinned TLS; legacy HTTP and native image/file-transfer paths remain, so not all traffic is encrypted. Updates, purchases and optional integrations use internet services. The source and the limitations below describe these boundaries.
Architecture
Two pieces: the phone app and couchsided.py, a single-file Python service (standard library only, no third-party imports) running on your box. The agent listens on two TCP ports:
| Port | What |
|---|---|
8787 | Plaintext HTTP + WebSocket. Always on (backwards compatibility with older apps). |
8788 | HTTPS + WSS with a self-signed certificate. On by default since agent 2.9.88; opt out with couchside tls off. |
Local box control does not require a cloud relay or a Couchside account. Keep the service on your trusted local network; do not expose its ports to the public internet. Optional online services are described below.
Transport encryption
The agent mints its own RSA-2048 certificate on first start (via the box's openssl) and serves HTTPS beside plaintext. Because the certificate is self-signed, trust doesn't come from a CA. It comes from pinning at pairing time:
- The pairing QR rendered on the box's own screen carries the certificate's SHA-256 fingerprint. Reading it requires being physically in front of the box; that is the trust anchor.
- When pairing establishes a TLS pin, supported API and WebSocket connections compare the live certificate’s RSA key against it and fail closed on mismatch. This protection applies to the pinned connection; it does not encrypt the separate native HTTP transfer paths.
- The key survives IP changes (re-signs reuse it), so the pin doesn't break when your router hands out a new lease.
Initial trust still matters. Discovery and legacy TLS upgrade information can arrive over plaintext, so an active attacker on the LAN during initial pairing or upgrade remains a risk. Pair only with the box you can verify on screen and check that the app shows an encrypted connection. A saved TLS pin protects subsequent pinned connections; it does not make initial pairing immune to interference.
Native image loading and phone-to-box file uploads still use HTTP. On a pinned box, short-lived tickets are minted over TLS; image tickets authorize image reads and upload tickets authorize one upload. Tickets limit authority but do not encrypt image or file bytes. Treat those payloads as visible to someone able to observe that network traffic.
Pairing and auth
Control requests normally require Authorization: Bearer <token>: a 48-hex-character (24-byte) random token minted on the box at install, stored at /etc/couchside/token (mode 600), compared in constant time. Native uploads can instead use a scoped, single-use ticket; discovery and PIN pairing have separate checks. A phone gets its credential through QR or PIN pairing:
- QR. The box renders a pairing link as a QR code. The token rides the URL fragment (
#token=…), which browsers never send to a server: couchside.tv never sees it, and the relaunch page that opens the app runs entirely client-side. The page serving the QR on the box (/pair) answers only to loopback and validates theHostheader, so neither a LAN client nor a DNS-rebinding page in the box's own browser can read it remotely. - PIN. The app finds the box on the LAN and asks it to display a 6-digit PIN on its own screen. The PIN lives 120 seconds, allows 5 wrong attempts before the session burns, is rate-limited (new sessions every 3 s at most, on-screen pop at most every 5 s), and is exchanged for the token over one request. Worst-case brute force is roughly 1.7 guesses per second against a million combinations, which is about a week of continuously strobing a full-screen pairing page onto your TV. Loud, slow, and visible; but there is no per-IP lockout beyond this, which is why the LAN itself is the trust boundary (see the last section).
On the phone, the token is stored in the platform keychain (iOS Keychain / Android Keystore via expo-secure-store), per box.
Revocation
The agent has one token per box, not per device. Every paired phone holds the same secret. That makes revocation blunt by design:
couchside new-tokenmints a fresh token and restarts the agent. Every previously paired phone stops working immediately, and each one re-pairs by QR or PIN. There is no way to cut off one phone and keep the others; if that matters to you (shared house, revoked roommate), rotate and re-pair. Treat the token like an SSH key: anyone holding it controls the box (next section).
If couchside new-token or couchside tls reports an unknown command, your box has an older copy of the management CLI (both were added 2026-08-15). Plain couchside update will not fetch it when your agent is already current (that command compares agent versions and exits early), so reinstall explicitly:
couchside update --forceor, equivalently, curl -fsSL https://couchside.tv/install.sh | bash. The installer is idempotent: it keeps your existing token, config, and pairings.
What the token grants
The agent implements predefined operations with validation of identifiers and arguments. Privileged tasks use specific sudoers permissions or argument-validating helpers. Some higher-risk capabilities have separate box-side settings: couchside allow-updates, couchside allow-system-updates, and couchside allow-launchers. These controls limit the direct API operations; they do not remove the authority granted by remote keyboard and mouse access. The source describes each operation and its checks.
But be clear about the baseline: the gamepad/remote path includes a virtual keyboard and mouse. A token holder can type into whatever session your box is showing; that's the product. The allowlist bounds what the HTTP API can execute directly; it is not a sandbox around someone who holds your token. This is why revocation and the pairing physical-presence requirements exist.
Discovery: what the box reveals to anyone
So the app can find boxes, the agent answers two unauthenticated probes from anything on your LAN:
GET /api/pingreturns{"couchside": true}plus agent version, the box's short hostname, the IP you reached it on, and (when TLS is up) the HTTPS port and certificate fingerprints.- A UDP broadcast responder on the same port number, with the same fields.
Discovery does not return the control token, but it tells LAN scanners that a Couchside agent, version and hostname are present. Adding a box by IP remains possible when discovery is firewalled off. Control routes, scoped-ticket routes and pairing each have their own authorization policy. The agent’s access log redacts query strings.
What can leave your LAN
Local box control does not need a cloud relay. That does not mean the app or website never uses the internet: purchases, updates, game data and optional integrations contact online services. Website checkout and opt-in email news also have their own data flows; see the privacy policy.
Examples of outbound requests include:
| From | To | When / what |
|---|---|---|
| agent | github.com | Update check (compares versions, cached 6 h, only when the app asks) and downloading signed releases you install. The request's User-Agent carries the agent version. |
| app | store.steampowered.com | Game names, artwork, review scores for your library view. |
| app | itunes.apple.com / the app stores | App-update version check; purchases go through Apple/Google. |
Installed-game artwork can be served from your box’s Steam cache. Other game-data and optional integration requests can reach third-party services. This table is an overview, not a complete network allowlist.
What this does NOT protect against
- A compromised device already on your LAN, present at pairing time. The whole trust model bottoms out at "you can see the box's screen and your LAN is yours." An attacker with a LAN foothold can watch discovery, and if they're active during a TOFU auto-upgrade (or can strobe pairing PINs onto your TV for a week without you noticing), the model has failed. Couchside assumes a home LAN, not a hostile one.
- A lost or stolen phone, until you rotate. The token in its keychain keeps working until
couchside new-token. Nothing expires on its own. - Yourself, port-forwarding the agent. The agent binds all interfaces and its auth is one static bearer token with no lockout on the HTTP gate. Exposing 8787/8788 to the internet is categorically outside the design. Don't.
- Plaintext sniffing of un-upgraded pairs. A box on an agent older than 2.9.88, or an app that never picked up the TLS upgrade, still talks cleartext HTTP on 8787, including the bearer header. On your own WPA2/3 Wi-Fi that means other devices that already know your Wi-Fi password. Update both sides; check with
couchside tls status. - Uploaded file contents on a TLS box. The streamed uploader rides plaintext with a single-use ticket (the token stays off the wire, the bytes don't). Don't upload secrets you wouldn't broadcast on your LAN.
- Physical access to the box. The token is a file; anyone at the keyboard owns the machine anyway.
Verify it yourself
Every claim above is testable from any machine on your LAN:
curl http://YOUR-BOX.local:8787/api/pingThat is the discovery surface, exactly as described. Then confirm a credential-less client is refused:
curl -i http://YOUR-BOX.local:8787/api/statusThat returns 401. On the box, couchside tls status prints the live HTTPS port and certificate fingerprints; compare against what the handshake presents:
openssl s_client -connect YOUR-BOX.local:8788 </dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -nooutAnd the source of everything on this page: the agent (auth gate, pairing, discovery, TLS), the installer (token mint, sudoers grants, the CLI), and the app (pinned transport, keychain storage). Releases are signed with an offline Ed25519 key, and the installer this site serves is build-verified against those signatures. Found something? support@couchside.tv.