Files
vps-control/control-plane
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
..
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00
2026-09-19 19:31:36 +03:00

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

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 <integration_token> and expose no agent endpoint or credential.

For the included provisioned instance:

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 <integration_token>

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:

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

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.