Report privately via GitHub's Report a vulnerability (the repo's Security tab → Report a vulnerability). The report stays private until a fix ships. For non-sensitive issues, open a GitHub Issue labeled security.
Include: steps to reproduce, potential impact, and any suggested fix.
In scope:
- Vulnerabilities in the application code (
app/) - Dependency vulnerabilities that affect Topic Watch
- Docker / docker-compose configuration issues
- CSRF or injection issues in the web UI
Out of scope (report to the relevant project instead):
- Your LLM provider (OpenAI, Anthropic, etc.)
- Apprise notification services (Apprise project)
- Your reverse proxy or hosting configuration
Topic Watch is a single-user, self-hosted tool. Built-in auth would mean managing users, passwords, and sessions — complexity that doesn't fit one user. Safe as-is on localhost.
For remote deployment, put it behind a reverse proxy with your auth layer (Authelia, Caddy basicauth, Nginx basic auth) — see Reverse Proxy Auth Examples below.
When deploying Topic Watch on a public network:
-
TLS is required. Terminate TLS at your reverse proxy (Caddy, Nginx, Traefik) before forwarding to Topic Watch. Without TLS, CSRF tokens and session cookies are transmitted in plaintext.
-
Enable secure cookies. Set
TOPIC_WATCH_SECURE_COOKIES=true(orsecure_cookies: trueindata/config.yml) so cookies are only sent over HTTPS connections. -
Declare your hostname. Topic Watch answers only to
localhost,*.localhost,*.localand IP addresses; anything else gets a 400. That stops a hostile site from re-pointing its own domain at this machine and driving the console from your browser. When a reverse proxy forwards a hostname, list it:TOPIC_WATCH_ALLOWED_HOSTS=topic-watch.example.com(comma-separated for several;*.example.comcovers the subdomains and the apex;*turns the check off). -
Restrict network access. Both compose files publish the port on
127.0.0.1by default, so a stock install is reachable only from the host. To reach it from other devices, setTOPIC_WATCH_BIND_ADDR=0.0.0.0in.env— and put an authenticating reverse proxy in front before you do. Do not expose port 8000 directly to the internet.Note that this is not something a host firewall will save you from: Docker inserts its own iptables rules ahead of the ones
ufwandfirewalldmanage, so a published port stays reachable even when the firewall is configured to deny incoming traffic. The bind address is the control that actually applies. -
Protect
data/config.yml. This file contains your LLM API key. Ensure it is not world-readable (chmod 600 data/config.yml). Saving from the Settings page applies that itself: the file is written owner-only, and a permissive mode is tightened rather than carried forward. -
Keep dependencies updated.
requirements.txtis a hash-pinned lockfile (exact==versions plus--hashentries), sopip install --upgrade -r requirements.txtcannot raise versions — it is a no-op for upgrades. Updates land through the configured Dependabot PRs; to bump versions locally, runmake lock-tools(installs the pinnedpip-compiletoolchain — use a throwaway venv, it pins pip to 25.1.1), thenmake lock-upgradeand reinstall. Plainmake lockreproduces the current pins and will not raise versions. -
Use the Docker image. It runs as a non-root user with resource limits.
topic-watch.example.com {
basicauth {
admin $2a$14$YOUR_HASHED_PASSWORD
}
reverse_proxy localhost:8000
}
Generate hash: caddy hash-password
server {
listen 443 ssl;
server_name topic-watch.example.com;
auth_basic "Topic Watch";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Create credentials: htpasswd -c /etc/nginx/.htpasswd admin
The one-line installers (scripts/install.sh, scripts/install.ps1) are meant to be run with curl | bash / irm | iex. That convenience carries the usual trade-off: your shell executes whatever the URL returns, and by default the scripts fetch the docker-compose file (which selects the container image) from the same source. Both are pulled from the mutable main branch with no commit pin, tag, signature, or checksum, so a repository/branch compromise or a man-in-the-middle proxy could run arbitrary code as the invoking user.
To reduce that trust before running either installer:
-
Read it first. Download the script and review it, or run it from a local checkout, instead of piping straight to a shell.
-
Pin a ref. Set
TOPIC_WATCH_REFto a release tag or commit SHA and fetch the installer from that same ref. This pins both the installer and thedocker-composefile it downloads:# Linux / macOS TOPIC_WATCH_REF=v1.1.2 curl -fsSL \ https://raw.githubusercontent.com/0xzerolight/topic_watch/v1.1.2/scripts/install.sh | bash
# Windows (PowerShell) $env:TOPIC_WATCH_REF="v1.1.2" irm https://raw.githubusercontent.com/0xzerolight/topic_watch/v1.1.2/scripts/install.ps1 | iex
The installers can set up boot/login autostart (a systemd user service + loginctl enable-linger on Linux; a Startup-folder shortcut on Windows). This is opt-in: the installer prompts when run interactively and skips autostart in a non-interactive piped run unless you pass TOPIC_WATCH_AUTOSTART=yes. The closing summary echoes the exact uninstall commands.
To remove autostart later:
# Linux
systemctl --user disable --now topic-watch
rm -f ~/.config/systemd/user/topic-watch.service
loginctl disable-linger "$USER"# Windows — delete the Startup-folder shortcut
Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\Topic Watch.lnk"- SSRF / DNS rebinding (TOCTOU). Feed and webhook URLs are validated against private/reserved addresses, including a DNS-resolution layer that now fails closed (an unresolvable host is blocked). Redirects are re-validated per hop. However, validation resolves DNS at check time while httpx re-resolves at connect time, leaving a narrow rebinding window between validation and fetch. Eliminating it would require a pinned-IP connect transport that risks breaking HTTPS feed fetching (SNI / cert verification), so it is an accepted limitation for this single-user self-hosted tool.
The /api/v1/ JSON API endpoints are unauthenticated, the same as the web UI. GET endpoints provide read access to all topic data including knowledge states. The single mutation endpoint (POST /api/v1/topics/{id}/check) is protected by CSRF.
If you expose Topic Watch to a network, apply the same reverse proxy authentication to API endpoints as you do to the web UI.