Files
vps-control/docs/architecture.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

88 lines
4.2 KiB
Markdown

# Architecture
## Components and trust boundaries
The Rust agent is a stateless, pull-oriented HTTP API. It stores no metrics or
log history and does no background polling. `/proc`, `/sys`, journald, and fixed
read-only utilities are queried only when the control plane asks. The Go backend
owns users, sessions, servers, metric/log samples, retention, and audit. React
is served behind nginx and only calls the backend.
Grafana also talks only to the backend. The backend implements the constrained
read portions of the Prometheus and Loki HTTP APIs, backed by PostgreSQL. There
is intentionally no Grafana-to-agent or Loki-to-agent path and no agent token is
ever provisioned into Grafana.
Agent transport is expected to be WireGuard plus a 256-bit application token,
or TLS plus that token. The listen address is loopback by default. Tokens live in
files outside configuration and are never returned by an API.
## Agent API v1
All `/v1` routes require `Authorization: Bearer …`; `/healthz` only returns
liveness. JSON bodies are capped at 64 KiB. Read traffic is capped at 10 rps,
writes at 2 rps, burst 20, with 32 concurrent requests and five-second timeouts.
| Method | Route | Purpose |
|---|---|---|
| GET | `/v1/system/status` | CPU, load, memory, swap, filesystems, uptime |
| GET | `/v1/network/interfaces` | interfaces, addresses and counters |
| GET | `/v1/network/routes` | kernel routes |
| GET | `/v1/network/listeners` | listening TCP/UDP sockets |
| GET | `/v1/network/connections` | current TCP/UDP sockets |
| GET | `/v1/firewall/rules` | backend and read-only ruleset |
| GET | `/v1/fail2ban/status` | installation and jail summary |
| GET | `/v1/fail2ban/jails/:jail` | validated jail detail |
| POST | `/v1/fail2ban/ban` | validated IP and jail ban |
| POST | `/v1/fail2ban/unban` | validated IP and jail unban |
| GET | `/v1/wireguard/status` | interface and peer dump |
| GET | `/v1/services` | allow-listed services only |
| POST | `/v1/services/:name/restart` | allow-listed restart only |
| GET | `/v1/logs` | cursor-paginated journald rows from allow-listed units |
No generic command route exists. Child processes receive a fixed executable and
fixed subcommand plus individually validated argv values.
## Data model
- `users(id, username, password_hash, role, disabled, timestamps)`
- `sessions(id_hash, user_id, csrf_hash, expires_at, source_ip)`
- `servers(id, name, hostname, agent_endpoint, credential_ref, tags, status,
last_seen, timestamps)`
- `metric_samples(server_id, sampled_at, cpu, memory, disk, rx, tx,
connections, bans)` with 30-day retention
- `agent_log_entries(server_id, journal_cursor, event_at, unit, priority,
message, boot_id, pid)` with configurable seven-day retention
- `audit_events(actor, server, action, target, before, after, result, source_ip,
created_at)`; secrets are excluded
Credentials are indirect references to Docker secrets or environment variables,
not database values. Database migrations are append-only and run on startup.
## Observability compatibility APIs
`/integrations/prometheus` accepts direct selectors for the fixed
`vps_control_*` metric set. `/integrations/loki` accepts bounded stream selectors
over `server_id`, `server_name`, `unit`, and `priority`. Full PromQL, arbitrary
LogQL pipelines, writes, remote-write, and Loki push APIs are deliberately not
implemented. Queries are bearer-authenticated with an independent secret,
rate-limited, range-limited to 31 days, and result-limited.
The data path is:
```text
Grafana ──Prometheus/Loki read APIs──> backend ──semantic polling──> VPS agent
PostgreSQL
```
## Privilege design
The agent has no ambient capabilities and runs as `vps-agent`. Linux usually
permits the read-only endpoints without elevation. Fail2Ban mutations and
service restarts need administrator-provided sudoers or polkit rules restricted
to each approved verb/name. The shipped service does not install such policy
automatically. Managed firewall mutation is out of v1 because giving the main
daemon `CAP_NET_ADMIN` violates least privilege; its future helper protocol must
provide prepare/apply/health/confirm/commit and persistent timed rollback.