mkServiceVm presumes the wrong axis: separate a public-host base from a stateful-service layer #55
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/archetypes#55
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replace the
mkServiceVmplaceholder with the axis that actually separates these machines, so the first public-host archetype is not built around a name that presumes the wrong answer.Primary goals:
Current state
flake.nix:1160reads# mkServiceVm = TBD when first service VM is added, andbuildersmapsdev,privacy, andhypervisor. The name presumes two things that do not hold.First, it presumes guest-ness.
mkDevVmandmkPrivacyVmtakeruntime ? machines.${name}.runtimeand map it to a guest module, failing with "no guest module for runtime" when there is none.mkHypervisortakes noruntimeat all, because the hypervisor is bare metal. So the framework already builds a machine with no hypervisor edge; a rented internet-facing host is structurally the same shape, and needs no new concept beyond a builder that does not demand a guest module.Second, it presumes "service" is the distinguishing property. It is not. A static site and a database-backed service are both services and have almost nothing in common operationally.
The axis that does separate them
Every machine here is recreatable — that is where the resilience comes from, and a parallel rebuild from a flake definition plus a backup is the normal recovery path, not an exotic one. What differs is what has to be carried across a recreation:
Dev guests and a static site are the same in the only way that matters operationally, despite one being a guest on the LAN and the other a rented host on the internet. A stateful public host is the different one.
Two layers
Public host base, needed by every internet-facing machine: a public DNS name, ACME and TLS, a reverse proxy, a public firewall posture with fail2ban, remote installation, and administrative SSH. No hypervisor edge, no guest module.
Stateful service layer, needed only by machines carrying irreplaceable state:
An existing Forgejo deployment already implements every item in the second list, and none of that implementation mentions Forgejo. That is the evidence the layer is real rather than one service's peculiarities.
How to decide rather than guess
A stateless public host is the cheap negative control. It needs the base and none of the stateful layer, so the diff between it and a stateful deployment is the layer boundary, measured. Building a second stateful service first would only confirm what is already suspected. That experiment is tracked separately.
Scope
In scope once the experiment reports: a public-host builder and a stateful-service module, named for the axis rather than for "service" or "VM".
Explicitly not in scope: folding any particular service's module into this repository. This repository is the composition framework; giving it a dependency on every service the fleet ever runs is how it stops being one. Service modules stay in their own repositories and are consumed as inputs.
Open question for the builder's shape: a stateful deployment module observed in the wild sets
services.caddy,services.openssh,services.fail2ban, andnetworking.firewallat machine scope rather than scoping them to the service. That is correct while one service owns one host, and it is exactly what has to change before two services can share a host. Whether colocation is wanted at all is a separate decision — for a service handling money it is probably wrong on blast-radius grounds regardless of what the code permits.Outcome: the base is built, and the guarantee it promised was removed
Settled 2026-08-28.
nixosModules.publicHostexists as one squashed commit onagent/public-host-base, awaiting a human to relay it into this repository.What it is. 221 lines. Caddy with ACME and a required contact address, administrative SSH on a caller-chosen non-standard port with keys only, fail2ban, and a firewall opening 80, 443, the administrative port, and whatever a service contributes. A service module contributes a virtual host and its ports; it does not touch machine-wide configuration.
rentedKvmGuestis offered separately, because the boot and disk layout the two machines share measures one provider rather than anything a public host needs.What was removed, and why it matters for anyone extending this. An earlier draft carried a port ledger: every open TCP port had to name a fail2ban jail or a written accepted-exposure reason, and the module claimed this was unbypassable. Successive adversarial review kept finding routes past it — per-interface firewall lists, per-interface ranges, trusted interfaces, raw iptables and nftables rules,
networking.nftables.ruleset, foreign tables, the firewalld backend,networking.nat.forwardPorts, disabling the owned services outright, and then disabling their generated systemd units one level below that. Each round closed the enumerated routes and the next round found more, because the set of ways a port becomes reachable is not enumerable.The ledger and its audits are gone: 1517 lines deleted, 147 kept. The module now states plainly that it is a default posture rather than a guarantee — a consumer can open a port another way or switch a service off, and the base does not try to stop them.
Two assertions survived, on a rule worth reusing: keep what catches a machine that would fail to come up, drop what defends against a configuration written against its owner's intent. Those are that the administrative port may not take 80 or 443, and that
services.openssh.portsis exactly that port so a second definition cannot move the daemon somewhere the firewall has not opened.The transferable lesson. When successive reviews keep finding new instances of the same class, the design is wrong rather than the implementation incomplete. That signal appeared at the second round and was misread as "keep patching" for four more.
Progress after #58 merged:
nixosModules.publicHostis on master at3ba8082, and the negative-control result is now consumed by the static-site module and Zola builder ate6a3ee4.This issue should remain open. The public-host half is complete, but the stateful-service layer and builder are not, and #58 deliberately did not answer the inventory design question for rented machines that have a platform but no meaningful runtime.
Closing: this issue's question has been answered, half by measurement and half by a failed attempt.
The half that was right, and is settled. The axis is what must be carried across a recreation, not service-versus-dev or VM-versus-VPS. A stateless public host and a dev guest are the same in the way that matters; a stateful public host is the different one. That produced the two layers this issue asked for:
nixosModules.publicHostis merged and is the base every internet-facing machine composes, and astatefulServicelayer remains the correct next one —modules/public-host.nixnames it explicitly as the deliberately-excluded neighbour.static-site-parts'sDIFF.mdis the measurement this issue asked for, taken against a real machine rather than asserted.The half that was wrong. The bridging clause — "guest-ness is not a barrier, the framework already builds a non-guest machine, so a rented host needs no new concept, only a builder that does not assume a hypervisor" — does not hold. The hypervisor is a non-guest but a full three-legged inventory machine: platform, hardware module, LAN presence, identity, profile definition. A builder in this framework is the join of inventory × secrets × profiles, and a rented static-site host has none of those legs. Acting on that clause produced
allod/archetypes#60, which was reviewed four times and closed unmerged; the reasoning is there.So the layering survives and the builder does not. A rented host is a
nixosConfigurationsentry composed from the exported modules, in the management path and outside the data-model path.What is left for the stateful case is not this issue. A forge host needs a
secretsidentity where a static host needs none — that, rather than guest-ness, is the axis that decides where a machine's data lives. It also needs backups, a restore gate, and a restore story that survives the forge being the thing that is down. That deserves its own issue written against a real machine, in the repository that will hold it.