Skip to content

Security

gladsonsam edited this page Jun 22, 2026 · 4 revisions

Security

Vantyr is a self-hosted endpoint monitoring and remote-control platform. It ships with deliberately powerful, privacy-sensitive capabilities - screen streaming, window/URL/activity tracking, keystroke-adjacent telemetry, file transfer, remote script execution, and an interactive terminal. Treat every auth, secret, enrollment, telemetry, and command-execution surface as security-critical.

This page documents the security model and a practical hardening checklist for operators. See also OIDC, Configuration, Environment-template, Deployment, and Usage.

The same vulnerability-reporting policy is committed in the repository as SECURITY.md (with a GitHub Security Advisories link).


⚠️ Project status - experimental, no professional review

Vantyr is primarily a personal/experimental project. The codebase has not undergone a professional security audit or penetration test. It is not a hardened, supported, or certified product.

  • Do not deploy it on untrusted networks, or against endpoints you are not authorised to monitor, without your own review.
  • Known, deliberately-documented gaps exist (see Known limitations). Read them before exposing the server to the internet.
  • Reports are welcome, but response and patch timelines are best-effort.

Reporting a vulnerability

Please do not open a public GitHub issue for security vulnerabilities.

Report privately:

  • Preferred: open a private security advisory on GitHub (Security → Advisories → "Report a vulnerability") for the repository.
  • If you can't use advisories: contact the maintainer through a private channel listed on the project's GitHub profile.

Include as much as you can:

  • A clear description of the vulnerability and its impact.
  • Reproduction steps and any proof-of-concept code.
  • Affected component(s): vantyr-agent, vantyr-server, vantyr-dashboard.
  • Environment details (Windows version, browser, server OS, configuration).
  • Whether the issue is remotely exploitable and what authentication is required.
  • Relevant logs - redact secrets and personal data.

Safe harbor

If you act in good faith and follow this policy - no data destruction, no service disruption, no privacy violations beyond what is strictly necessary to demonstrate the issue - we will consider your research authorized for the purpose of reporting.

Sensitive data in reports

Do not include real screen captures, keystrokes, credentials, tokens, or other personal data. If such data is needed to demonstrate the issue, use synthetic/test accounts and redact wherever possible.


Authentication model (dashboard users)

The dashboard is backed by a multi-user, DB-backed identity store (dashboard_users) with per-session cookies. The server is always the source of truth - UI route guards are a convenience only.

Passwords

  • Passwords are hashed with Argon2id before storage; plaintext is never persisted or logged.
  • The release server refuses to boot without an admin password (ADMIN_PASSWORD / UI_PASSWORD); there is no hardcoded or default password, and bootstrap_default_admin never invents one.
  • Anti-lockout protections: the last remaining admin cannot be demoted or deleted, and a user cannot delete their own account out from under themselves.
  • Minimum password length is only 6 characters - weak. Enforce strong passwords operationally, and prefer 2FA and/or OIDC SSO for real deployments.

Sessions and cookies

On successful login the server generates a random session token, stores only its SHA-256 hash in Postgres (dashboard_sessions), and returns the token in a cookie:

Attribute Value Notes
HttpOnly always not readable from JavaScript
Secure when HTTPS detected set when X-Forwarded-Proto: https or COOKIE_SECURE=true
SameSite None when Secure, else Lax None allows cross-site WebSocket upgrades behind a proxy
Path /
Max-Age 86400 (1 day) session also has a server-side expiry

Because the cookie is SameSite=None in HTTPS deployments, CSRF protection rests on the explicit header check below, not on SameSite.

CSRF tokens

  • Every session has a per-session CSRF secret (returned from POST /api/login and GET /api/me).
  • All mutating verbs (POST / PUT / PATCH / DELETE) on protected routes require an X-CSRF-Token header that matches the session value, compared in constant time (subtle).
  • WebSocket upgrades remain GET-only and are not CSRF-checked.

Login rate-limiting and per-username lockout

Brute-force protection runs two independent buckets, and a failure increments both:

  • Per-client-IP - the client IP is resolved through the trusted-proxy allowlist (see Trusted proxies), so a spoofed X-Forwarded-For cannot rotate the key.
  • Per-username - keyed by the normalized (lowercased) username, so rotating the source IP cannot sidestep the limit on a targeted account.

Defaults: up to 10 failures per 15-minute sliding window; exceeding either bucket returns 429 Too Many Requests with a Retry-After header until the window drains. Lockout, rate-limit, and login success/failure events are written to the audit log.

Two-factor authentication (TOTP + recovery codes)

2FA is opt-in, per user:

  • A user starts enrollment, scans the returned otpauth:// URI (or secret) into an authenticator app, then confirms a code to enable TOTP.
  • On enable, the server issues 10 single-use recovery codes, shown exactly once. Recovery codes are stored Argon2-hashed and consumed atomically.
  • After 2FA is enabled, login requires the password and a valid 6-digit TOTP code (or a recovery code). The API returns totp_required: true when a code is needed; a wrong code records a login failure against the lockout buckets.
  • Disabling 2FA also requires a valid current TOTP or recovery code.
  • 2FA materially raises the bar on the weak 6-char password minimum - enable it for every privileged account.

Roles and RBAC

Three roles (DashboardRole) gate what a session can do. Enforcement lives on the server (AuthUser::is_admin() / is_operator()); the frontend mirrors it but must never be trusted.

Role Read telemetry Remote control / scripts / terminal User & system administration
admin ✅ (manage users/roles, settings, enrollment)
operator
viewer
  • Viewers cannot remote-control endpoints. Agent control commands are operator-gated and validated against a strict per-command allowlist plus a key allowlist in ws_viewer.rs before anything is forwarded to an agent.
  • The interactive terminal (/ws/terminal) requires the operator role and the remote-exec master switch (below).
  • Role changes and password resets should be treated as session-affecting - re-authenticate privileged sessions after a role change.

Agent enrollment & per-device WebSocket tokens

  • Agents authenticate to /ws/agent with a per-device 256-bit bearer token sent in the Authorization header (never in the URL). The server stores only an Argon2 hash of each token. Tokens are issued at enrollment approval.
  • Pairing codes are 6-digit, SHA-256-digested at rest, single-use, expiry-bounded (default 10 minutes), and revocable. quick_pair/invite codes auto-approve; other codes require admin approval.
  • Brute-force control on enrollment is the per-IP rate limiter on the enroll routes - distributed guessing across many IPs is feasible (documented gap).
  • Legacy direct enrollment (agent_enroll_http.rs) is retired and returns 410 Gone.

Threat model: a stolen agent token is effectively a full RCE foothold on that endpoint when remote-exec is enabled (no agent-side command allowlist or on-endpoint audit). Protect agent tokens accordingly and rotate on suspicion.


Remote script execution gating (kill-switch)

Running PowerShell/cmd on agents - ad-hoc, scheduled, or via the interactive terminal - is gated behind a single master switch:

ALLOW_REMOTE_SCRIPT_EXECUTION=false   # default; remote exec is OFF
  • Default is off. When off, every execution entry point refuses:
    • ad-hoc "run script now" (api/software_scripts.rs),
    • scheduled-script manual trigger and background fire (scheduler.rs),
    • the interactive terminal (/ws/terminal), which additionally requires the operator role and is per-session audited (output is routed only to the requesting session, never broadcast).
  • Agent-side, scripts run with -ExecutionPolicy Bypass and the agent supports arbitrary-path file read/write/delete and power commands. There is no agent-side allowlist and no on-endpoint audit log.
  • Leave this switch off unless you genuinely need remote execution, and restrict who holds operator/admin when it is on.

Secrets handling

  • Every server secret supports a _FILE variant (Docker/Compose secrets), resolved by read_env_or_file: e.g. DATABASE_URL_FILE, ADMIN_PASSWORD_FILE, INTEGRATION_API_TOKEN_FILE. Prefer the _FILE form over inline env values, and supply secrets via files or a secrets manager - never bake them into images or commit them.
  • Agents do not use a shared secret: each device authenticates with its own per-device WebSocket bearer token minted at enrollment (see the enrollment model above), so there is no AGENT_SECRET to manage.
  • OIDC secrets are an exception: the OIDC loader reads the plain OIDC_* environment variables only (it does not consult OIDC_*_FILE). See OIDC.
  • Secret comparisons are constant-time (secrets.rs / subtle).
  • The integration API (GET /api/integration/agents/live) is protected by Authorization: Bearer <INTEGRATION_API_TOKEN> and is only exposed when that variable is set.
  • The agent config is encrypted at rest with Windows DPAPI machine scope plus app-specific entropy (%ProgramData%\Vantyr\config.dat), so the stored connection policy/token is not plaintext on disk.
  • Never log session tokens, agent tokens, enrollment/pairing codes, OIDC tokens, passwords, recovery codes, keystrokes, or full sensitive payloads.

Trusted proxies & forwarded client IPs

Security decisions that depend on the client IP (login lockout keys, audit IPs) must not trust attacker-controlled forwarding headers by default.

# Comma/space-separated CIDRs or IPs of reverse proxies you trust.
# Empty (default) = trust nobody; key on the direct TCP peer.
TRUSTED_PROXY_CIDRS=10.0.0.0/8,172.18.0.0/16
  • When empty (the default), forwarded headers are ignored for security decisions and the direct TCP peer is used.
  • When set, X-Forwarded-For / X-Real-IP / X-Forwarded-Proto are honored only when the immediate peer is inside the allowlist. This stops a spoofed X-Forwarded-For from rotating the login-lockout key.
  • Invalid CIDR/IP entries cause the server to fail fast at startup rather than silently mis-parse.
  • Always set this to your proxy's address(es) when deploying behind a reverse proxy, otherwise IP-based lockout keys all collapse to the proxy IP.

TLS / HTTPS enforcement

  • HTTPS is enforced by default (ENFORCE_HTTPS is treated as true when unset). Non-HTTPS requests get 426 Upgrade Required; only /healthz, /readyz, and /metrics are exempt.
  • Terminate TLS in a reverse proxy and forward X-Forwarded-Proto: https (or wss for WebSocket upgrades). The server uses that header to mark cookies Secure and to satisfy the HTTPS gate.
  • COOKIE_SECURE=true forces the Secure cookie attribute even if proxy headers are missing - but the proxy should still terminate real TLS.
  • Set ENFORCE_HTTPS=false only for local plain-HTTP testing.

Agent IPC hardening

The agent's local control channel is a Windows named pipe (\\.\pipe\VantyrAgentIpc), created with an explicit SDDL security descriptor / DACL:

  • Full control for SYSTEM (SY) and BUILTIN\Administrators (BA).
  • Read/write for the resolved interactive console user SID only.
  • If the console-user SID cannot be resolved, it falls back to granting read/write to Authenticated Users (AU) - a documented, broader fallback.

This prevents arbitrary low-privilege local processes from driving the agent over the pipe in the normal (SID-resolved) case.


Audit log

  • audit_log is append-only at the API layer (no delete/truncate endpoint).
  • Admin/agent-affecting actions go through db::insert_audit_log_traced, with the client IP passed via helpers::audit_ip.
  • It is not tamper-evident (it lives in the same database), and WS-control actions currently record client_ip = None. Ship audit data off-box if you need stronger guarantees.

Known limitations (be honest about these)

As of the latest review, the following are known and not fully mitigated:

  1. Local privilege escalation via the update path - the privileged service's MSI install validates the staging-path allowlist but not the minisign signature (the signature is verified only client-side); the staging dir and pipe DACL are broad. Re-verify signatures on the privileged side before extending this path.
  2. No on-endpoint consent/notice for screen/keystroke/URL capture (stalkerware / GDPR / wiretap exposure). ACCEPTABLE_USE.md is the doc-level mitigation - get informed consent before monitoring real people.
  3. Agent-token theft ⇒ endpoint RCE when remote-exec is enabled (no agent-side allowlist or audit).
  4. Pairing-code brute force is feasible across many source IPs (only per-IP rate limiting).
  5. Insecure default dev posture (plaintext HTTP, weak default passwords) if you bypass the production guards - see Deployment.

Operator hardening checklist

Before exposing Vantyr beyond a trusted lab:

  • Run a reverse proxy with real TLS; keep ENFORCE_HTTPS=true and forward X-Forwarded-Proto: https / wss.
  • Set COOKIE_SECURE=true (belt-and-suspenders with the proxy header).
  • Set TRUSTED_PROXY_CIDRS to your proxy's address(es) so lockout/audit IPs are accurate and unspoofable.
  • Provide all secrets via *_FILE (Docker secrets), not inline env.
  • Set a strong ADMIN_PASSWORD (the 6-char minimum is not enough); rotate it.
  • Enable TOTP 2FA on every admin/operator account; store recovery codes offline.
  • Prefer OIDC SSO and constrain provisioning with OIDC_ALLOWED_GROUPS.
  • Keep ALLOW_REMOTE_SCRIPT_EXECUTION=false unless you truly need remote execution; minimize who holds operator/admin when it is on.
  • Assign least-privilege roles - use viewer for read-only consumers.
  • Set API_RATE_LIMIT_PER_SECOND when the dashboard/API is internet-facing.
  • Lock down enrollment: short pairing-code expiry, revoke unused codes, restrict who can approve agents.
  • Protect/rotate agent tokens and INTEGRATION_API_TOKEN.
  • Ship audit_log off-box; monitor for login_rate_limited, login_2fa_failed, and oidc_provisioning_denied events.
  • Get informed consent before capturing screen/keystroke/URL data; review ACCEPTABLE_USE.md.

See Configuration and Environment-template for the full variable reference, and Deployment for production topology.

Home

Install and configure

Day to day

Integrations

Developers and security

Clone this wiki locally