# Control Plane The control plane is a small Go API, PostgreSQL 16, and a React/TypeScript UI served by unprivileged nginx. The browser only reaches nginx/backend; agent credentials are mounted into the backend as files and never enter PostgreSQL or the browser. ## Docker deployment ```bash cp .env.example .env mkdir -p secrets openssl rand -hex 32 > secrets/db_password openssl rand -base64 24 > secrets/admin_password openssl rand -hex 32 > secrets/integration_token openssl rand -base64 24 > secrets/grafana_admin_password openssl rand -hex 32 > secrets/grafana_secret_key # Securely copy the exact token from one agent for bootstrap: install -m 600 /secure/source/control-plane.token secrets/agent_token chmod 600 secrets/* docker compose config docker compose up -d --build docker compose ps ``` Open `http://127.0.0.1:8080` through a TLS reverse proxy. With plain local HTTP for development only, set `COOKIE_SECURE=false`. The default host binding is loopback. PostgreSQL and backend ports are not published. Set `BOOTSTRAP_SERVER_NAME` and `BOOTSTRAP_SERVER_ENDPOINT` before the first start, or enroll a server in the UI as Administrator. Endpoints must use a literal loopback/private IP and explicit port. `credential_ref` is a filename in `/run/secrets`, never token material. Add another Compose secret mount for each additional server token. ## Authentication and database On an empty database, the backend reads `secrets/admin_password`, hashes it with Argon2id, and creates `INITIAL_ADMIN_USERNAME`. Change the bootstrap password after adding a user-management workflow or rotate it through an audited database administrative procedure. Sessions are stored server-side, expire after 12 hours, use Secure/HttpOnly/SameSite cookies, and require a CSRF token on writes. Roles are Viewer (read), Operator (Fail2Ban and service actions), and Administrator (server/firewall/policy administration). Every current mutation is audited with actor, target, result, time, and source IP. Secrets are excluded. The backend creates schema on startup. One-minute metric samples are retained 30 days (`METRICS_RETENTION=720h`) and agent logs seven days (`LOGS_RETENTION=168h`). PostgreSQL is the only durable history store. ## Grafana and Loki-compatible access The backend is both a constrained Prometheus-compatible metrics source and a Loki-compatible read-only log source. A separate Loki server is not required: this is what preserves the required `Grafana -> backend -> agent` boundary. Both APIs require `Authorization: Bearer ` and expose no agent endpoint or credential. For the included provisioned instance: ```bash docker compose --profile observability up -d --build ``` Open `http://127.0.0.1:3000`, use the `admin` account and the value in `secrets/grafana_admin_password`. The two datasources and `VPS Control Overview` dashboard are provisioned automatically. `grafana_secret_key` encrypts the datasource credential stored by Grafana and must be backed up; changing it invalidates encrypted secure data. For an external Grafana, configure: - Prometheus datasource URL: `https://vps-control.example/integrations/prometheus` - Loki datasource URL: `https://vps-control.example/integrations/loki` - Custom HTTP header on both: `Authorization: Bearer ` The frontend proxy publishes these protected routes. Restrict them to Grafana's source network at the outer TLS proxy when possible. Supported queries are a deliberately small subset: direct `vps_control_*` metric selectors and Loki stream matchers (`=` or `=~`) on `server_id`, `server_name`, `unit`, and `priority`. ## TLS, backup, and upgrades Terminate TLS at Caddy, nginx, Traefik, or a managed load balancer and forward to the loopback frontend port. Preserve the original client IP only from a trusted proxy. Keep `COOKIE_SECURE=true` in production. Back up with: ```bash docker compose exec -T postgres pg_dump -U vps_control -Fc vps_control > vps-control.dump ``` Also back up `.env`, reverse-proxy/TLS configuration, and secret files through a secret manager. Test restore into a separate database. For upgrades, take a backup, pin image/source revisions, build, then `docker compose up -d`; check health and Audit. Migrations are forward-only in v1, so database restore is the rollback path. ## Development ```bash cd backend && go test ./... && go vet ./... cd ../frontend && npm ci && npm run lint && npm run typecheck npm test -- --run && npm run build ``` The frontend Vite server proxies `/api` to `localhost:8080`. Use a development PostgreSQL and `COOKIE_SECURE=false`. Never point development at a production agent credential.