Files
vps-control/docs/security.md
DizelBadCoder c4dc19cd41
Some checks failed
CI / containers (push) Has been cancelled
CI / agent (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
git init
2026-09-19 19:31:36 +03:00

4.1 KiB

Security model

Assumptions

The agent host, control-plane host, private transport, and browser are distinct trust zones. Root compromise is out of scope; reducing the blast radius of an application compromise is in scope. WireGuard/TLS protects transport while the application token authenticates the control plane. Control-plane cookies are HttpOnly, Secure in production, SameSite=Strict, short-lived, and paired with a CSRF header on mutations. Passwords use Argon2id.

Threats and mitigations

Threat Mitigation
Public scanning loopback default, documented WireGuard-only binding, no unauthenticated metadata beyond liveness
Stolen agent token file permissions, independent per-server credential refs, rotation without config secrets, TLS/WireGuard
Malicious API input typed JSON, size limits, IP/jail/service validation, fixed argv, no shell
Request flooding request timeouts, concurrency semaphore, token buckets, reverse-proxy limits
Command injection no command strings; executable and subcommands are constants; service names are allow-listed
Privilege escalation unprivileged user, systemd sandbox, no capabilities, write privileges opt-in and narrow
Accidental firewall lockout v1 is read-only; future mutations require local, crash-safe confirm-or-rollback
Disk/log exhaustion journald only, no metric spool, PostgreSQL retention, bounded body/audit fields
Observability token theft independent file secret, constant-time verification, per-source limiting, no agent credentials in Grafana
Expensive dashboard queries fixed metric set, restricted selectors, 31-day ranges, point/row/result caps
Compromised browser RBAC, CSRF, Secure cookies, no agent access, audit; administrators still must use a trusted endpoint
Compromised control plane semantic agent API and allow-lists limit actions; unique rotatable tokens reduce lateral movement
Authentication brute force generic errors, Argon2id, per-IP login limiter, login audit
Suspicious traffic misclassification display factual events and counts; no ungrounded “security score”

Credential rotation

Generate at least 32 random bytes (openssl rand -hex 32). Write the new value to /etc/vps-agent/credentials/control-plane.token, owned by vps-agent, mode 0600, atomically replace the file, restart the agent, then update its control-plane Docker secret and restart the backend. Keep the overlap window short; the agent intentionally accepts one token. Never pass the token on a command line.

Firewall safety design (future helper)

The future root helper owns only table inet vps_control. It writes a bounded snapshot (maximum five), fsyncs a pending transaction with a monotonic deadline, applies a complete validated table, and starts an independent rollback timer. Only an authenticated confirmation commits. On restart it rolls back any pending transaction before accepting a new one. Unknown tables/chains remain read-only.

Operational logging

The agent logs startup/shutdown, invalid configuration, auth failures (without tokens), mutations, and unexpected errors. Successful metric polls are silent. Bound journald globally only after reviewing other services, for example with SystemMaxUse=500M, RuntimeMaxUse=100M, and MaxRetentionSec=1month.

Agent log export is off by default. When enabled, unit names are configuration allow-listed, journal cursors and timestamps are validated, message/page sizes are capped, subprocess time and output are bounded, and backlog ingestion is limited per poll. Common Authorization, password, token, secret, and API-key shapes are redacted on the agent and again before database insertion. This is a defense in depth measure, not a substitute for applications avoiding secrets in logs. PostgreSQL is the durable log store. The Loki-compatible API is read-only and cannot be used to inject logs.

Treat the integration bearer token like a password. Keep it distinct from all agent tokens, mount it from a secret file, rotate it by replacing the file and restarting backend/Grafana, and restrict /integrations/ at the outer reverse proxy to Grafana source addresses where possible.