Vitals is a single-user, self-hosted application. Only the latest commit on master is supported.
| Version | Supported |
|---|---|
latest (master) |
β |
| older commits | β |
Please do not open a public GitHub issue for security vulnerabilities.
If you discover a security issue, email the maintainer privately:
π§ ilodezzis@gmail.com β subject line: [Vitals] Security Vulnerability
Include:
- A clear description of the issue
- Steps to reproduce
- Impact assessment (what data or access could be affected)
- Any suggested fix (optional but appreciated)
You will receive a response within 72 hours. If the issue is confirmed, a fix will be released and you'll be credited in the commit message (unless you prefer to remain anonymous).
Vitals is designed for single-user self-hosted deployment, not as a multi-tenant SaaS. The security model assumes:
- The application runs on your own server behind a VPN or Cloudflare Access
- Only you have access to the dashboard
- The
.envfile with credentials is never committed to the repository
Key security controls already in place:
- Bcrypt password hashing
- Signed session cookies (itsdangerous)
- CSRF protection via Origin header validation
- CSP headers
- Loopback-only port binding in
docker-compose.yml(127.0.0.1:8000) - MCP OAuth 2.0 + PKCE for Claude.ai integration
The proactive layer adds the only endpoint that is reachable without a session:
POST /tg/<path>. It is guarded in layers, and it is off unless you configure
it β with no VITALS_TELEGRAM_WEBHOOK_SECRET the route fails closed with 401,
it is never open.
- The path segment itself is a random secret (
VITALS_TELEGRAM_WEBHOOK_PATH), so the endpoint is not discoverable by crawling. - Telegram's
X-Telegram-Bot-Api-Secret-Tokenheader must matchVITALS_TELEGRAM_WEBHOOK_SECRET. Both comparisons usecompare_digestβ a plain==on a secret leaks its prefix through timing. - A rate limit sits in front of the route (fail-open on Redis, as everywhere else).
- An update that clears all of the above but comes from another chat id gets a
plain
200and is discarded. Not a 403: a distinguishable answer tells a prober they found something, and Telegram would retry a non-200 for hours. - Updates are idempotent by
update_id, so a Telegram retry cannot double-write.
Optionally narrow the path to Telegram's own subnets at your reverse proxy β that is infrastructure, not application code.
Revoking bot access: clear the webhook at Telegram
(curl -X POST "https://api.telegram.org/bot<TOKEN>/deleteWebhook"), then rotate
or blank VITALS_TELEGRAM_BOT_TOKEN and VITALS_TELEGRAM_WEBHOOK_SECRET in
.env and restart. Switching the Signals module off in Settings silences all
outgoing messages immediately, without a deploy β it is the emergency switch, not
a revocation.
MCP access tokens are stateless (signed, not stored), so there is no per-token
revoke. To revoke Claude.ai's access, rotate VITALS_SESSION_SECRET in your
.env and restart the app. That invalidates every signature at once β all MCP
tokens and your browser session cookie β so you will need to log in again and
reconnect the Claude.ai connector.
- The application being accessible on your own local network
- Rate limits being bypassable by the single authorized user
- Log messages containing non-sensitive operational information