KLYRN VM  beside SolusVM 2

Install it on the server SolusVM already runs on.

A node enrolled in observe mode reads libvirt, storage and networking and reports. It performs no mutation of any kind. That is not a habit the code keeps: it is refused at the three places a change can physically leave the agent's process, and every one of them is fail-closed. So the way to evaluate KLYRN VM is to put it on a hypervisor that is serving customers and measure what moved.

0 of 21
host artefacts differed around an observe install: packages, routes, addresses, firewall rules, libvirt config, domains, storage, services. None beyond KLYRN's own.
18
differences the same probe found when a negative control changed the host on purpose. The probe was checked before the result was believed.
27 of 27
attempts to operate a staged guest refused, across every spelling of its identity, with no task queued.
0 hits
for the panel token across eleven endpoints and the audit log, searched byte-wise for the value and its prefix.

Observe mode

The mode a node is enrolled in when it belongs to another manager.

Every KLYRN VM node starts in observe mode and stays there until an administrator promotes it. On a host SolusVM 2 owns, that promotion never happens, and the agent spends its whole life reading: the libvirt domain list and each domain's definition, the storage pools and volumes, the networks, the nwfilters, the routes and the addresses. It reports them to the controller. It writes nothing back to any of them.

  1. 1

    An external program

    Every argv is matched against an allowlist of reads, on the basename, so an absolute path cannot change the answer. The rule reads the arguments, not just the program: qemu-img info reads a disk and qemu-img resize changes one, and they are the same binary.

  2. 2

    A libvirt RPC

    The call's name must appear in a table of reads. Absent means refused, including refresh pool, which only rescans a directory and harms nothing, but which observe mode has no need of. A guest-agent ping is permitted; every other guest-agent call is not, because fsfreeze stops a customer's database writing.

  3. 3

    A write to the filesystem

    Atomic writes, directory creation, copies and symlinks all check the path against KLYRN's own trees. A write to /etc/libvirt/qemu/foo.xml, to /etc/netplan, to /etc/nftables.conf or to a guest's disk does not reach the filesystem. It returns a refusal naming the path.

All three are fail-closed. A command, a call or a path nobody classified is refused, and the agent says out loud what it declined to do. The cost of that mistake is a fact the agent could not collect. The cost of the opposite mistake is somebody's hypervisor.

The same rule that starts KLYRN's own agent refuses to stop yours.

Observe mode has to be able to install itself, or it could not be installed at all. So it is allowed three things, all of them KLYRN's own: its own directories, its own systemd unit, and systemctl daemon-reload, which re-reads unit files and starts or stops nothing. The write verbs (enable, disable, start, stop, restart, mask) accept only a unit whose name begins klyrn-vm. Everything else falls off the end of the same function.

Permitted

  • systemctl restart klyrn-vm-node
  • systemctl daemon-reload
  • systemctl is-active libvirtd read verbs may name any unit
  • virsh dumpxml the definition, unchanged
  • writes under /etc/klyrn-vm/, /var/lib/klyrn-vm/, /var/log/klyrn-vm/, /opt/klyrn-vm/, /run/klyrn-vm/

Refused

  • systemctl stop libvirtd
  • systemctl disable solusvm-agent
  • any libvirt call that is not in the read table
  • any write outside KLYRN's own trees
  • console.open reclassified from read to mutating: a console is a keyboard attached to a running guest

Whether the guard is armed is checkable rather than asserted. klyrn-vm doctor prints its state on the host:

klyrn-vm doctor observe
identity       OK    solus-fixture-2 (id 3), observe mode
observe guard  OK    armed: no package, no libvirt write,
                     no file outside /etc/klyrn-vm/, /var/lib/klyrn-vm/

The measurement

Twenty-one photographs of a host, taken twice.

A structural argument is still an argument. observe-safety.sh runs on the host being enrolled: it photographs everything a takeover could damage, runs the install, photographs it again, and diffs. The only allowance is klyrn-vm* in the two systemd lists. Every other artefact must be identical, with no allowances at all. Rule counters are stripped from both firewall rulesets, because a comparison that can never pass teaches nobody anything.

observe-safety.sh run, 11 September 2026 21 rows
  PASS  no package was installed, removed or upgraded
  PASS  no route changed
  PASS  no interface was added, removed or reconfigured
  PASS  no address moved
  PASS  the nftables ruleset is byte-identical
  PASS  the iptables ruleset is byte-identical
  PASS  the ip6tables ruleset is byte-identical
  PASS  no file under /etc/libvirt changed
  PASS  no domain was defined or undefined
  PASS  every domain is in the state it was in
  PASS  no domain definition changed
  PASS  no storage pool changed
  PASS  no volume was created, resized or removed
  PASS  no libvirt network changed
  PASS  no nwfilter was defined or removed
  PASS  no block device changed
  PASS  no LVM volume changed
  PASS  no forwarding or filtering sysctl changed
  PASS  no guest was restarted
  PASS  no service changed state
  PASS  no service was enabled or disabled

  KLYRN's own systemd lines added: 2 (expected: the agent's unit)
  systemd on-demand units a read activated: 1
  Differences that are not KLYRN's own: 0

  VERDICT: the observe install changed nothing on this host.

The probe was verified before the result was believed

A run with --negative-control changed the host on purpose (a dummy interface, an nftables table, a stopped vendor service, a restarted guest, a created volume, a line appended to /etc/libvirt/qemu.conf), and the same script reported 18 differences across eight of its rows, then undid all six changes. A run that reported nothing would have made every PASS above worthless. self-test then mutated each of the 21 artefacts in turn: 0 rows that cannot detect a change.

One honest side effect, reported rather than filtered

Reading the host's time synchronisation state with timedatectl show causes systemd to D-Bus-activate systemd-timedated.service. That is how systemd answers the question. It appears on its own line in the report (named, counted, and attributed to systemd rather than to KLYRN), because filtering it silently would turn a measurement into a reassurance.

The hole this closed first

Observe safety used to rest on one hand-maintained map of the operations that mutate, and the executor refused an operation that appeared in it. Six operations were in neither table (neither classified as mutating nor as read), so they were permitted on an observed hypervisor by omission. It is the failure mode nothing looks wrong for in review, because the wrong thing is the line that is not there. The two tables now have to partition the vocabulary exactly, and a test fails the build if any operation falls in neither or in both.

While both are installed

SolusVM stays authoritative. What KLYRN holds is a shadow.

Connect the panel and KLYRN reads your estate into staging tables: customers, locations, compute nodes, plans, storage, IP blocks, images, limit groups, SSH keys, virtual servers, snapshots and backups, and the source's own task list. Thirteen phases, each reporting a count that moves rather than a spinner. Six workers, never one per virtual machine, because the panel on the other end is serving somebody's customers. Nothing staged is a KLYRN resource.

A staged guest cannot be operated, because there is nothing to find

A staged guest has no row in the vms table, no id in KLYRN's id space and no node. Every VM handler begins by looking up a vms row. So start, shutdown, reboot, force-stop, reset, console, edit and delete do not fail a check: they find nothing. Twenty-seven of those calls were tried against a real staged estate, by source id, by UUID and by a KLYRN-looking integer: every one refused, the vms table still empty, and no task row written.

The client cannot write to your panel either

KLYRN's SolusVM 2 client has one request builder and it is hard-coded to GET. A test parses the package's syntax tree and fails the build if any other verb appears anywhere in it. The recommendation on the connection screen is still to narrow the credential at the source (a dedicated user, a role built only from get permissions, allowed IPs set to the controller), because a source-side restriction is the one that survives a bug in KLYRN.

The credential is a screen, not an environment variable.

The person who holds the API token is rarely the person with shell on the controller. An environment variable means somebody edits a unit file and restarts a service on their behalf; it is inherited by everything the controller spawns, readable in /proc, and it survives into a crash dump. So the token goes in at Migration → Connect a panel, and rotating it is the same screen.

  1. Proved before it is stored

    KLYRN calls the panel with the token first. A row whose token has never worked is a row that fails at the worst possible moment. Send test_only to check a credential and store nothing at all.

  2. Sealed, with the key outside the database

    AES-256-GCM, bound to the row's id as additional authenticated data so a ciphertext moved between rows will not open. The key is 32 random bytes in /etc/klyrn-vm/migration.key, mode 0600. A database copy that reaches a support inbox carries ciphertext and no key.

  3. Never returned, and never printed

    One function can open it, called only from the scan and the connection check, and its result goes straight into a type that redacts itself in every printing form. What an operator sees is four characters of tail behind bullets. Eleven endpoints, the audit log and the rotation refusal were searched byte-wise for the token and its twelve-character prefix: zero hits, with the database column asserted to be ciphertext.

Plain HTTP is permitted only to a private address given as a literal, and refused everywhere else, including to a private name. A great many SolusVM controllers genuinely live on a management network behind a self-signed certificate or none, and that is a different matter from a bearer token crossing the public internet.

Enrolling the host.

One command on the SolusVM node itself. --observe is the default and is written out here because a flag that matters should be visible in the shell history. --allow-controller-host permits enrolling the controller's own machine, which is how a lab runs both on one box.

on the hypervisor read-only
klyrn-vm node install \
  --controller https://ctl.example.com:8444 \
  --token <one-time bootstrap token> \
  --ca sha256:<pinned fingerprint> \
  --observe

There is a fourth flag, --install-packages, which installs the hypervisor packages in observe mode by explicit request. It exists so an operator can prepare a host they own. The safety acceptance above would report it as a violation, correctly.

The handoff, on a real SolusVM 2 estate: 0 packets lost.

On 28 September 2026 KLYRN VM took over a production SolusVM 2 estate: three compute nodes in France and Germany carrying customers' servers, plus a fourth lent for the rehearsal. Every running guest was pinged once a second from outside for the whole session. 18 guests, 10,887 replies, 0 lost. No disk moved, no guest restarted, no network changed.

  • What the handoff does, per host. A fresh inventory from the agent; a check, read from the host's process list, that SolusVM's agent is stopped; the operator's confirmation; a final three-way reconciliation; then the node becomes KLYRN's under a hold, in one transaction with the record of why. The agent proves it took the hold, every guest is verified against a baseline taken before it, and one guest is driven through KLYRN's own task path as the canary.
  • The hold is what makes zero downtime true. A held host is sent no network, no storage pool and no overlay, and runs only power operations, the console, backups and agent updates. SolusVM's switch rules and routes stay exactly as they were, and the agent keeps a copy of them so a later reboot can bring them back without SolusVM. Until the hold is released, handing the host back to SolusVM is one click.
  • Measured on a lent host first. The rehearsal found five faults before they could reach a customer: the freeze check counting guests as SolusVM processes, a canary that passed before its guest had rebooted, a saved switch copy overwritten after a reboot, the console missing SolusVM's VNC password, and an adopted network that would have been built on another host. All five were fixed before the estate.
  • Windows guests come across too. Two Windows Server guests were taken over with the rest, consoles included.

What is still not done.

  • KLYRN's own networking for an adopted guest. While a host is held its guests run on the switch SolusVM built. Firewall groups, resizing, reinstalling and restoring an adopted guest wait until that guest's network is moved to KLYRN, one guest at a time, which is the next stage and is not released.
  • Any 802.1Q VLAN tag on a guest blocks that host. KLYRN VM creates no VLAN interfaces in this version. A machine type outside q35 and i440fx, a disk format that is not qcow2 or raw, a network disk and a macvtap interface block for the same reason: KLYRN refuses rather than preserving them by accident.
  • OpenVZ and Virtuozzo are out of scope. A container compute resource is named out of scope and is never counted as adoptable.
  • Per-VM XML patches under /usr/local/solus/patches cannot be read by anything. The reconcile report names the directory to read by hand.

Start the way the estate did: install the agent in observe mode on one host, read what it finds, and rehearse the handoff on a host you can spare before the ones your customers are on.