- Shell 92.1%
- Nix 7.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
allod/vm#12 drops the guest login user from trusted-users, and nixos-rebuild --target-host copies a pre-built closure in as that user; an unsigned import from an untrusted user is refused under require-sigs, so every rebuild of every guest would fail. Target root instead, which stays trusted, and drop --sudo (nothing left to elevate). VM_USERNAME is unchanged for the status line and verify-vm. provision-vm already runs nixos-anywhere as root; bootstrap-vm only clones git repos over ssh as the login user, no closure involved. |
||
| docs | ||
| hooks | ||
| nix | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
| setup.sh | ||
nexus
NixOS host framework for an Allod nexus: a libvirt/QEMU VM host with agenix secrets and the provisioning/rotation script toolchain.
The flake exports:
nixosModules.host: virtualization, networking, security defaults, provisioning tools, and host utilities.nixosModules.remoteAdmin: SSH for a hypervisor nobody sits at.homeModules.host: Allod operator commands and the provisioning scripts.
This repository contains no machine identity, disk UUIDs, credentials, or
personal packages. It is designed to be consumed alongside a private
profiles fork.
Flake input
Add the framework to the consumer flake and follow its shared dependencies:
nexus = {
url = "git+ssh://git@forge.example.org:2222/owner/nexus.git";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
inputs.agenix.follows = "agenix";
};
Import nexus.nixosModules.host into the host's NixOS module list and
nexus.homeModules.host into the host user's Home Manager imports.
Remote administration
A hypervisor nobody sits at has no console, so it needs SSH to be
administered at all. nixosModules.remoteAdmin has no enable option: a
machine either imports it or runs no sshd, the same shape
allod/archetypes modules/public-host.nix uses for internet-facing
machines.
Importing it sets services.openssh.enable = true with password and root
login refused (PasswordAuthentication, KbdInteractiveAuthentication, and
PermitRootLogin all off), and opens nexus.remoteAdmin.port (default 22)
only on the interfaces named in nexus.remoteAdmin.interfaces --
networking.firewall.allowedTCPPorts itself stays empty. interfaces
defaults to the empty list, which fails evaluation: a machine that imports
this module and forgets to name an interface is refused rather than left
reachable on none or, worse, on all of them.
Every part of that policy is an assertion over the machine's final composed
value, so a module composed beside this one cannot merge or mkForce it
away: a global allowedTCPPorts entry or a port range covering the port, an
override of any of the three authentication settings, an extraConfig line
touching authentication or opening a Match block, a listenAddresses entry
on another port, a different sshd port, openFirewall, networking.firewall
switched off, and a named interface left without the port each fail
evaluation with the option's name in the message. A NixOS module composed
into a machine can always undo another module's policy, and these assertions
cover the routes we know of, so a route that reaches sshd past them is a bug
to report against this module.
The module sets no authorized keys. Which keys the account accepts is the hypervisor builder's job, from the identity record.
Host System Support
nexus exposes packages and checks for these Linux host systems:
| System | Status |
|---|---|
x86_64-linux |
Tested development target. |
aarch64-linux |
Exposed Tier 1 target, not yet validated on project hardware. |
The flake system list controls where the framework can evaluate its packages and checks. It is separate from deployment inventory: consumers still define VM target systems, machine data, and credentials in their own inventory and secrets inputs.
ARM users should be able to evaluate the framework without patching the flake. Provisioning on ARM still needs hardware validation because QEMU machine types, KVM availability, and UEFI firmware packages differ from the tested x86_64 path.
Forge sync
forge-sync reconciles a forge VM to the deployment's declared state: a
human operator account that owns every organisation, an agent account, an
organisation and repository for every forge-sourced registry id, and
branch protection from the secrets checkout's git/protected-branches.
The hypervisor's own key is the push identity: it is registered as a
deploy key with write access on every repo, and the branch protection this
command installs admits deploy keys and no user, admins included, so the
hypervisor's push lands on every run and nothing else does. The agent key
keeps one job — registered on the agent account for the dev VM's clones
and pull requests, never used to push. A dev VM with no forge key yet gets
one generated and its forgeAccess granted automatically — this command
runs host-only, so that capability absence is the gate, not a separate
hand-edit. It waits for admin SSH and the Forgejo API to actually answer
before doing anything, preflights every other precondition before any
mutation, and verifies each mutation by reading it back rather than
trusting a 2xx. It runs every time, not once; a second run converges, and
--check reports the full plan while touching nothing.
No password or token is ever a command argument. The human account's
Forgejo-generated password is printed once to the operator's terminal,
flagged must-change, and stored nowhere; the agent's access token goes
straight from Forgejo's own CLI into age, encrypted to the recipients the
secrets checkout derives. The command writes that ciphertext, any new forge
key, and the forgeAccess flip, but does not commit or push them — see its
own --help for the exact steps it prints for the operator to run.
Bitcoin node command
allod bitcoin status|start|stop|clearnet|cli is the operator command for a
bitcoin-node machine, driven from the hypervisor the way the forge VM is.
status waits on the node's own RPC rather than the domain, because a node
mid first-sync blocks RPC through its coin-cache flushes, and reports height,
sync state, and whether the clearnet exception is active. start and stop
drive the libvirt domain with local virsh and refuse anywhere but the
hosting hypervisor; stop is an ACPI shutdown that waits out the node's stop
timeout and never destroys, and start returns once RPC answers, which is
mid-sync. clearnet re-enables the clearnet exception for a node that fell
behind; the node's own switch undoes it within minutes once synced, so it is
never a standing mode. cli runs bitcoin-cli over the admin SSH channel.
It ships as allod-bitcoin on PATH and is reached through allod's fallback
for an unlisted namespace, like allod forge will be. The command itself
carries no facts: a deployment turns it on by setting nexus.bitcoinNode.facts
and nexus.bitcoinNode.machines in the hypervisor definition's home modules,
reading the facts straight from the node profile so the command and the
profile cannot disagree:
{
nexus.bitcoinNode.facts = import <its profiles repo>/hosts/privacy/bitcoin-node/facts.nix;
nexus.bitcoinNode.machines = [ "bitcoin-node" ];
}
Leaving facts unset (the default) ships no command at all.
Documentation
- Integration: module arguments and consumer-owned configuration.
- Credentials: SSH access for private inputs and secret lifecycles.
- Host key rotation: Nexus host key rotation, external SSH trust gates, and operator validation points.
- Provisioning scripts: commands, development,
deployment, and stale
PATHrecovery. - VM provisioning test plan: planned regression coverage and rollout phases.