Guides / Nodes
Nodes: deploy to your machines
A node is a small agent on another machine. It watches config files and, only where its own config says so, writes what your vault renders: the nginx config for a live site, wg0.conf on a laptop.
The server that holds the vault is the hub. Nodes are opt-in: run unv-server --nodes. Without the flag every /api/nodes/* route answers 404.
Setting it up
# on your machine: mint a single-use token
unv --server https://hub:8743 node token new vps-01 --project edge --out token.txt
# on the managed host
unv node enroll --hub https://hub:8743 --hub-fingerprint <sha256> --token-file token.txt
unv node check --config /etc/unv-node/node.toml
unv node run --config /etc/unv-node/node.toml
Or, as root on the managed host, install the agent as a hardened systemd service in one command (add --plan to print every step without running any):
unv node install --hub http://10.10.0.2:8743 --relay 10.10.0.2:8743 --token-file token \
--target "id=wg0,path=/etc/wireguard/wg0.conf,project=wg/vps,exporter=wireguard,apply=true,reload=wg-quick@wg0,require_approval=true"
A reload runs through a small root-owned systemd path unit that reloads that one service when that one file changes, so the agent never gains the right to reload anything else.
Everything executable is on the host
# /etc/unv-node/node.toml
[[target]]
id = "edge-nginx"
path = "/etc/nginx/sites-enabled/edge.conf"
project = "edge"
exporter = "nginx"
mode = "push" # push: vault to file. pull: file to vault.
apply = false # observe until you flip it
validate = "nginx -t" # must pass before reload
reload = "systemctl reload nginx"
The hub cannot add a command, a path or an apply. A target without apply=true only observes.
What it guarantees
- A node never holds a vault key. Its identity is an Ed25519 key it generated. Every request is signed (method, path, timestamp, body hash), replayed or stale requests are refused, and revoking a node takes effect on its next beat. The node pins the hub's certificate and offers TLS 1.3 only.
- A node is sent only what its token named, and only for files whose
applyit switched on. The hub runs the config checker first and withholds a project with an error finding. - An apply is transactional. The bytes must match the hash the hub announced, the previous file is kept (the last three), the new one is written beside the target and renamed,
validateruns against it, and a failure restores the old file without reloading. - The hub never stores rendered content. It keeps hashes and status. A pulled file passes through memory for two minutes at most and goes to the owner once.
- Everything that matters is audited. Tokens, enrolments, revocations, pulls and every apply go into the hash-chained audit log, attributed to the node. A locked hub observes but pushes nothing.
Production can require a human
unv node policy prod-1 --approval required
unv node approvals
unv node approve ID
The hub holds every push to that node: it records the proposed file in the config history and opens a request for those exact bytes. approve shows the diff against what the node has (secrets as fingerprints) and the file's hash before it asks. The approval lapses in an hour. The push carries a signed approval naming the node, target and hash, and a target with require_approval = true refuses anything else: wrong key, other bytes, another target, expired or replayed. If the vault changes while a request waits, the old yes does not cover the new bytes.
Approval can be signed on your own machine, so a taken-over hub cannot approve itself: unv node approver register --label laptop, put the key in the node's config as approver = "...", and set --approval device.
Nodes with a public address
Enrol with --listen 0.0.0.0:9443 --advertise https://node.example:9443. The node generates its own TLS certificate, the hub pins it at enrolment and polls every 30 seconds. The node refuses anything older than a minute, anything it has seen before and anything for another path.
Reading a file back
unv node pull NODE TARGET --into-chunk NAME previews what would change (names only) and --apply writes it. A field holding a ${reference} is never overwritten with the secret it points at. unv history diff-file FILE PROJECT compares the newest snapshot with a file you fetched; values stay hidden unless you pass --reveal.
When something goes wrong
The config time machine keeps a snapshot of every project's rendered config whenever a save changes it, and unv blast-radius --host vps-01 --since 2026-10-01 lists exactly which credentials were on that machine and the one command that rotates them. Both read records the vault already keeps.
A container can observe but should not apply. Node agents are documented for Linux with systemd.