All writeups

ProxDoc: An Inventory & Compliance Platform with Agentless Proxmox Scanning, Risk Scoring, and Auto-Generated Docs

September 2, 2026

ProxmoxComplianceCSPMPythonReactAutomationHomelab

The lab grew faster than my memory of it. Twelve Proxmox nodes, fifty-nine guests, hardware ranging from a fanless Celeron to a Ryzen 9 5950X, and no honest answer to the questions that actually matter: what is running right now, what is exposed, and where the hardening gaps are. So instead of SSHing around for an hour every time I needed a straight answer, I built ProxDoc, an inventory and compliance platform that connects to the Proxmox API, scans every guest, scores its posture, and writes the documentation for me. This is that build.

The problem is sprawl, not any single box

Off-the-shelf dashboards tell you a host is up. They don't tell you it's a privileged container with no firewall and no snapshot that has been quietly serving something to the DMZ for three months. I didn't want another status light. I wanted the thing security vendors sell as CSPM (cloud security posture management), pointed at my own cluster.

The scale is the whole problem: a dozen nodes and dozens of guests, a mix of virtual machines and containers, far more than anyone holds in their head accurately. A machine should keep that inventory, not me.

Architecture

                         ┌──────────────────────────────┐
 browser ──HTTP :8091──▶ │  React / Vite SPA (frontend)  │
                         └───────────────┬──────────────┘
                                         │  /api/v1/*  (REST)
                         ┌───────────────▼──────────────┐
                         │      Python backend           │
                         │  discovery · scanner · rules  │
                         │  scoring · exporter · AI       │
                         └───────────────┬──────────────┘
                                         │  Proxmox API :8006 (one connection)
                         ┌───────────────▼──────────────┐
                         │   PVE cluster: 12 nodes,       │
                         │   59 guests (LXC + QEMU)       │
                         └──────────────────────────────┘

A Vite-built React/TypeScript single-page app on :8091 talks to a versioned REST API (/api/v1/...) served by a Python backend. The backend holds one thing: a single connection to a node's Proxmox API on :8006. Everything else is derived from there.

One connection, the whole cluster

The tool doesn't need an inventory file and it doesn't need an agent on every guest. It authenticates once against a single node's API, then rides Proxmox's own cluster membership to enumerate every node, every VM, and every container. Add a node to the cluster and it appears on the next scan for free. The entire configuration surface, in the end, is one connection entry.

That single-connection design is the whole ergonomic argument for building this against Proxmox rather than bolting agents onto guests: the hypervisor already knows what exists, so ask it.

Agentless introspection is the hard part

Knowing a guest exists is easy. Knowing what is inside it, without installing anything, is where the work is. The scanner picks a method per guest and records which one it used:

  • Container exec for LXC, running commands inside the container through the Proxmox API to read installed packages, listening ports, and running services. This is why containers scan cleanly and quickly.
  • QEMU guest agent for VMs that have it installed.
  • SSH or a scan-agent as alternate paths where exec isn't available.
  • Config-only as the honest fallback: if a guest is stopped, or it's a VM with no guest agent, the scan still records everything the hypervisor config exposes and flags the rest as unknown.

Every scan row carries its method and a status of complete or partial, plus the reason when it's partial. Treating config-only and "no introspection method available" as valid recorded outcomes rather than errors is deliberate: a posture tool that only works on cooperative hosts is useless precisely where you need it, on the box nobody set up properly.

Service detection and dependency inference

For guests it can get inside, the scanner detects what services are actually listening, and it infers inter-guest dependencies from established TCP connections. If guest A holds an open socket to guest B, that is an edge in the graph, so there is no manual wiring diagram to keep up to date. The caveat is that these edges reflect only what was live at scan time: a nightly batch job that talks to a database for ten seconds at 3am won't appear in a 5pm scan, so an absent edge means "not seen," not "not there."

The compliance engine

This is the part that makes it more than an inventory. Each guest runs through rule packs, grouped by intent (security, disaster recovery, CIS hardening, operations, and performance), and comes out with a posture score. The score isn't a grade for its own sake. It's a prioritized hardening backlog, generated automatically instead of assembled by hand: the hosts that could use a local firewall, the containers missing a snapshot, the occasional privileged container. That list is the thing I actually work off.

And it doesn't flatter me, which is the entire point of holding a mirror up to your own lab. The first run also turned out to be a clean illustration of where an agentless tool has blind spots. It flagged "no backup schedule" across the fleet, which is a false positive: my backups run to a dedicated Proxmox Backup Server that lives outside the cluster the tool connects to, so from the scanner's single vantage point it cannot see them. That is a useful reminder that an agentless tool only knows what its one connection can reach, and it is why teaching the scanner about the backup server sits on the roadmap.

Ask the cluster in plain language

On top of the structured data sits an AI assistant that answers natural-language questions grounded in the scan results: "which guests are running a database," "list all containers with a web server," "which guests likely talk to the one named mariadb." It isn't a chatbot bolted on for show. It queries the same inventory the rest of the tool is built on, so the answers are the lab's real state rather than a hallucinated one.

Documentation as a byproduct

The reason this project pays for itself: it exports. Every scan can become an HTML report, a print-ready PDF (rendered with WeasyPrint), per-guest Markdown files plus a cluster overview, structured JSON for anything programmatic, or a full HTML project portfolio. From there it can commit and push to Git, publish to Confluence, or fire a webhook, and the scans themselves run on cron schedules.

That closes the loop I actually cared about. The lab documents itself on a timer, the docs live in version control, and a version of the write-up you might be reading started life as one of this tool's exports.

Why it's designed this way

  • Agentless on purpose. Dozens of guests are dozens of things to not install an agent into. Reading through the hypervisor keeps the footprint at exactly one connection and leaves the guests untouched.
  • Posture, not just presence. Anything can list running containers. Scoring them against security, DR, CIS, operations, and performance rules turns a list into a to-do list.
  • Honest partial results. The tool is most valuable on the machine that's misconfigured, half-off, or undocumented, so "I couldn't fully see inside" is a recorded finding, never a failure.
  • Docs are the deliverable. Inventory you can't hand to someone (or to future-you) is just a screen. Export and version control make it durable.

What's next

  • Close the agentless gaps with a lightweight scan-agent or SSH path for the Windows VMs that currently fall back to config-only.
  • Teach it about the backup server so the DR checks read the external Proxmox Backup Server instead of reporting a gap that isn't there.
  • Scope the connection down to a least-privilege API token rather than a broad login, so the tool holds the minimum the scan needs and nothing more.
  • Historical scoring so a regression (a new guest dragging the fleet down) is visible over time, not just in the current snapshot.
  • Remediation, not just detection. The backend already holds a Proxmox API connection, so the obvious next move is letting a finding offer to fix itself instead of only naming the gap.