Only the latest release gets fixes.
Report privately through GitHub: this repository's Security tab → Report a vulnerability. Please don't open a public issue for it.
Patchbay has no authentication anywhere. It is built for one person's own machine or trusted network, and the network is the whole perimeter. This is what that means in practice.
| Port | Process | What it is | Bound to |
|---|---|---|---|
| 8765 | pipeline | the WebSocket: audio in and out, transcripts, and every control message | --ws_host. SETUP.md's first run passes 127.0.0.1; the systemd template passes 0.0.0.0; if you omit the flag, the patched default is 127.0.0.1 — 0.0.0.0 only when you pass it explicitly |
| 8443 | pipeline | optional TLS copy of the same WebSocket, only when VOICE_WS_CERTFILE and VOICE_WS_KEYFILE are both set (VOICE_WSS_PORT moves it) |
same host as 8765 |
| 8770 | webclient/serve.py |
HTTP (or HTTPS with --certfile/--keyfile): the cockpit's files, /models + /v1/models (reports whether 8765 is listening), and /ha/states, /ha/stream, /ha/intent only when HA_URL and HA_TOKEN are set |
--host, default 127.0.0.1; the systemd template passes 0.0.0.0 |
Anything that can reach 8765 is a full participant in the one shared session:
it hears the live audio and transcripts, and it can rewrite the persona,
switch the brain and model, reset the chat, upload or delete cloned voices,
toggle the wake word, re-import drop-in tools, and answer an agent-lane
approval request (permission_respond).
serve.py serves every file under webclient/. Don't keep private keys or
anything else private in that directory (including webclient/certs/, which
is gitignored but not hidden from the server).
VOICE_WS_ORIGINS (and the same-host rule used when it is unset) filters
which browser pages may open the WebSocket. It stops a web page on
another site from connecting through your browser. A non-browser client
sends no Origin at all; the default admits it, as does any allowlist that
includes -. It is not a login.
The same-host rule compares the Origin's hostname with the Host header's
hostname — which alone could not stop DNS rebinding (a hostile hostname that
resolves to your machine produces a page whose Origin and Host match).
That case is covered by a separate Host-header allowlist, enforced on
both the pipeline's WebSocket and serve.py, in every configuration
including VOICE_WS_ORIGINS set: a rebinding request always carries the
attacker's hostname, so any Host that is not an IP literal, localhost
(or *.localhost), one of this machine's own names — hostname, FQDN,
<hostname>.local, and its Tailscale MagicDNS name when installed — a
hostname named in VOICE_WS_ORIGINS, or an entry of VOICE_ALLOWED_HOSTS,
is answered with 403. Its remaining limit: a hostname you allow yourself
(VOICE_ALLOWED_HOSTS, or one named in VOICE_WS_ORIGINS) is trusted as
fully as a same-machine page can be, and VOICE_ALLOWED_HOSTS=* disables
the check altogether.
With HA_URL and HA_TOKEN set, serve.py forwards write requests from
/ha/intent to Home Assistant, for a fixed list of domains and services
that includes lock.unlock. The token stays inside serve.py. A write must
be a Content-Type: application/json POST (else 415), and if it carries an
Origin, that Origin's hostname must match the Host header (else 403).
That blocks a cross-site form or text/plain POST from a page you visit,
and the Host allowlist (above) now stops a rebinding page from reaching this
route at all. It does not stop anything that can reach port 8770 directly —
curl sends no Origin. With Home Assistant
configured, keep serve.py on 127.0.0.1, or put real authentication in
front of it.
- Drop-in tools are code.
VOICE_TOOLS_DIRnames a directory whose.pyfiles the pipeline imports and runs. Treat that directory like your shell profile. - Camera and screen frames are written to disk. When a client shares its
camera or screen, the latest frame is written to
/dev/shm/voice_camera_frame.jpg//dev/shm/voice_screen_frame.jpg(VOICE_CAMERA_FRAME,VOICE_SCREEN_FRAME) with the process's default file permissions — world-readable under the usual022umask — and stays there until it is overwritten or the machine reboots. - Phone context leaves the machine. If a browser turns on Phone
context, it sends its location, timezone and battery state to the voice
server. The server sends the coordinates to OpenStreetMap's Nominatim
(
https://nominatim.openstreetmap.org/reverse) for a place name, and to Open-Meteo (https://api.open-meteo.com/v1/forecast) whenget_weatherruns without a place; the resolved place name and local time go into the prompt of whichever brain is active — including a hosted one.VOICE_PHONE_CONTEXT=offdisables it server-side. - Voice cloning needs consent. pocket-tts's licence prohibits voice impersonation or cloning without explicit and lawful consent. Clone only voices you have the right to.
- API keys belong in the mode-
0600filesbrains.jsonpoints at (api_key_file), and secret env vars in a systemdEnvironmentFile=, not inEnvironment=lines (unit files are readable by any local user viasystemctl cat).brains.jsonis gitignored; keep it that way.
Bind 0.0.0.0 (or a LAN/VPN address) only when you mean every device that
can reach that address to have everything above. README's Who can connect
covers the WebSocket side; patches/README.md Remote access / HTTPS
covers TLS. If you need real access control, put a reverse proxy with
authentication in front of both ports and keep the processes themselves on
127.0.0.1.