All notable changes to InfraCanvas are documented here.
The format follows Keep a Changelog. Versions follow Semantic Versioning.
- The v0.19.2 rate-limiter IP detection took the first entry of a spoofed
X-Forwarded-Forheader, which a client could still forge ahead of whatever a third-party reverse proxy appends — removed theX-Forwarded-Forfallback entirely.Cf-Connecting-Ip(set by Cloudflare's real edge network on the default bundled tunnel) remains the sole trusted signal; anything else falls back to the direct peer address.
Regressions found by a follow-up re-audit of v0.19.1's own changes — this is why we test before shipping, and why we re-audit after.
- Critical. The v0.19.1 CSP (
default-src 'self'with noscript-src/style-srcoverride) blocked the dashboard's own Next.js hydration scripts and inline styles outright — the UI failed to render in any CSP-enforcing browser.script-src/style-srcnow explicitly allow'unsafe-inline'(the static-export dashboard has no nonce infrastructure to do better without a larger rewrite); framing protection and same-origin resource restriction are unaffected. - Medium. The v0.19.1 rate limiter bucketed every visitor behind the default bundled cloudflared tunnel into a single shared IP (
127.0.0.1, since that's how requests arrive locally), meaning anyone with the tunnel URL could lock out the legitimate operator with ~20 bad requests. Now readsCf-Connecting-Ip(only when the immediate peer is loopback, i.e. genuinely our own tunnel) so real visitors are rate-limited independently. - Failed auth attempts (bad UI token, bad agent token, bad
?token=) are now logged with the source IP and route — previously silent until the rate limiter tripped into a 429, with no signal an attack was in progress. - Frontend Next.js bumped 14.2.29 → 14.2.35 for consistency with the hosted product (low practical risk here — OSS ships a static export with no server runtime, so most of what 14.2.30+ fixes doesn't apply, but no reason to stay behind).
Hardening pass from a full OWASP Top 10 self-review (no external report this time).
- Removed the unused, UI-unreferenced
host_run_commandaction type — arbitrary shell execution gated only by session auth, with no allowlist. Nothing in the dashboard used it. - UI/agent token generation and cluster-ID generation now fail closed. Both previously fell back to a hardcoded value (
"infracanvas") or a predictable timestamp ifcrypto/randever errored — now they refuse to start instead. - Constant-time comparison for every token/secret check (UI token, agent bearer token, hub resume secret) — closes a theoretical timing side-channel across 5 call sites.
- WebSocket frame size limits added (32MB agent / 4MB browser) — an unbounded frame could previously exhaust memory.
- Security headers on every response — CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and HSTS-when-TLS.
- Per-IP rate limiting on every auth-checking endpoint (20 failed attempts/minute) — previously unlimited guesses were possible against the UI token, agent token, and resume secret.
install-agent.shnow checksum-verifies its first download against the release's publishedchecksums.txt, matching theupdate_agentin-app path fixed in v0.18.1.- kubeconfig parse-error logging now routed through the existing secret redactor as defense-in-depth.
- Per-cluster read-only mode. Independent of the global
--read-onlyflag, each connected cluster now has its own read-only toggle — set it at connect time or flip it later — so some clusters can stay view-only while others remain fully interactive on the same dashboard. - Permission preview before connecting. The Add Cluster dialog now shows what a kubeconfig can actually do (view, exec, restart/kill, scale/edit, read Secrets) before you commit to connecting it, via a set of
SelfSubjectAccessReviewchecks that persist nothing. - Audit log. Every write action, terminal session, and read-only-blocked attempt is recorded (append-only, local file) and viewable from a new Audit tab in the dashboard, or
GET /api/audit. Attributed by session/machine, not by user — OSS has no per-user login.
Fixes six vulnerabilities responsibly disclosed via r/selfhosted. Every one was independently verified against the code before fixing.
- Critical.
--read-onlydemo mode was never enforced onPOST/DELETE /api/clusters. A kubeconfig can contain anexec:credential plugin that client-go runs verbatim, so a read-only public demo could be turned into arbitrary command execution as the service user. Both routes now check read-only mode. - High. Hub mode's shared join token let any connected VM claim any other machine's session —
/api/sessions(which discloses every machine ID) also accepted that same token, making the attack practical end-to-end. Fixed with a per-machine resume secret required on every reconnect; a mismatch is rejected outright instead of silently swapped in over a session with browsers already attached./api/sessionsnow requires the UI token only. - High. Shell injection via the service/unit action parameters, string-interpolated into
sh -c. Switched to direct argv execution (no shell involved). - High. The cloudflared tunnel binary was trusted from a
/tmpfallback cache directory with no ownership check and no checksum — reachable because the installer only setHOMEfor non-root run users, so the default root install silently fell back to a world-writable path any local user could plant a binary in ahead of time. Installer now always setsHOME; a cached binary is only trusted if this process actually owns its directory. - High.
update_agentdownloaded and installed a binary from a caller-supplied URL with zero checksum verification, despite every release already publishingchecksums.txt. Now requireshttps://, verifies sha256 against the release's own checksums, and refuses a custom URL without an explicit hash to check it against. - High. The dashboard/join token file was written world-readable (0644 in a 0755 directory), unlike every other credential file in the codebase.
- Mobile-responsive dashboard. The 200px sidebar had no responsive handling at all, consuming over half a 375px viewport with no scroll fallback — now an off-canvas drawer below 720px. The canvas toolbar, side panels, terminal/logs panels, and overview tile grid all had the same class of issue (fixed widths/heights and hard column minimums with no narrower fallback) and are fixed to match.
- Clusters — connect Kubernetes via kubeconfig, zero agent install. Drop a kubeconfig and InfraCanvas talks to your cluster's API server directly, the same way
kubectldoes — nothing installed anywhere, nothing added to the cluster, and the kubeconfig never leaves this machine. Multiple contexts in one file get a picker so you connect exactly the cluster(s) you mean to. See the Clusters section.
- Terminal output no longer garbles on connect. A PTY resize race meant the terminal could send its first
exec_startbefore the initialfit()ran, and every subsequent resize fired unconditionally with no debounce — both are now sequenced and debounced correctly.
- Progressive disclosure on the canvas. Large graphs now drill down host → category → type → instance instead of dumping every node at once, keeping dense clusters and multi-host views readable.
- Add machine flow, without leaving the browser. The dashboard now generates a ready-to-run join command inline, instead of sending you to the README for the install one-liner.
- Multi-VM hub mode. One dashboard, many VMs — agents connect outbound to a hub instance, so you can watch a whole fleet from a single
infracanvas servewithout exposing anything inbound on the boxes themselves. - LXD/Incus wired into the continuous agent. The cloud agent (
infracanvas start) now keeps LXD/Incus instances live via a dedicated collection tick and auto-appends thelxdscope when a socket is present, matching the one-shot discovery shipped in v0.13.0.
- LXC / LXD / Incus discovery. Containers and VMs managed by LXD or Incus are auto-discovered from the local unix socket (snap LXD, deb LXD, and Incus paths) and drawn on the canvas alongside Docker and Kubernetes — name, status, type, and network, no config and no external dependencies. Instances render as container nodes carrying a
runtime(lxd/incus) label.- Wired into the continuous agent: a new
lxdcollection tick keeps LXD instances live in the graph (previously LXD was only picked up by one-shotdiscover). infracanvas serveincludeslxdin its default discovery scope; the cloud agent (infracanvas start) auto-appendslxdwhen an LXD/Incus socket is present, so no scope change is needed at install time.
- Wired into the continuous agent: a new
- Service and process nodes from v0.12.0 arrived in the graph but were never displayed: the
servicesfilter key was missing from the default active set and the hardcoded filter-chip rows, and Overview had no tiles for them. Canvas now shows Services/Processes group cards and a Services chip; Overview gains "System Services" and "Processes" tiles.
- Systemd services and processes on the canvas. Plain VMs — nginx + postgres + node via systemd or PM2, no Docker, no Kubernetes — now render their real workloads instead of an empty canvas:
- Services with listening ports (postgres, nginx, redis…), failed units, and critical services appear as nodes; ports are aggregated from each service's process tree via a single-pass
/proc/net/tcp+/proc/<pid>/fdsocket scan. - Standalone listening processes (PM2 apps, dev servers) appear as their own nodes.
CONNECTS_TOedges drawn from established local TCP connections — e.g.next-server → postgres :5432— giving real service topology on a bare VM.- New actions: service start/stop/restart, process terminate (SIGTERM) and force-kill (SIGKILL, refuses pid ≤ 1 and the agent itself).
- Service journalctl logs in the Logs panel (
host_logswith unit scoping). - "Services" filter chip, dedicated icons, and detail-panel metadata (unit, ports, main PID, restart count).
- Services with listening ports (postgres, nginx, redis…), failed units, and critical services appear as nodes; ports are aggregated from each service's process tree via a single-pass
- Zombie processes are marked degraded (state
Zin/proc/<pid>/stat).
- Read-only mode for public demos:
infracanvas serve --read-only, installer flag--read-only, andINFRACANVAS_READONLY=trueinconfig.env. The relay blocks every action and terminal request server-side before it reaches the agent; the topology graph and log viewing still work. Blocked actions surface as normal action errors in the UI, blocked terminals print a short notice and close cleanly, and the sidebar shows a "read-only demo" badge. - Late-joining browsers now receive
AGENT_CONNECTED(hostname, scope, read-only flag) immediately on pairing instead of waiting for the next agent event.
README.md: added a "How is this different from Portainer, Lazydocker, or Dozzle?" comparison; compressed the demo gif 7.2 MB → 3.0 MB.
- Installer no longer silently runs the systemd service as
rootwhen$SUDO_USERis unset (e.g. when invoked from a root shell,sudo -i,sudo su -, cloud-init, or some pipedcurl | sudo bashconfigurations). Running as root meant~/.kube/configwas empty and Kubernetes discovery was a no-op for users who hit this path.
install-agent.shnow picks the service user via a cascade:--run-userflag →$SUDO_USER→ first user in/home/*with a readable~/.kube/config→ first user in thedockergroup → first user with a real login shell →root. The chosen user is printed during install.- New
--run-user <user>flag for explicit override.
- "OSS vs hosted" section and table from
README.md. The hosted SaaS doesn't exist yet; promising features that don't ship was misleading. The repo now describes only what's actually shipped. - "brew install … (coming soon)" line from
README.mdquick start. - Stale "legacy/SaaS" annotations in docs.
uninstall-agent.shnow cleans up the bundledcloudflaredbinary cached under~/.cache/infracanvas/(~30 MB) for$SUDO_USERand any other home directories.README.mdUninstallsection expanded to list exactly what gets removed and document the local./uninstall-agent.shpath for repo clones.
Major UX overhaul. The OSS flow is now one binary on each VM, exposed through a free Cloudflare quick-tunnel. The installer prints a public https://*.trycloudflare.com URL — no Docker, no laptop relay, no pair codes, no firewall change.
infracanvas serve(default command) — boots relay, embedded dashboard, and in-process agent on a single port.- Cloudflare quick-tunnel by default:
pkg/tunnelmanages acloudflaredchild process and prints a public HTTPS URL. The binary downloadscloudflaredon first run (Linux); on macOS install viabrew install cloudflared.--no-tunneldisables it and binds the port directly.--privateimplies--no-tunneland binds127.0.0.1. - Random per-install UI auth token, stored in
/etc/infracanvas/config.env. Token is required via?token=query param on first load; subsequent requests use an HTTP-only cookie. - Static-export Next.js dashboard embedded into the Go binary via
go:embedunder build tagembed_full. Default builds embed a placeholder so plaingo buildworks without a Node toolchain. - Install-script port preflight, binary self-test, systemd unit verification, and tunnel-URL extraction from
journalctl(filtered by restart timestamp) for the final banner. - Installer auto-detects
$SUDO_USERand writesUser=/Group=into the systemd unit so the agent runs as the invoking user — Kubernetes discovery picks up~/.kube/configautomatically, andSupplementaryGroups=dockeris added when the user is in thedockergroup. make all,make build-frontend,make build-stubtargets.
- Frontend simplified to a single auto-connecting dashboard (no
Connect VMmodal, no per-VM cards) — one VM per dashboard. - Relay supports
LocalMode: browser WS auto-binds to the in-process agent without aPAIRexchange. agent.env→config.env; service unit renamed frominfracanvas-agenttoinfracanvas(the installer migrates the legacy unit).install-agent.shrewritten: drops the relay-URL config, adds--port,--no-tunnel,--private,--version.
cmd/infracanvas-server/(standalone relay binary — folded intoserve).Dockerfile.server,docker-compose.yml,docker-compose.prod.yml,frontend/Dockerfile,Caddyfile,Caddyfile.prod,.dockerignore,.env.example— all artifacts of the old laptop-relay model.frontend/components/ConnectModal.tsx,frontend/components/VMCard.tsx,frontend/app/vm/[code]/page.tsx.start.sh,examples/agent-config.yaml,examples/infracanvas-agent.service.
The installer detects the legacy infracanvas-agent systemd unit and removes it before installing infracanvas. Re-run the install one-liner to upgrade.
- Bytestrix purple/fuchsia UI palette with Catppuccin Mocha terminal theme
- Container detail panel: environment variables with secret masking toggle, port mappings, volume mounts, image metadata
frontend/.envcommitted with public relay URL so cloned repos work out of the boxCONTRIBUTING.md,SECURITY.md,CHANGELOG.md,LICENSE- GitHub issue and PR templates
- Container terminal: blank screen fixed by importing
@xterm/xterm/css/xterm.cssin layout - Container terminal: double output fixed by disabling React StrictMode
- Container terminal: colors and readline fixed by setting
TERM=xterm-256colorin exec env - Container terminal: real PTY resize via ResizeObserver wired correctly
- Container exec:
No such container: container:XXXXfixed by strippingcontainer:prefix innormalizeEntityID - Port mappings: camelCase field names (
hostPort,containerPort) now match backend output - Kubernetes discovery:
KUBECONFIGpropagated to systemd service via agent env file - Pair codes now generated with
crypto/rand— not time-seeded or guessable - Pair code entropy increased:
WORD-WORD-NNNNNNformat (~1.44B combinations vs ~4.5M before)
- Removed stale dev-only scripts (
deploy-*.sh,connect-vm.sh,test_e2e.sh,test_security.sh) — gitignored - Removed redundant docs (
AZURE_VM_SETUP.md,DEVOPS_*.md,QUICKSTART.md, etc.) — gitignored - Removed internal implementation notes from tracked files
- CI now validates Go linting and frontend build
- README: added Security section, Roadmap, badges, known limitations
- Real-time WebSocket relay: agent → relay → browser
- One-command agent installer (
install.sh) with systemd integration - Grouped and flat canvas views using ReactFlow
- Filter chips with spotlight mode and right-click to hide
- Container actions: restart, stop, start, update image
- Container logs viewer with ERROR/WARN/INFO color-coding
- Interactive container terminal (
docker execvia xterm.js) - VM shell terminal (host PTY via xterm.js)
- Kubernetes full resource graph (cluster → pods → services → ingress → PVCs)
- Kubernetes actions: rollout restart, scale, update image, get logs
- Host discovery: OS, CPU, memory, cloud provider detection
- Health colors: healthy/degraded/unhealthy/unknown per node
- Critical alert banner for degraded groups
- Export canvas as PNG or JSON
- Multi-VM dashboard with per-VM status cards
- Secret redaction before data leaves the VM
- Optional shared auth token between agent and relay
- Docker Compose setup for relay + dashboard
- Production Docker Compose with Caddy reverse proxy and TLS
- Cross-platform agent builds: linux/darwin × amd64/arm64
- GitHub Actions CI (build + test) and release workflow