Skip to content

lost-coder/panvex

Repository files navigation


Panvex

Fleet management control plane for Telemt MTProto proxy nodes

Quick Install  •  Features  •  Architecture  •  Configuration  •  Development  •  Docker

Go React gRPC Linux


✨ Features

Feature Description
📊 Fleet Dashboard Real-time monitoring with metrics, health indicators, and alerts
👥 Managed Clients Centralized client management with secret rotation and quotas
🤖 Agent System Lightweight per-node agents with mTLS enrollment and gRPC streaming
🗄️ Dual Storage SQLite for dev/lightweight, PostgreSQL for production
🔄 Self-Update Panel and agents update themselves from GitHub Releases
📦 Embedded UI Single binary ships the React dashboard — no separate web server
🔐 TOTP 2FA Optional two-factor authentication for operator accounts
🛡️ RBAC Viewer, Operator, and Admin roles with middleware enforcement

🚀 Quick Install

Control Plane

sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install.sh)"

Interactive wizard: ports, storage, TLS, firewall, admin account — all configured step by step.

Agent

The control-plane embeds the installer and serves it at <panel>/install-agent.sh, so once you have a running panel:

sudo bash -c "$(curl -fsSL https://panel.example.com/install-agent.sh)"

For the GitHub-hosted bootstrap script (when no panel is reachable yet — typically the very first agent on a fresh control-plane), the upstream copy is also published:

sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install-agent.sh)"

Requires a panel URL and enrollment token (create one in Settings → Enrollment Tokens).

📋 Non-interactive mode (CI / automation)
# Control Plane
PANVEX_ADMIN_PASS='<password>' \
PANVEX_HTTP_PORT=8080 \
PANVEX_GRPC_PORT=8443 \
  sudo -E bash install.sh

# Agent
PANVEX_PANEL_URL='https://panel.example.com' \
PANVEX_ENROLLMENT_TOKEN='<token>' \
  sudo -E bash install-agent.sh

Run bash install.sh --help for all environment variables.


🏗️ Architecture

┌─────────────────────────────────────────────────────┐
│                    🌐 Browser                        │
│           React · TanStack Router/Query              │
├─────────────────────────────────────────────────────┤
│              📡 Control Plane (:8080)                │
│        HTTP API · WebSocket · Embedded UI            │
├─────────────────────────────────────────────────────┤
│              🔒 gRPC Gateway (:8443)                 │
│         mTLS · Bidirectional Stream · Jobs           │
├─────────────────────────────────────────────────────┤
│              🤖 Agent (per Telemt node)              │
│       Heartbeats · Snapshots · Job Execution         │
└─────────────────────────────────────────────────────┘
📁 Repository Layout
Directory Description
cmd/control-plane Control plane server (HTTP + gRPC + embedded UI)
cmd/agent Agent binary with bootstrap and enrollment
internal/controlplane Auth, jobs, presence, storage, server logic
internal/agent Telemt client, runtime, self-updater
internal/gatewayrpc Generated gRPC stubs (protobuf)
internal/security Enrollment, crypto, mTLS CA
web React dashboard (Vite + TailwindCSS 4 + TanStack)
db/migrations PostgreSQL and SQLite schema migrations
proto Protobuf gateway contract
deploy Install scripts, Docker Compose, nginx config
🔧 Tech Stack
Layer Technology
Backend Go 1.26, chi/v5, pgx/v5, modernc.org/sqlite, gRPC
Frontend React 19, Vite 8, TailwindCSS 4, TanStack Router + Query
UI Kit Inlined under web/src/ui/ — Radix UI primitives + CVA
Database PostgreSQL (primary) · SQLite (lightweight)
Deploy Multi-stage Docker · systemd · nginx

⚙️ Configuration

Panvex reads its configuration from environment variables and a config.toml file at startup. Operational tunables (password policy, job worker cadences, presence thresholds, GeoIP, retention) are edited at runtime via the dashboard and stored in the database.

Quick reference

Layer Source Edited via
Bootstrap PANVEX_* env / config.toml Edit and restart panel
Operational DB Settings → ⚙️ Sections
Per-user DB (user_appearance) Settings → Appearance

Essential env vars (bootstrap)

Name Required Purpose
PANVEX_STORAGE_DSN yes sqlite path or postgres URL
PANVEX_ENCRYPTION_KEY yes master at-rest encryption key
PANVEX_DB_PASSWORD postgres overrides DSN password (keeps it out of files)
PANVEX_HTTP_ADDR no HTTP bind, default :8080
PANVEX_GRPC_ADDR no gRPC bind, default :8443
PANVEX_TLS_MODE no proxy (default) or direct
PANVEX_TRUSTED_PROXY_CIDRS reverse-proxy trust X-Forwarded-* from these CIDRs
PANVEX_ENV no production tightens cookie/HSTS defaults

The full list of bootstrap variables (~26) lives in docs/settings/reference.md. A ready-to-edit example config is at docs/settings/example.config.toml.

Operational settings

Operational tunables (password lockout, session timeouts, presence thresholds, retention, GeoIP, update channel, plus 20 others) are managed through the dashboard at Settings. Changes take effect immediately; a few items (session timeouts) require a panel restart and the UI surfaces a banner when that's the case.

The same list is also visible at the /api/settings/schema endpoint and documented in docs/settings/reference.md.

Logging

Both binaries log via log/slog to stderr by default. Three controls are available on the command line; each backend also accepts an env fallback so deployments can configure without flag plumbing.

Flag Env Binary Values Default Effect
-log-level PANVEX_LOG_LEVEL both debug | info | warn | error info Minimum level emitted.
-log-format PANVEX_LOG_FORMAT both text | json text Output encoding. json is flat (no envelope) and ingests directly via Loki/Promtail/journald json parsers.
-log-file PANVEX_LOG_FILE control-plane path none When set, tees stderr to the file (append). Agent logs go to stderr only — daemonise via systemd / Docker.

Both formats carry the same fields. The structured attributes per layer (request_id, agent_id, attempt_id, job_id, …) follow the conventions in docs/superpowers/logging.md; new code should use the slog.*Context variants so the request-id correlation works through HTTP and gRPC.

Typical recipes:

# Dev: verbose text on stderr
panvex-control-plane -log-level=debug

# Prod: JSON to stdout for Loki, plus a local rotation file
PANVEX_LOG_FORMAT=json PANVEX_LOG_FILE=/var/log/panvex/panel.log \
  panvex-control-plane

# Agent under systemd: structured logs picked up by journald
panvex-agent -log-format=json

The control-plane also exposes a per-attempt enrollment timeline in the dashboard (Server Detail → Enrollment history, plus the fleet-wide /enrollment-attempts page) and a live "Recent events" section on the Server Detail page that streams the agent's slog Info+ records in real time over the existing gRPC connection.


💻 Development

Prerequisites

  • Go 1.26+  ·  Node.js 22+  ·  sqlc  ·  protoc + Go plugins

Backend

go build ./...                    # Build all
go test ./...                     # Run tests
go test -race ./...               # Race detector
golangci-lint run ./...           # Lint
sqlc generate                     # Regenerate DB code

Frontend

cd web
npm install                       # Install deps
npm run dev                       # Dev server (proxies API to :8080)
npm run build                     # Production build
npm run lint                      # ESLint

Glossary

Three terms appear in different layers and mean related but distinct things:

Term Where you see it Meaning
Server Dashboard UI ("Servers" page) Operator-facing label for the box being managed.
Node Specs, design docs, fleet-group detail Same thing as Server, used in lower-level technical copy.
Agent DB tables, Go types, gRPC routes (agents, /api/agents/*) The panvex-agent Go process running on the node. One agent row ⇄ one node.

When writing code, always use agent / agent_id. When writing user copy, prefer "Server".

🏃 Local Development Flow

The dev fleet (scripts/dev-panel.sh, scripts/dev-agents.sh, scripts/dev-stop.sh) lives one level above this repo, in the workspace. It writes its DB, logs, and PIDs to .tmp/dev/. See the workspace scripts/README.md for the full orchestration loop. The flow below is the manual equivalent.

1. Bootstrap admin:

go run ./cmd/control-plane bootstrap-admin \
  -username admin \
  -password '<strong-password>'

2. Start control plane:

PANVEX_STORAGE_DSN=data/panvex.db PANVEX_ENCRYPTION_KEY=<your-key> go run ./cmd/control-plane

3. Start frontend dev server:

cd web && npm run dev

Dashboard at http://localhost:5173, API proxied to :8080

📦 Single binary build
cd web && npm run build:embed
cd .. && go build -tags embeddedui -o panvex-control-plane ./cmd/control-plane

🐳 Docker

Each compose file ships a bootstrap service under --profile bootstrap that creates the first admin one-shot. It refuses to plant an account on a non-empty store, so it's safe to re-run.

SQLite (lightweight)
# 1. First-run admin (run before bringing up the backend so the SQLite
#    file isn't contended).
PANVEX_BOOTSTRAP_PASSWORD='<strong-password>' \
  docker compose -f deploy/docker-compose.sqlite.yml \
    --profile bootstrap run --rm bootstrap

# 2. Start the stack.
docker compose -f deploy/docker-compose.sqlite.yml up --build -d
PostgreSQL (dev — default password, plaintext DB traffic)
# 1. Start Postgres + backend (creates schema on first boot).
docker compose -f deploy/docker-compose.postgres.yml up --build -d

# 2. First-run admin.
PANVEX_BOOTSTRAP_PASSWORD='<strong-password>' \
POSTGRES_PASSWORD='<db-password>' \
  docker compose -f deploy/docker-compose.postgres.yml \
    --profile bootstrap run --rm bootstrap
PostgreSQL (production — TLS, no default credentials)
# 1. Bring up the stack. Required env vars are enforced via ${VAR:?...}.
POSTGRES_PASSWORD='<strong-db-password>' \
PANVEX_ENCRYPTION_KEY='<strong-encryption-key>' \
  docker compose -f deploy/docker-compose.prod.yml up --build -d

# 2. First-run admin. Reuse the same PANVEX_ENCRYPTION_KEY so freshly
#    minted secrets are readable to the running backend.
POSTGRES_PASSWORD='<strong-db-password>' \
PANVEX_ENCRYPTION_KEY='<strong-encryption-key>' \
PANVEX_BOOTSTRAP_PASSWORD='<strong-admin-password>' \
  docker compose -f deploy/docker-compose.prod.yml \
    --profile bootstrap run --rm bootstrap

The prod profile refuses to start without POSTGRES_PASSWORD and PANVEX_ENCRYPTION_KEY, forces sslmode=require, sets resource limits, JSON-log rotation (15 MiB × 10), PANVEX_ENV=production, and binds publishers to loopback (terminate TLS at a reverse proxy — see deploy/nginx/default.conf).

Override the admin username via PANVEX_BOOTSTRAP_USERNAME (default: admin).

PANVEX_GEOIP_DIR is an optional override for where auto/URL-mode GeoIP .mmdb files are written. Defaults to <dir(panvex.db)>/geoip for SQLite deployments or /var/lib/panvex/geoip otherwise. Local-mode files are read from operator-supplied absolute paths and ignore this setting.

Dashboard: http://localhost:8080  ·  gRPC: localhost:8443


🤖 Agent Deployment

Panvex supports two transport modes per agent:

  • Inbound (default). The agent dials the panel. Use this when the panel is internet-reachable from the agent host.
  • Outbound (reverse). The panel dials an agent that listens on a public host:port. Use this when the panel is firewalled (private network, VPN-only, behind NAT) but the agent has a public address.

The Add Server wizard (Dashboard → Servers → Add) generates the install command for either mode in one click.

Inbound (the agent dials the panel)

  1. Create an enrollment token: Settings → Enrollment Tokens, or the wizard mints one for you.
  2. Paste the rendered command on the Telemt host:
sudo bash -c "$(curl -fsSL https://panel.example.com/install-agent.sh)"

The script is embedded into the control-plane binary and served from the panel itself — no external CDN required.

Outbound (the panel dials the agent)

Use POST /api/agents/provision-outbound (admin) or the wizard's "Panel connects to agent" branch. The endpoint creates the agent row, mints a 5-minute bootstrap token, and returns a pre-baked install command pre-configured with --mode=reverse, --listen-addr=:<port>, and the panel's CA pin. The agent listens; the panel's outbound supervisor dials it via mTLS.

Install-script source

Each wizard run lets the operator pick one of two install-script sources:

Source URL Integrity Default for
Panel <panel>/install-agent.sh SHA-256 self-check baked into the rendered curl command Inbound
GitHub https://raw.githubusercontent.com/lost-coder/panvex/main/deploy/install-agent.sh Pin a release tag in your deploy automation for reproducibility Outbound

Operators on a fork or behind a private mirror override the URLs via:

PANVEX_INSTALL_SCRIPT_URL=https://panel.example.com/install-agent.sh   # panel source
PANVEX_INSTALL_SCRIPT_GITHUB_URL=https://github.example.com/raw/panvex/main/deploy/install-agent.sh
Manual bootstrap (without installer)
./panvex-agent bootstrap \
  -panel-url https://panel.example.com \
  -enrollment-token '<token>' \
  -state-file /var/lib/panvex-agent/agent-state.json

👥 Managed Clients

Create and manage Telemt clients centrally from the dashboard:

  • 🔑 Generate secrets and user_ad_tag
  • 📏 Set limits: connections, unique IPs, quota, expiration
  • 🌐 Assign by fleet group or individual nodes
  • 🔄 Rotate secrets without recreating the client
  • 📈 Live deployment status, connection links, and usage per node

🔐 Security

Two-Factor Authentication — TOTP 2FA is optional. Enable in Profile page.

Emergency TOTP reset via CLI:

./panvex-control-plane reset-user-totp \
  -storage-driver sqlite \
  -storage-dsn /var/lib/panvex/panvex.db \
  -username admin

🛟 Operator tooling

Subcommand Purpose
diagnose Markdown health snapshot — schema version, row counts, pool stats, CA expiry, encryption-key fingerprint. Paste into a support ticket.
backup SQLite-only tar.gz of a VACUUM INTO snapshot plus metadata.json. Postgres operators use pg_dump.
restore Prints the manual restore recipe (tar -xzf + migrate-schema). Auto-restore is intentionally not supported — overwriting a populated DB is the kind of mistake we refuse to make easy.
verify-audit-chain Walks audit_events chronologically, recomputes the SHA-256 chain (migration 0038), exits non-zero on the first tampered or missing link. Run after a suspected incident or as part of a periodic compliance check.
./panvex-control-plane diagnose \
  -storage-driver sqlite \
  -storage-dsn /var/lib/panvex/panvex.db

./panvex-control-plane backup \
  -storage-driver sqlite \
  -storage-dsn /var/lib/panvex/panvex.db \
  -out /var/backups/panvex-$(date -u +%Y%m%dT%H%M%SZ).tar.gz

The encryption-key fingerprint embedded in both diagnose output and metadata.json is a one-way SHA-256 prefix — operators can confirm two panels share the same PANVEX_ENCRYPTION_KEY without ever exchanging the key itself.


🔄 Updates

The control plane checks GitHub Releases for new versions automatically.

Method Command
Dashboard Settings → Updates → Update Panel / Update Agent
CLI ./panvex-control-plane self-update
Auto-update Enable in Settings → Updates (disabled by default)

Agents can be updated individually or in bulk. The panel sends an update job via gRPC — the agent downloads and installs the new binary automatically.


Built with ❤️ for Telemt fleet operators

About

Panel & agent system for Telemt fleet management

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors