@zincapp/znvault-plugin-appliance
v0.1.2
Published
Manage the appliance holding the hardware root of trust for the ZnVault bootstrap key
Maintainers
Readme
@zincapp/znvault-plugin-appliance
Manage the appliance that holds the hardware root of trust for the ZnVault
bootstrap key (BSK), from the znvault CLI. This is a plugin for
@zincapp/znvault-cli
(>= 2.11.0); installing it adds the znvault appliance … command family.
Status: v0.1.0 — never run against a real appliance. Every command here is unit-tested against fakes only; nothing in this repository has ever touched real hardware. At the same time, the box this tool is written for holds a production root of trust: the daemon it manages answers the vault's boot-time key requests. Treat every mutating command as unproven — read the plan output, use
--dry-run, and keep a console session to the appliance open the first time you run anything with--confirm.
npm install -g @zincapp/znvault-plugin-appliance
znvault appliance statusThe invariant that shapes everything here
This plugin must work with the vault DOWN. The appliance is what the vault boots from, so a tool that needs a live vault to fix the appliance is useless exactly when it is needed.
That inverts the usual plugin pattern. Every command declares two planes:
| plane | transport | needs the vault |
|---|---|---|
| appliance | ssh with your own key → the daemon's own CLI | no |
| vault | superadmin rootkey … through the CLI client | yes |
The appliance half always runs. The vault half degrades loudly — never silently, and never into a green result.
Two consequences worth knowing before using it:
- The local profile file is the source of truth. It stores values (host, ssh user, paths, the golden KCV), not vault pointers, because a pointer needs a live vault to resolve. There is no vault-stored copy; a second copy is drift.
- The transport is plain
ssh, deliberately not the vault's SSH-CA tunnel — that path rides the vault's own CA and dies with the vault.
Commands
| command | does | vault | mutates |
|---|---|---|---|
| appliance profile add\|list\|use\|rm | local profiles: host, ssh user, source dir, venv, config path, golden KCV | no | no |
| appliance status | daemon status + provenance + unit state, merged with the vault's root-key status when reachable | degrades | no |
| appliance verify | full gate: appliance + strict all-provider rootkey verify + golden-KCV match | required for a verdict | no |
| appliance audit verify\|tail | walk the audit chain; tail the daemon journal | no | no |
| appliance window status\|close | report / close the wrap window | no | yes (close) |
| appliance upgrade | backup → rsync → reinstall → preflight → restart → assert → KCV gate | gated (see below) | yes |
There is no window open, and there will not be one
The daemon's own CLI states why: "a wrap window that could be opened over the network would defeat the physical-presence gate it exists to be". Opening the window is a human at the console. Closing it is offered here, because closed is the safe state — and the daemon does not roll a close back, so a failed audit row after a close is reported as "the window is CLOSED, but…", never as a failed close.
Exit codes
The daemon's contract is three-valued and this plugin keeps it that way. The distinction is the point: collapsing 1 and 2 turns "your audit chain is broken" into "bad flag".
| code | meaning | |---|---| | 0 | ok | | 1 | the appliance answered and reports a problem (broken audit chain, KCV mismatch, a failed assertion) | | 2 | the command could not run: bad usage, a fail-closed config, a missing profile. Nothing was inspected | | 3 | a plane was unreachable (ssh transport, or the vault leg when a verdict needed it) | | 4 | an answer outside the documented contract |
Getting started
# 1. Describe the appliance. Nothing here is guessed: --host AND --ssh-user are
# both required and both yours — this tool ships no default account name.
znvault appliance profile add anchor --host appliance.example.com --ssh-user operator --use
# 2. Look at it. Works with the vault down.
znvault appliance status
# 3. Full gate. The FIRST successful verify records the golden KCV in the profile.
znvault appliance verifyThe golden KCV is never invented. It stays absent until a verify actually observes one, and once recorded, no command offers a way around a mismatch.
Targeting a non-active profile: --appliance, deliberately not --profile
Every subcommand accepts --appliance <name> to run against a profile other
than the active one. The flag is not named --profile on purpose: the host
CLI owns a global --profile <name> (its vault profile — the house idiom is
znvault --profile prod <cmd>), and a subcommand option of the same name would
silently shadow it. znvault appliance verify --profile prod would then aim
the appliance leg at the prod appliance while the vault leg hit whatever CLI
profile happened to be active — two planes of one command pointed at different
targets, with the host's own fail-closed unknown-profile guard never firing.
A test asserts no option registered by this plugin is ever named profile.
Upgrading the daemon
upgrade is the hand-run procedure, transcribed, with its gotchas encoded. It
is a plan by default:
znvault appliance upgrade --source ./zn-trust-root # plan + read-only recon
znvault appliance upgrade --source ./zn-trust-root --confirm appliance.example.comGuard order, and the order is the design:
--confirm <host>must equal the profile's target exactly, checked before any process runs.- The vault KCV pre-gate: every provider matches, the independent second root is
healthy, and the active KCV is the profile's golden value. If the vault is
unreachable the upgrade blocks — override with
--no-vault-gate, which additionally requires--confirmand states in its own output that the KCV gate did not run. - The remote identity assertion: the box must present a hostname, the source tree, the venv interpreter, the live config and the daemon unit. It fails closed.
- Only then: backup → rsync → chown → reinstall → preflight → restart → post-checks → the KCV post-gate.
What the plugin knows about the box, so you don't have to remember
rsync -ashipped the files as the local uid where the originals were root-owned. This already broke a real deploy, so--no-owner --no-groupis mandatory here and achown -R root:rootfollows it.- The source tree is a plain rsync'd directory, not a git checkout —
"update the checkout" is an rsync, never a
git pull. - There is no
sqlite3on the box. The policy-store assertion runs through the venv's own python, and reads column names, trigger names and a row count — never a stored value. provenanceand--versionmay be absent on the deployed build. Absence is reported as a fact about that build, never as a failure.- The live config is not touched by an upgrade, and that is asserted by comparing its digest before and after.
- Store migrations are one-way: the backup restores code, not state. The plan says so before anything runs.
What this plugin will never do
- Invoke
wraporunwrap, or open a wrap window. - Touch any bootstrap-key file, or the independent second root.
- Offer a force path around a KCV mismatch.
- Carry a host name, address, account name, serial or KCV value in its source.
Those live in your local profile file; this repository uses
example.com, neutral placeholder accounts and RFC 5737 documentation addresses, and a shape-based test (test/no-topology.test.ts) enforces it without itself naming anything real. - Default an ssh account name.
--ssh-useris required onprofile addprecisely so no real account name ever needs to exist in this repository.
Requirements
- Node.js >= 20
@zincapp/znvault-cli>= 2.11.0 (the host CLI that loads this plugin)sshandrsyncon your workstation, with your own key installed on the appliance — this plugin never manages credentials
Development
npm install
npm run build
npm run typecheck
npm run lint
npm testEverything is unit-tested against a fake plugin context and a scripted process runner — no test touches an appliance, a network or an ssh binary. Which is the other half of the status warning at the top: the test suite proves the plugin's contracts, not the appliance's behaviour.
License
MIT © ZincApp SL
