-
Notifications
You must be signed in to change notification settings - Fork 0
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).
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.
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.
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.
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.
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 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, andbootstrap_default_adminnever 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.
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.
- Every session has a per-session CSRF secret (returned from
POST /api/loginandGET /api/me). - All mutating verbs (
POST/PUT/PATCH/DELETE) on protected routes require anX-CSRF-Tokenheader that matches the session value, compared in constant time (subtle). - WebSocket upgrades remain
GET-only and are not CSRF-checked.
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-Forcannot 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.
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: truewhen 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.
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.rsbefore 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.
- Agents authenticate to
/ws/agentwith a per-device 256-bit bearer token sent in theAuthorizationheader (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.
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).
- ad-hoc "run script now" (
- Agent-side, scripts run with
-ExecutionPolicy Bypassand 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.
- Every server secret supports a
_FILEvariant (Docker/Compose secrets), resolved byread_env_or_file: e.g.DATABASE_URL_FILE,ADMIN_PASSWORD_FILE,INTEGRATION_API_TOKEN_FILE. Prefer the_FILEform 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_SECRETto manage. -
OIDC secrets are an exception: the OIDC loader reads the plain
OIDC_*environment variables only (it does not consultOIDC_*_FILE). See OIDC. - Secret comparisons are constant-time (
secrets.rs/subtle). - The integration API (
GET /api/integration/agents/live) is protected byAuthorization: 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.
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-Protoare honored only when the immediate peer is inside the allowlist. This stops a spoofedX-Forwarded-Forfrom 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.
-
HTTPS is enforced by default (
ENFORCE_HTTPSis treated astruewhen unset). Non-HTTPS requests get426 Upgrade Required; only/healthz,/readyz, and/metricsare exempt. - Terminate TLS in a reverse proxy and forward
X-Forwarded-Proto: https(orwssfor WebSocket upgrades). The server uses that header to mark cookiesSecureand to satisfy the HTTPS gate. -
COOKIE_SECURE=trueforces theSecurecookie attribute even if proxy headers are missing - but the proxy should still terminate real TLS. - Set
ENFORCE_HTTPS=falseonly for local plain-HTTP testing.
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) andBUILTIN\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_logis 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 viahelpers::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.
As of the latest review, the following are known and not fully mitigated:
- 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.
-
No on-endpoint consent/notice for screen/keystroke/URL capture
(stalkerware / GDPR / wiretap exposure).
ACCEPTABLE_USE.mdis the doc-level mitigation - get informed consent before monitoring real people. - Agent-token theft ⇒ endpoint RCE when remote-exec is enabled (no agent-side allowlist or audit).
- Pairing-code brute force is feasible across many source IPs (only per-IP rate limiting).
- Insecure default dev posture (plaintext HTTP, weak default passwords) if you bypass the production guards - see Deployment.
Before exposing Vantyr beyond a trusted lab:
- Run a reverse proxy with real TLS; keep
ENFORCE_HTTPS=trueand forwardX-Forwarded-Proto: https/wss. - Set
COOKIE_SECURE=true(belt-and-suspenders with the proxy header). - Set
TRUSTED_PROXY_CIDRSto 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=falseunless 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_SECONDwhen 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_logoff-box; monitor forlogin_rate_limited,login_2fa_failed, andoidc_provisioning_deniedevents. - 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.
Install and configure
Day to day
Integrations
Developers and security