Case study
Palisade
Continuous attack-surface monitoring for self-hosted, homelab, and small-team infrastructure — with first-class coverage of the AI infra (LiteLLM, Ollama, MCP servers, vector DBs) that generic scanners miss. A pull-only agent enrolls once, discovers listening services on-host, and runs CVE detections locally; only normalized findings ever leave the box. The detections are authored by someone who finds these CVEs.
Why it exists
Turning CVE PRs into an operated product
I write real CVE detections for self-hosted and AI infrastructure — LiteLLM, Next.js, Audiobookshelf, Splunk, Kopia. Today they live as pull requests in other people's repos. Palisade turns that work into something owned and operated: a curated catalog of freshly-authored detections in a niche the big scanners ignore, often before a public Nuclei or Nessus template even exists.
The gap is real. Nessus and Qualys are enterprise-heavy and not homelab-shaped. Raw Nuclei has no control plane, scheduling, history, or triage. Trivy and Grype only see image and dependency CVEs, not running-service exposure. Shodan is external-only. None of them treat AI infra as a first-class category. Palisade is the project built to force what the rest of a portfolio never has to: shipping to multiple users and operating in production, where the infrastructure is the product.
What I built
A pull-only agent and a multi-tenant control plane
The system is two halves. A lightweight Go agent — stdlib-only — runs on each monitored host and never accepts inbound connections. A FastAPI control plane serves the signed detection catalog, ingests findings, scores posture, and drives the loop. A React UI sits on top, multi-tenant from the first request.
The loop, end to end:
- The agent enrolls once with a single-use token, then heartbeats. The control plane issues a discover job; the agent reports the assets it found listening on-host.
-
A heartbeat issues a scan job. Detections are matched by service
and version range — a
litellm <1.40.2detection fires on1.39.0but not1.41.0. Unknown versions fail open, so a vuln is never silently skipped. - The agent pulls the signed catalog bundle, verifies it, and runs the detections locally. It reports back only normalized findings — never raw service data.
- The control plane scores posture with real 30-day trends, evaluates alert rules, and optionally AI-triages each finding — all off the request path.
The interesting part
Integrity over an untrusted channel
The agent runs detections that come down over the network, so the trust boundary is the catalog itself. The control plane signs the detection bundle with Ed25519 over a canonical manifest; the agent rebuilds the same manifest and verifies it against a pinned public key before running any detection. The policy is fail-closed: an empty signature means refuse to scan, a verification failure means refuse to run, and only an explicit dev escape hatch will ever run an unsigned bundle. Key rotation is documented, not improvised.
That fail-closed instinct runs through the rest of the security model too:
multi-tenancy enforced by Postgres row-level security per
org_id (with a SECURITY DEFINER path so the cross-tenant catalog
aggregate stays correct under RLS), agent mTLS via an internal CA whose
client certs are verified at a TLS-terminating proxy, and per-org encryption of
evidence at rest under AES-256-GCM with a per-org wrapped data key.
AI in the loop
Drafting detections, then triaging findings
AI shows up twice, and both times it advises rather than decides. From the Detections screen, + New from CVE URL drafts a detection straight from an advisory with an LLM. I review the draft, then Accept & ship persists it and bumps the catalog version so agents pull it on their next bundle — the human stays in the loop, and nothing auto-ships.
On the other end, new findings are AI-triaged after ingest — a priority, a score, and a rationale surfaced on the finding API. It runs in a durable Arq + Redis background queue (with an in-process fallback when Redis is absent), best-effort and entirely off the ingest request path: it never blocks ingestion, and it no-ops cleanly without an API key.
Outcome
Status
The full loop is implemented and tested end to end — enroll, heartbeat, discover, scan,
findings, posture — with a Go agent test suite, control-plane API and smoke tests, and an
offline make smoke that walks the whole on-host flow against a temp database.
A bundled demo brings up the stack and surfaces a real critical finding (a LiteLLM pre-auth
SQL injection, CVE-2026-42208) you can then mute and watch posture recover.
What's deliberately still open is the production ops layer: infrastructure-as-code, a live deploy, a public status page, observability dashboards, and runbooks. That's the next phase — and the whole point of the project is to actually operate it, not just build it. The way I build is with AI as a pair programmer, not an oracle; the architecture and the security decisions are mine.
Stack