senn-techsenn-tech
Security
Security2026-09-18· By Franz Senn

Komodo-MCP, ANAS, Changerawr: three weekend projects read from the inside

On Friday evening a list circulates with seven "projects for the home lab weekend": a TrueNAS storage plugin for Proxmox, a changelog system, an MCP server for Komodo, a NAS layer for Proxmox nodes, Proxmox on ARM64, break-glass accounts ahead of single sign-on, and rules against unwanted adjacency in the cluster.

Item 1, the TrueNAS plugin, we already read at source level: clean architecture, officially beta as an operating state. The next three items had not been described from the inside in any language we follow. We read the licence file, the issue tracker and the source, and ran one of the three in our lab. In none of the three does the problem sit where the recommendation stands.

Four files get read before anything is installed. The licence file itself, not the badge in the sidebar. The issue tracker, the one artefact a project cannot speak well of. Contributor count and repository age, because anyone can generate commits in their sleep these days. And what the tool touches on the host. The measurement of our own estate comes at the end.

ToolLicence fileDemand (stars · watchers · open issues)Headline finding
Komodo-MCPnone (four filenames, four 404s)15 · 0 · 0accepts write commands unauthenticated, forced in our lab
ANASAGPL-3.0-or-later158 · 6 · 3patches Proxmox's own files, and the apt hook repeats the patching
Changerawrbespoke "Non-Commercial", not an OSI standard297 · 2 · 1security incident in the tracker, closed the next day without explanation

None of the three has a SECURITY.md. Not one of them has a way to report a flaw privately. Taken on its own, that is the worst shared finding.

Proxmox brand mark
The target is not one machine but the control plane of the whole environment. Whoever leaves a key there hands it out for every host. (Quelle: Proxmox VE)

Komodo-MCP: the control plane without a lock

Komodo is a control program for containers and stacks (GPL-3.0, 12,340 stars, maintained since 2022). MyrikLD's MCP server translates natural language into its API. The readme describes the state without OAuth in a single sentence, and that sentence is the news:

"Without OAuth configured, the server accepts all connections without authentication."

We reproduced it: the unmodified source, started with no OAuth variable set, against a Komodo stub on loopback. The log shows both lines that belong together.

OAuth disabled — missing env vars: KOMODO_MCP_OAUTH_JWT_SECRET, KOMODO_MCP_OAUTH_PASSWORD, KOMODO_MCP_BASE_URL
Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Then a single POST to / with no Authorization header and no MCP handshake, which stateless_http=True does not require anyway. tools/list answers 200 with all 53 tools. tools/call with delete_stack answers 200, and the stub receives the write call with the API key already attached. update_stack passed a modified compose file containing volumes: ["/:/host"] and a curl | sh line straight through. After six calls, the bridge's entire audit trail was one access-log line: path, status code. No tool name, no arguments, no caller.

What a call without credentials reachesMCP clientno token, no handshake0.0.0.0:8000auth = None53 tools33 of them writeKomodo APIX-Api-Key attachedFleetdeploy, stop, delete
In the source, the auth provider is only built if all three OAuth variables are set; otherwise the value is None. The secure form is the exception. (Quelle: Our own run of the unmodified source, 18 September 2026)

Three findings from the source that the readme does not give you. The 53 tools have no read-only tier, no dry run, no allowlist, no approval step. All seven delete_* tools are labelled benign (idempotentHint: True, openWorldHint: False, no destructiveHint), so the client is told that deleting a stack is non-destructive and does not touch the outside world. And even with OAuth switched on there is one shared password for every client with exactly one scope: no roles, no lockout threshold at the login endpoint.

For fairness, the counter-check: there is a second, independently written bridge for the same problem (nicolasestrem/komodo-mcp). Apache-2.0 as a real file, bearer-token comparison in constant time, log output that redacts credentials, and with no token set the program binds loopback only. Same task, two defaults. Using the first one is a choice, not a necessity.

Komodo also needs no bridge in order to deal with permissions: the platform has its own role model with the levels None, Read, Execute and Write per resource. A service user with Read, an expiring key, the decision made in the control program rather than in an open bridge with 53 tools: that is the shape that fits our MCP principles. The lab proof is ten lines and changes nothing on a running system:

curl -sS -X POST http://TARGET:8000/ \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

If the tool list comes back, the finding sits in your own log instead of in a blog post. The reply arrives as server-sent events, which trips up the first attempt at parsing it as JSON.

ANAS: clean core, blind entrance

ANAS is the storage layer that turns a Proxmox node into a NAS, with panels inside the Proxmox UI. The positives first, because they make the rest credible. A real licence file (AGPL-3.0-or-later, read by us), 630 commits, 19 releases since mid-July, 158 stars and 8 forks, three open issues, and all three come from outsiders asking for features rather than for help (#62 Samba recycle bin, #61 "Let ANAS work on PVE-Pool too", #57 cloud sync). Of twelve requests from outside, the maintainer answered eleven, one of them with "Confirmed — excellent diagnosis".

The safety mechanics are real too, not decoration. Destructive operations require a challenge code fetched in advance, called at 33 sites; twelve hex characters, valid two minutes, single use, bound to operation and parameters. The storage pool holding the node's own root cannot be destroyed, there is no override, and hard refusals during a running rebuild have no bypass flag. mdadm --action=repair on a RAID1 leg is blocked by a dedicated check, because copying the first in-sync leg over the others would bless the wrong thing roughly half the time, and that check is wired in so there is no way past it. Commands are built only as argument lists through execFile or spawn, never via a shell, names and paths are charset-validated, passwords go over stdin rather than in argv. And there is no privilege escalator anywhere in the project: no sudoers file, no pkexec, no setuid helper. That is more safety thinking than a lot of established products show.

The findings that decide the matter sit elsewhere.

The repeated patching. ANAS patches Proxmox's own files in place: a script line in /usr/share/pve-manager/index.html.tpl, a proxy hook in /usr/share/perl5/PVE/APIServer/AnyEvent.pm. So that an upgrade cannot lose those lines, the installer drops an apt hook, and it reads verbatim like this (from packages/pve-integration/install.sh, line 413, read by us):

DPkg::Post-Invoke { "if [ -x ${SCRIPT_DIR}/install.sh ]; then ${SCRIPT_DIR}/install.sh || true; fi"; };

After every package transaction on the hypervisor, ANAS's installer therefore runs again, as root, with || true so that a failure cannot break apt. The intent is self-healing, and it is guarded properly, with a syntax test before any swap and a .anas-orig backup. It remains a root script that runs itself after every apt-get upgrade. The same package pulls Node.js via curl -fsSL https://deb.nodesource.com/setup_22.x | bash - and installs Samba, nfs-kernel-server, targetcli-fb, mdadm and btrfs-progs, none of which a Proxmox host ships; the package managers enable and start smbd and nfs-server as part of that. The machine that runs other people's virtual machines becomes a file server on the network. And the default of that new service is the same category of mistake as the MCP tool: bind interfaces only is unset, the value read out is no, so Samba answers on every interface, including the VM bridge, until somebody restricts it.

The blind entrance. The authentication is real cryptography: the gateway verifies the PVEAuthCookie against /etc/pve/authkey.pub, signature, two-hour window, three hundred seconds of skew. Behind it, nothing. A search through the daemon and gateway source for an authorisation check returns zero hits: no pvesh, no /access/, no role lookup. The project's own CLAUDE.md states it as policy: "Add roles, permissions, or authorization logic — auth is binary." Anyone holding a valid Proxmox ticket, from any realm with any privileges, gets storage control on that node up to the confirmation code. And that code is an accident guard, not authorisation: it is handed back to the very caller being challenged, its signature covers operation and parameters but never the user, and a daemon restart throws away every outstanding challenge.

A call that is root on the nodePVE ticketany realm, any privileges/anasforwarded on URL prefix aloneGateway127.0.0.1:3000, checks signatureanasdroot, no role modelsmbd, nfsd, LIOnew on the hypervisor
Authentication cryptographically correct, authorisation absent. The only listener is on loopback, the socket to the daemon is 0600, and whatever arrives there runs as root. (Quelle: Our own read of the source, packages/gateway/src/auth/providers/pve.ts and packages/pve-integration, 18 September 2026)

Two truths about the same blocks. This is the sentence on which our decision turns, and it is provable in the source. A check for whether a ZFS volume is the disk of a running machine does not exist in the destroy path; the function isPveManagedPool is used for replication and schedules and is written fail-open (catch { return false }), and nothing calls it in the destroy handler. A recursive zfs destroy -r needs exactly one confirmation code, with the number of children passed as a warning. Retention cleans up unattended: a schedule calls the snapshot pruning routine and therefore zfs destroy with no code at all, because it runs from a schedule. And the audit trail is thinner than the documentation's tone suggests: logging happens from the job queue, a rejected destruction attempt leaves no trace, and the job store is an in-memory map.

The provenance question. The two newest releases do not carry their tarballs out of the pipeline. The release run for v0.3.2 ended on 15 September in a failed state after 35 seconds, and the one for v0.3.1 likewise; the fields of the release API name the maintainer's own account as the uploader of the offered files, where v0.3.0 and v0.2.12 still show github-actions[bot]. Meanwhile the project's own docs describe the process: "Ultimately GitHub Actions builds the release artifacts." No signed commits, no attestation, no checksum in the docs: nothing binds the offered file to the tagged state. The pipeline itself checks build, unit tests and lint; the 19 integration specs need a local Proxmox stub and never run on GitHub. Across 630 commits there are 33 pipeline runs, the last 100 commits inside 20 days were checked in five runs, and 95 of them carry an AI-authorship trailer. A two-month-old single-author project shipping 19 releases is recognisable as such once you read those four fields instead of the version number.

Our decision: test node yes, pve1 through pve6 no. Not because of maturity, but because our production runs on LINSTOR with DRBD, PBS does the backups, and a second storage design with its own root path on the same node produces two truths about the same blocks. We made the same call on Vitastor.

Changerawr: fine internally, not for passing on

Changerawr is a changelog system (Next.js 16, Prisma, PostgreSQL, Node.js 24 or newer) and works fine as an operations journal, as the recommendation rightly says. The licence file is a bespoke "CHANGERAWR NON-COMMERCIAL OPEN SOURCE LICENSE" from Supernova Software, LLC, not an OSI standard. The text cleanly separates two things that usually get conflated: running it inside your own company is explicitly allowed ("Deploy the Software on your own infrastructure, including for use in commercial organizations and business operations"), while selling, renting, paid hosted instances and, as its own item, "Billing System Integration" are forbidden. Governing law and venue are the LLC's home jurisdiction, with no reference to ours. For internal use that is clear rules; for anything we run for a customer or put into a processing agreement, it is purpose-built use with someone else's venue.

The tracker is worth more here than anywhere else. On 15 April 2026 the maintainer opened a pinned issue, "Security Vulns Found - please shut off your instance.", containing: "At this time, I recommend that you shut off your instance. The next update will have all of these issues fixed." Closed as completed on 16 April. Which flaws, which version fixes them, whether a disclosure path was involved: nowhere in the repository. On 18 September there is one open issue, opened by the maintainer. On the edges, master carries a CHANGELOG.md.backup-2026-04-16T07-12-45-733Z and a package-lock.json.backup, and the shipped compose file also starts an AI-tagging container that pulls roughly 1 GB of model weights on first start. 1,046 commits with 2 watchers is supply side.

Our consequence: internal operation would be defensible under this licence, with documentation duty because of venue and licence chain. The April incident would have been reportable material in our hands, and anyone assessing a tool like this reads the tracker before the feature list gets interesting; that same reach is how we prepare for a NIS2 case. For customer deployment: no.

Two items worth correcting

ARM64 is more than the list says, and less of what the list thinks it is. Proxmox VE 9.2 for arm64 appeared on 5 August 2026 and is not a preview: "Proxmox VE on arm64 is fully supported on the platforms listed above, with the same release lifecycle and support windows as the x86-64 builds." But exactly two families are fully supported (NVIDIA Grace Hopper, NVIDIA Vera), everything else is best effort, and a Raspberry Pi is explicitly excluded, because an arm64 host must boot through UEFI and describe its hardware through ACPI. Add live migration within one architecture only, Ceph on arm64 starting at Tentacle, and arm64 subscription keys on request. No consequence for us, we run x86. For anyone planning "Proxmox officially on the Pi", that is the answer.

Break-glass accounts (item 6) are not a weekend task but a rule. Our login runs through Keycloak and Authentik; a local account that does not need the directory is a matter of course. What is measurable is not whether it exists, but who tests it before it is needed.

And our own measurement

Item 7, redundant virtual machines unknowingly on the same node, is the only item with no tool question in it. So we measured: production cluster, six nodes, Proxmox VE 9.2.20, 63 guests (61 machines, 2 templates), as of 18 September 2026, 23:00.

Guests per node, production, 18 September 2026pve515pve612 · node losspve112pve211pve39pve44017
A node failure here is not an event of one but of up to twelve. At the time of measurement pve6 had been up 15.4 days, pve4 had come back 11.5 days earlier. (Quelle: Own measurement via /cluster/resources)

First: our rules have existed for years, and they are not called /cluster/affinity. Our estate holds six rules in /cluster/ha/rules: three strict positive rules pinning guests to a node pair each (pve1+pve2, pve3+pve4, pve5+pve6), and three negative rules keeping one redundant pair apart each: DNS, domain controllers, Caddy. The two DNS machines therefore sit on pve1 and pve5, the domain controllers on pve2 and pve6, the two Caddy instances on pve2 and pve5. The example from the recommendation is production reality here, just with the real API path.

Second: affinity rules are a high-availability feature, and that is the trap. Introduced with Proxmox VE 9.0 on 5 August 2025, valid only for resources listed in /etc/pve/ha/resources.cfg, and extending them beyond HA-managed services is still an unshipped goal on the roadmap. Reconciling both rule worlds in our cluster: 58 entries in the HA resources, two of them templates (a leftover we are cleaning up), and five running machines outside every rule. For all five the reason is plausible and unwritten: four GPU workstations and one terminal server. Anyone rebuilding item 7 from the list and looking only at distribution will never see those five. The asymmetry matters too: rules over node sets are soft, strict is what turns them into a mandate; rules over resource groups are hard, and put the resource into recovery or error state on failure.

Third: the API path the recommendation implies does not exist. On our version GET /cluster/affinity answers 501 Not Implemented. The path meant is /cluster/ha/rules, maintained with ha-manager rules. And the affinity option in a VM config is CPU-core pinning, not node placement, which explains the mix-up. The answer to "do we already run this?" is two lines long:

pvesh get /cluster/resources --type vm --output-format json \
  | jq -r '.[] | [.node, .name] | @tsv' | sort | awk '{c[$1]++} END{for(n in c) printf "%-6s %s\n", n, c[n]}'

pvesh get /cluster/resources --type vm --output-format json \
  | jq -r '.[] | [.node, .name] | @tsv' \
  | awk -F'\t' '{ if (match($2,/-pve[0-9]+$/)) { w=substr($2,RSTART+1,RLENGTH-1);
      if (w!=$1) print "DRIFT  "$2"  reported:"$1 } }'

The first line produces the distribution above. The second checks our naming convention: 61 of 63 guests carry a -pveN suffix in the name, two carry none, and the deviation between name and actual node is zero. It was never a control, it was an agreement; it only becomes checkable once a script compares them. What gets reported is the difference, not the aggregate, the same design as our patch run. The homelab next door is a single-node cluster, by the way: 22 guests on pve7, 21 of them running. There the question about two machines does not arise; there is no second one.

What we do differently now

  1. Licence file before feature list. Curling four filenames costs six seconds. No licence text means no permission, not "probably fine".
  2. The default is the statement, not the capability. "Supports OAuth" does not answer what the ordinary run produces. We ask about the state with no configuration at all, and now have a ten-line proof per tool.
  3. Ignore supply side, count demand. Commits, releases and version numbers can be produced in any quantity today; 95 of 100 commits in one of these projects carry AI trailers. Stars, watchers and issues from people with no connection to the project cannot.
  4. Provenance before version. Read the pipeline runs and the asset uploader fields, and within a minute you see whether the offered file came out of the checked pipeline. For two of the nineteen releases, it did not.
  5. Authorisation is not authentication. A correctly verified ticket with no role model ends as force on the node. This question now comes before any feature trade-off.
  6. One own measurement before any recommendation. Two pvesh lines, and the list has a reference point nobody can copy.

Item 2 pays out fastest: one line of configuration, and an endpoint that previously could do everything on the network can now only do what is authenticated.

State of 18 September 2026, 23:00. Stars, watchers and issue counts are a snapshot of that evening. We repeat the cluster measurement before every audit and after every node change, and the five guests without an HA rule have a follow-up task.

Further reading

Questions?
Is a tool free to use just because the source sits publicly on GitHub?+

No. Without a licence text you fall back to copyright: publicly readable is not the same as permitted to use, modify and redistribute. For purely internal use that is defensible; for anything we have to document to a customer or partner, it is not. The licence file is our first check, not a footnote.

The MCP server supports OAuth. Isn't „no authentication“ then just a configuration question?+

It is, and that is the point. The default is open and the server binds 0.0.0.0. The secure form is a state somebody has to switch on, not one that the usual compose run produces. The second independent bridge for the same problem shows what it looks like otherwise: a real licence file, a bearer token, and loopback-only binding when no token is set.

Why does this end with a measurement in our own cluster instead of a recommendation?+

Because the most useful line from a list like this is not a tool recommendation but the question you ask your own estate. Ours came down to 63 guests on six nodes and an API path that does not exist in our version. Nobody can copy that number; they can copy the tool list.