crates/gunnar-recipe/src/reachability.rs
//! **A shipped appliance that nobody can ever administer must not build.**
//!
//! # The defect this refuses
//!
//! MEASURED under KVM on 2026-08-25: `gunnar-installer` installed to a scratch
//! disk mechanically perfectly — GPT, a 64 MiB ESP, a 2 GiB rootfs, a 5.9 GiB
//! data partition, a DHCP lease, an SSH host key that survives a restart, and
//! `SSH-2.0-gunnar_1.1.3` on the wire. And on every start the appliance printed:
//!
//! ```text
//! ERROR NO IDENTITY HAS EVER BEEN ENROLLED at /data, and the gRPC control
//! plane is DISABLED, so there is no way to enrol one. Nobody can reach
//! this server. Start without --no-grpc and call Bootstrap.Enroll, or
//! pass --admin-key.
//! ```
//!
//! That line is written by `gunnar_server::serve::announce_readiness`, and it is
//! `tracing::error!` — a **log**, not a refusal. The server then serves anyway.
//! An installed git server that authorizes nobody, forever, is not a degraded
//! appliance; it is a brick with a TCP port.
//!
//! # THE GATE WAS TEXTUAL, AND THE MEDIUM IT PASSED WAS A BRICK
//!
//! The rule this module shipped until 2026-08-27 was
//! `disables_control_plane(argv) && !declares_admin_key(argv)` — a test of the
//! argv's TEXT and nothing else. `gunnar-appliance-iso` spells
//! `--admin-key @/data/admin.pub` **unconditionally** in its `boot_params`
//! while its `admin_key` parameter is `required = false`, so the literal
//! satisfied the gate and no file had to exist. MEASURED, booted, on the ISO
//! published as `gunnar-appliance-iso-thin-nowg.iso` on release `v1.1.3`:
//!
//! ```text
//! gunnar: reading the public key from /data/admin.pub: No such file or directory
//! ```
//!
//! A gate that reads a string can only ever prove what somebody typed. This one
//! now reads the DECLARATION: an `@<path>` admin key must be produced by a
//! parameter the build cannot omit, landing at exactly that path.
//!
//! # There are exactly TWO doors, and a medium must leave one open
//!
//! **Door A — the seeded key.** `--admin-key @<path>` installs the first
//! identity from a file the BUILD staged. `gunnar_core::auth::bootstrap_admin`
//! reads it once at start and never again; it is a SEED, not an
//! `authorized_keys` list. For the door to be open the file must be guaranteed
//! to exist, which is a property of the declaration and not of the argv:
//!
//! * some declared parameter must be `required = true` — a `required = false`
//! parameter means "a build with no value is legal", and such a build boots a
//! server that hard-fails on the missing file;
//! * and that parameter's staged file must land at the path the argv names.
//! MEASURED 2026-08-27 (`workspace_nordisk/much`, row I7): the ISO's
//! `staged_file = "admin.pub"` landed at `assets/appliance-root/admin.pub`,
//! which arrives as `/admin.pub`, while the argv read `/data/admin.pub`. One
//! directory wrong, `EXIT=0`, `+119` initramfs bytes, and a medium that could
//! never serve. [`staged_runtime_path`] is where that mapping is written down
//! once, per medium typ, so the gate can catch it instead of the customer.
//!
//! **Door B — TOFU.** With the gRPC control plane up, `Bootstrap.Enroll` is
//! open to any caller with no credential and closes permanently on the first
//! success (`gunnar-proto/proto/gunnar/v1/bootstrap.proto`, and the server says
//! so itself: `bootstrap_window="OPEN"` → `The unauthenticated door is now
//! CLOSED, permanently.`). Dropping `--no-grpc` is NOT enough to open it:
//!
//! * `--grpc-bind` defaults to `127.0.0.1:50051`
//! (`gunnar_grpc::config::DEFAULT_BIND`, `gunnar_common::cli`), and
//! * gunnar has **no shell and no port forwarding** — `gunnar-ssh/src/lib.rs`
//! answers `shell_request`/`subsystem_request`/`pty_request` with a channel
//! failure and implements no `direct-tcpip`, so the `ssh -L 50051:…` recipe
//! the flag's own help suggests is impossible against a gunnar appliance.
//!
//! So a loopback control plane on an appliance is reachable by nobody, and a
//! gate that accepted "no `--no-grpc`" as sufficient would have passed a SECOND
//! medium that admits nobody — the same defect wearing the opposite argv. Door
//! B is open only when the listener binds a non-loopback address.
//!
//! # Why both doors, and why neither is the default answer
//!
//! The two arguments are both right and they differ only in SCOPE. The proto
//! argues for TOFU — *"a key baked into an image is a credential that cannot be
//! revoked and cannot be rotated"* — and it is arguing about a LIVE medium whose
//! whole state is RAM and dies with the boot. The paragraph this module used to
//! carry argued the opposite — a POSTED medium's open window is *"a race between
//! the customer and whoever else finds the box"* — and it is arguing about an
//! INSTALLED appliance on a customer's floor.
//!
//! gunnar's declaration now says which is which by SPELLING IT IN THE ARGV,
//! which is the only place a medium's own answer can live:
//!
//! * `gunnar-appliance-iso` (typ `iso`, the live demonstration medium, RAM
//! state) takes door B: no `--no-grpc`, `--grpc-bind 0.0.0.0:50051`, no
//! `--admin-key`. The first caller on the installation LAN enrols and the
//! window shuts behind them.
//! * `gunnar-installer` (typ `installer`, a PERSISTENT server laid onto a
//! customer's disk) takes door A: `--no-grpc` with
//! `--admin-key @/data/admin.pub`, backed by `admin_key`, `required = true`.
//! The customer's own key is the credential and the window never opens to
//! anyone else.
//!
//! An `--opt`-style toggle between the two was CONSIDERED AND REFUSED, and the
//! reason is structural rather than a preference: a
//! `nornir_build_thing::options::OptionSpec` **is** a cargo feature by
//! construction (`validate_options` refuses a row with no `feature`, and
//! `validate_against_variant` holds `default` against the app variant's own
//! feature list, because "off" must mean NOT COMPILED IN). The bootstrap door is
//! argv, not codegen — `gunnar-server` has no `grpc` feature and the control
//! plane is unconditionally compiled — so declaring it as an option would put a
//! checkbox in front of a feature that does not exist. It is a per-medium
//! product decision, so it lives in the medium's own row, in a diff, with a
//! blame line.
//!
//! # What "reachable" means, in full
//!
//! Door A buys *authentication*; on its own the admin's session could be
//! refused three git verbs for want of a repository. That half is closed by the
//! **first-repository door** (`gunnar_ssh::Control::create_for_push`): the
//! enrolled admin's first `git push` to a repository that does not exist CREATES
//! it — same three verbs, no new wire surface, gated on the same `Server`/`Admin`
//! grant bootstrap writes. So the argv door A demands is genuinely sufficient:
//! the appliance it admits can be administered with nothing but the customer's
//! key and stock git.
//!
//! # Why the rule lives HERE and not in edda
//!
//! `--no-grpc`, `--admin-key`, `--grpc-bind` and "the staging tree is grafted
//! at the initramfs root" are gunnar's vocabulary. `nornir-build-thing` is the
//! shared build framework for the whole constellation and knows about kinds,
//! params and options — not about one product's argv. Teaching it these names
//! would make every other member carry gunnar's control-plane model. Product
//! knowledge belongs to the product (one implementation cuts the other way
//! here: this is not a twin of anything in edda, it is the half edda must not
//! have).
//!
//! # Total over all three argv spellings
//!
//! gunnar's two appliance variants spell the app's argv differently, and a
//! gate that read only one of them would go green on the other — the exact
//! shape of a guard that can never fail (measure, never assert). [`app_argv`]
//! is therefore total over every spelling the declaration can use:
//!
//! ```text
//! cmd = ["serve", "--no-grpc"] InstallerBuild::cmd, the 0.1.7 field
//! "korp.args=serve,--no-grpc" a comma-joined boot_params token
//! "--", "serve", "--no-grpc" boot_params after a `--` separator
//! ```
//!
//! The first two are the installer variant's; the third is the live ISO variant's.
use anyhow::{bail, Result};
use nornir_build_thing::params::ParamSpec;
use nornir_build_thing::variant::ArtifactKind;
/// The flag that turns the gRPC control plane off.
pub const NO_GRPC: &str = "--no-grpc";
/// The flag that seeds the bootstrap admin from a public key (or `@<file>`).
pub const ADMIN_KEY: &str = "--admin-key";
/// The flag that chooses the gRPC listener's socket address.
pub const GRPC_BIND: &str = "--grpc-bind";
/// The boot parameter naming the interfaces the medium brings up, and how:
/// `korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp`.
pub const GUNNAR_NET: &str = "korp.env.GUNNAR_NET";
/// **How many interfaces this medium brings up**, read off [`GUNNAR_NET`] —
/// the ONE declaration that owns the fact.
///
/// Deliberately NOT `target.nics`, which is what the cloud hands out rather
/// than what the medium configures; the KVM rig boots `max(preset, declared)`
/// for that reason, and `installtest::firmware::declared_interface_count` — the
/// same rule, on the same string — records why a second copy of the number
/// would drift.
///
/// Absent or empty reads as 1: the historical shape, and never 0.
pub fn declared_interface_count(boot_params: &[String]) -> u16 {
boot_params
.iter()
.find_map(|p| p.strip_prefix(&format!("{GUNNAR_NET}=")))
.map(|spec| spec.split(',').filter(|e| !e.trim().is_empty()).count())
.unwrap_or(0)
.max(1)
.min(u16::MAX as usize) as u16
}
/// The `boot_params` token whose comma-joined tail is the app's argv.
pub const KORP_ARGS: &str = "korp.args=";
/// The app's argv, recovered from a variant's declaration — total over every
/// spelling gunnar's variants use (see the module docs).
///
/// `cmd` is the 0.1.7 `InstallerBuild::cmd` field; `boot_params` is the raw
/// token list. Both are read, because a variant may use either and a gate that read
/// one would be blind to the other.
pub fn app_argv(boot_params: &[String], cmd: &[String]) -> Vec<String> {
let mut argv: Vec<String> = cmd.to_vec();
let mut after_separator = false;
for token in boot_params {
if after_separator {
argv.push(token.clone());
continue;
}
if token == "--" {
after_separator = true;
continue;
}
if let Some(tail) = token.strip_prefix(KORP_ARGS) {
argv.extend(tail.split(',').filter(|s| !s.is_empty()).map(str::to_owned));
}
}
argv
}
/// The VALUE of a `--flag` in an argv, in both spellings clap accepts —
/// `--flag V` and `--flag=V`. `None` when the flag is absent; `Some("")` when it
/// is present with nothing after it, which is a declaration bug of its own and
/// is refused by the caller rather than silently read as "no value".
fn flag_value<'a>(argv: &'a [String], flag: &str) -> Option<&'a str> {
let eq = format!("{flag}=");
for (i, a) in argv.iter().enumerate() {
if let Some(v) = a.strip_prefix(&eq) {
return Some(v);
}
if a == flag {
return Some(argv.get(i + 1).map(String::as_str).unwrap_or(""));
}
}
None
}
/// Does this argv name an admin credential? Both spellings — `--admin-key K`
/// and `--admin-key=K` — because clap accepts both and a gate that knew only
/// one would refuse a variant that is in fact reachable.
pub fn declares_admin_key(argv: &[String]) -> bool {
admin_key_argument(argv).is_some()
}
/// The `--admin-key` VALUE as declared: a literal OpenSSH public key, or
/// `@<path>` naming a file the medium must carry.
pub fn admin_key_argument(argv: &[String]) -> Option<&str> {
flag_value(argv, ADMIN_KEY)
}
/// Does this argv disable the control plane?
pub fn disables_control_plane(argv: &[String]) -> bool {
argv.iter().any(|a| a == NO_GRPC)
}
/// **Is the gRPC control plane reachable from OFF the box?**
///
/// Not the same question as "is it running". `--grpc-bind` defaults to
/// `127.0.0.1:50051` and gunnar offers no shell and no port forwarding, so a
/// listener on a loopback address is reachable by nobody — `Bootstrap.Enroll`
/// included. An appliance therefore has to say a non-loopback bind out loud.
///
/// A `0.0.0.0`/`[::]` wildcard, any routable literal, or an INTERFACE NAME
/// (`eth1:50051`) counts; `127.x`, `::1` and `localhost` do not, and neither
/// does an absent flag.
///
/// The interface spelling is the one an appliance should use and the wildcard
/// is the one it should not: a wildcard binds the PUBLIC interface too, and on
/// a variant with no `--admin-key` that puts `Bootstrap.Enroll` — the ONE
/// service mounted outside the auth gate — on the open internet until the first
/// enrolment succeeds. This predicate cannot tell the two apart (both are "off
/// the loopback", which is the question it answers); [`binds_a_wildcard`] is
/// the one that can.
///
/// ⚠ This comment used to say the exposure was an *anonymous admin surface*,
/// on a 2026-08-31 measurement that `repo list`, `user list` and `repo create`
/// served an uncredentialled caller. That measurement was wrong and the
/// correction is recorded at [`crate::recipe::OPEN_KORP_ARGS`]: `gunnar control`
/// silently signs with `~/.ssh/id_ed25519`, the probing host's key is enrolled on that box
/// as gunnar-admin, and a re-measurement with a raw gRPC request carrying no
/// metadata got `UNAUTHENTICATED` from every gated service. The enrolment
/// window above is real; the anonymous admin surface was not.
pub fn binds_control_plane_offbox(argv: &[String]) -> bool {
if disables_control_plane(argv) {
return false;
}
let Some(bind) = flag_value(argv, GRPC_BIND) else {
// The default, which is loopback.
return false;
};
let host = bind.rsplit_once(':').map(|(h, _)| h).unwrap_or(bind);
let host = host.trim_start_matches('[').trim_end_matches(']');
!(host.is_empty()
|| host.eq_ignore_ascii_case("localhost")
|| host == "::1"
|| host.starts_with("127."))
}
/// **Does this argv put the control plane on EVERY interface?**
///
/// `0.0.0.0`, `[::]`, or an empty host — the spellings of "everywhere".
/// Distinct from [`binds_control_plane_offbox`] on purpose: "reachable from
/// off the box" is what an appliance NEEDS, and "on every interface including
/// the public one" is what it must not have. One predicate answering both
/// questions is how the second one stopped being asked, and how
/// `gunnar-installer-open` shipped a wildcard for four days.
pub fn binds_a_wildcard(argv: &[String]) -> bool {
if disables_control_plane(argv) {
return false;
}
let Some(bind) = flag_value(argv, GRPC_BIND) else {
return false;
};
let host = bind.rsplit_once(':').map(|(h, _)| h).unwrap_or(bind);
let host = host.trim_start_matches('[').trim_end_matches(']');
host.is_empty() || host == "0.0.0.0" || host == "::" || host == "*"
}
/// **Which NIC does `--grpc-bind <iface>:<port>` name, as an index?**
///
/// `eth1` → `Some(("eth1", 1))`. `None` when the bind is an address rather
/// than an interface, or when the interface has a name this function cannot
/// index — `ens5`, `enp0s3`, `wg0`. **A gate must not guess.** Predictable
/// names encode a bus address, not an ordinal, so there is no honest mapping
/// from `enp0s3` to "the machine needs N NICs", and inventing one would refuse
/// correct declarations. `eth<N>` is the only spelling with a defined answer,
/// and it is the spelling every gunnar medium uses.
pub fn bound_interface_index(argv: &[String]) -> Option<(String, u8)> {
if disables_control_plane(argv) {
return None;
}
let bind = flag_value(argv, GRPC_BIND)?;
let host = bind.rsplit_once(':').map(|(h, _)| h).unwrap_or(bind);
let host = host.trim_start_matches('[').trim_end_matches(']');
let n = host.strip_prefix("eth")?;
// `eth` alone, `eth01`, `eth1x` — all refused as unindexable rather than
// silently read as 0. A leading zero is a different string, so treating it
// as index 1 would let one spelling pass a gate the other fails.
if n.is_empty() || (n.len() > 1 && n.starts_with('0')) || !n.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
n.parse::<u8>().ok().map(|i| (host.to_string(), i))
}
/// **Where a staged parameter file lands at RUNTIME on this medium typ.**
///
/// `ParamSpec::staged_file` is relative to the cell's staging tree, and the two
/// media graft that tree in two different places. This is the one place the
/// mapping is written down, so the gate compares the argv's path against the
/// same answer the writer produces rather than against a resemblance of it.
///
/// * **`Iso`** — `assets/appliance-root` is grafted at the INITRAMFS ROOT, so
/// `<staging>/data/admin.pub` arrives as `/data/admin.pub` and
/// `<staging>/admin.pub` arrives as `/admin.pub`. Both measured 2026-08-27:
/// the second is the shipped defect, the first is the fix.
/// * **`Installer`** — the staged file rides the ISO volume and the installer
/// PID 1 materializes it onto **P3** (or, for an xfs `/data`, the ESP's
/// `korp/data-stage/` that the first boot copies in), which the appliance
/// mounts at `/data`. So `admin.pub` arrives as `/data/admin.pub`, and a
/// nested path — the pairing seed's `pairing/seed.toml` — as
/// `/data/pairing/seed.toml` (korp-installer on edda
/// `agent/pairing-seed-stage` carries it on the flat volume under
/// `staged_volume_name`; before it, the composer refused a `/`).
///
/// Any other kind returns `None`: this function answers for the two media that
/// carry an appliance, and inventing an answer for the others is how a gate
/// starts agreeing with something nobody built.
pub fn staged_runtime_path(kind: ArtifactKind, staged_file: &str) -> Option<String> {
let rel = staged_file.trim().trim_start_matches('/');
if rel.is_empty() {
return None;
}
match kind {
ArtifactKind::Iso => Some(format!("/{rel}")),
ArtifactKind::Installer => Some(format!("/data/{rel}")),
_ => None,
}
}
/// **The gate.** An appliance variant must leave at least one administration
/// door open, and the door must be open in the DECLARATION rather than in the
/// argv's prose. See the module docs for both doors and for the medium that
/// passed the old textual rule while admitting nobody.
///
/// `kind` is the variant's typ, which decides where a staged file lands
/// ([`staged_runtime_path`]); `params` is the cell's declared parameter roster,
/// which decides whether that file is guaranteed to exist.
pub fn check_reachable(
variant: &str,
kind: ArtifactKind,
argv: &[String],
params: &[ParamSpec],
boot_params: &[String],
) -> Result<()> {
// ── Door A: the seeded key, and whether the declaration guarantees it ──
if let Some(value) = admin_key_argument(argv) {
let value = value.trim();
if value.is_empty() {
bail!(
"appliance variant '{variant}' declares `{ADMIN_KEY}` with no value — the flag \
takes an OpenSSH public key or `@<path>`. argv as declared: {argv:?}"
);
}
match value.strip_prefix('@') {
// A literal key in the argv is present unconditionally: whatever
// else is wrong with baking a credential into a cmdline, the door
// it opens is open. Not this gate's business.
None => return Ok(()),
Some(path) => {
let backing = params.iter().find(|p| {
p.required && staged_runtime_path(kind, &p.staged_file).as_deref() == Some(path)
});
if backing.is_none() {
let roster: Vec<String> = params
.iter()
.map(|p| {
format!(
"{} (required = {}, lands at {})",
p.name,
p.required,
staged_runtime_path(kind, &p.staged_file).unwrap_or_else(|| {
if p.boot_param.trim().is_empty() {
"nowhere".to_string()
} else {
format!("the cmdline as {}", p.boot_param)
}
})
)
})
.collect();
bail!(
"appliance variant '{variant}' reads its bootstrap admin from \
`{ADMIN_KEY} @{path}`, but NO declared parameter is `required = true` \
AND lands there — so a build that supplies nothing is legal and the \
medium it produces boots a server that dies on `reading the public key \
from {path}: No such file or directory` (MEASURED 2026-08-27 on the \
ISO published as v1.1.3). An argv literal is not a guarantee; a \
`required = true` parameter landing at that exact path is. Declared \
parameters of this {} cell: {}. argv as declared: {argv:?}",
kind.as_str(),
if roster.is_empty() {
"none".to_string()
} else {
roster.join(", ")
}
);
}
return Ok(());
}
}
}
// ── Door B: TOFU, which needs the listener OFF the loopback ────────────
if binds_control_plane_offbox(argv) {
// ...AND on an interface THIS MEDIUM ACTUALLY BRINGS UP.
//
// The gate above proves the bind is not loopback. It cannot prove the
// interface will have an address, and `BindSpec::resolve_with`
// deliberately refuses to fall back to a wildcard: an interface that is
// not there stops the boot ("a declaration that lies"), and one with no
// IPv4 lease is `NoAddress`. Either way a medium with no `--admin-key`
// authorizes nobody, forever — the defect this module exists to refuse,
// in a shape it could not see.
//
// **The count is read off `GUNNAR_NET`, which is the ONE declaration
// that owns it, and never off `target.nics`.** That distinction is not
// a detail: `target.nics` is what the CLOUD HANDS OUT — a floor — while
// `GUNNAR_NET` is what this medium configures, and the KVM rig already
// boots `max(preset, declared)` for exactly that reason.
// `installtest::firmware::declared_interface_count` says why a second
// copy would be wrong: "a `target.nics` field saying '2' would be a
// second place for the same fact to live, and the two would drift the
// first time somebody edited one." A gate built on the floor would
// refuse six correct cells and teach everyone to raise a preset to
// silence it, which is that drift arriving by another door.
//
// Every cell gunnar declares passes this today. It is a REGRESSION
// guard, not a fix — it fires when someone binds the control plane to
// an interface the medium never brings up, which is one edit away at
// any time and invisible until a boot.
// ...AND, on a PERSISTENT medium with no key, not on EVERY interface.
//
// `binds_a_wildcard` was written after `gunnar-installer-open` shipped
// `0.0.0.0:50051` for four days, and then had no caller: it was
// asserted only over synthetic argv in its own unit test, so nothing
// ever asked the question about a real cell. A predicate built from an
// incident and wired to nothing is the guard this file's own history
// warns about. This is its caller.
//
// The rule is the one this module already argues for, and the two
// media differ in exactly the property that decides it:
//
// * an **`Iso`** grafts its staging tree into the INITRAMFS ROOT — its
// whole state is RAM and dies with the boot. `gunnar-appliance-iso`
// declares `0.0.0.0:50051` deliberately: it brings up one interface,
// so there IS no private one to name, and the enrolment window lives
// as long as a demonstration. Allowed, and it must stay allowed or
// the live medium has no door at all.
// * an **`Installer`** materializes onto P3 and is a server on a
// customer's floor. There, an open enrolment window on every
// interface is "a race between the customer and whoever else finds
// the box" — and it is not hypothetical: this control plane answered
// `repo list` and `repo create` to an uncredentialled caller on the
// open internet, MEASURED 2026-08-31.
//
// Only the keyless case is refused, because only there does the
// wildcard decide who becomes gunnar-admin. A persistent medium that
// binds a wildcard AND seeds `--admin-key` has shut the window before
// anyone can reach it; that is a smaller and different question about
// exposing a control plane, and inventing a verdict on it here would
// be this gate answering something nobody asked it.
if kind == ArtifactKind::Installer && binds_a_wildcard(argv) {
bail!(
"appliance variant '{variant}' declares no `{ADMIN_KEY}` and binds its control \
plane to a WILDCARD (`{GRPC_BIND}` names 0.0.0.0, [::] or an empty host). This \
is a PERSISTENT medium — it is laid onto a customer's disk and outlives the \
boot — so an unauthenticated `Bootstrap.Enroll` on EVERY interface, including \
the public one, means the first caller to reach the box becomes gunnar-admin. \
MEASURED 2026-08-31: this control plane answered `repo list` and `repo create` \
to an uncredentialled caller on the open internet. Name the PRIVATE interface \
(`{GRPC_BIND} eth1:50051`, and bring it up in `{GUNNAR_NET}`), or seed the \
customer's key with `{ADMIN_KEY} @<path>` backed by a `required = true` \
parameter. A live `iso` cell, whose state is RAM and dies with the boot, is \
allowed a wildcard and is not what this refuses. argv as declared: {argv:?}"
);
}
if let Some((iface, index)) = bound_interface_index(argv) {
let brought_up = declared_interface_count(boot_params);
if u16::from(index) >= brought_up {
bail!(
"appliance variant '{variant}' has no `{ADMIN_KEY}`, so its gRPC listener is \
the ONLY door — and it binds `{GRPC_BIND} {iface}:…` while this medium's \
`{GUNNAR_NET}` brings up {brought_up} interface(s), so {iface} (index \
{index}) is never configured and never gets a lease. gunnar does NOT fall \
back to a wildcard: `BindSpec::resolve_with` answers `NoAddress` and the \
appliance would install, boot, listen on nothing reachable, and authorize \
NOBODY, forever (MEASURED 2026-08-31 on a one-NIC door: `no interface named \
\"eth1\"`, then the init panic). Name an interface `{GUNNAR_NET}` brings \
up, or bring that one up. Declared: {boot_params:?}"
);
}
}
return Ok(());
}
let why = if disables_control_plane(argv) {
format!("it declares `{NO_GRPC}`, so there is no control plane at all")
} else {
format!(
"the gRPC listener binds a LOOPBACK address (`{GRPC_BIND}` is absent or names \
127.0.0.1/::1, and its default is `127.0.0.1:50051`) — and gunnar offers no shell \
and no port forwarding, so nothing off the box can reach it"
)
};
bail!(
"appliance variant '{variant}' declares no `{ADMIN_KEY}` and {why}: the identity table \
on a freshly installed medium is EMPTY and `Bootstrap.Enroll` is unreachable — the \
appliance would boot, listen, and authorize NOBODY, forever (measured under KVM \
2026-08-25, and again on the published ISO 2026-08-27). Open ONE door: declare the \
customer's OpenSSH public key as a `required = true` parameter whose staged file lands \
at the path `{ADMIN_KEY} @<path>` names, or — knowing that it opens Bootstrap.Enroll to \
any caller with no credential until the first success — drop `{NO_GRPC}` AND bind the \
control plane off the loopback by naming the PRIVATE interface: `{GRPC_BIND} \
eth1:50051` — NOT `{GRPC_BIND} 0.0.0.0:50051`, because a wildcard binds the \
PUBLIC interface too and this variant has no `{ADMIN_KEY}`, so the enrolment \
window stands open to the whole internet and the first caller to reach it \
becomes gunnar-admin. \
argv as declared: {argv:?}"
);
}
#[cfg(test)]
mod tests {
use super::*;
fn v(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| s.to_string()).collect()
}
fn staged(name: &str, file: &str, required: bool) -> ParamSpec {
ParamSpec {
lands: nornir_build_thing::params::Landing::Data,
name: name.into(),
label: format!("the {name}"),
kind: nornir_build_thing::params::ParamKind::Text,
required,
boot_param: String::new(),
staged_file: file.into(),
}
}
/// Measured on the extractor itself: each of the three spellings must yield the
/// argv, and a gate reading only one would be blind to the other two. Drop
/// any arm of `app_argv` and exactly one of these fails.
#[test]
fn app_argv_is_total_over_all_three_spellings() {
assert_eq!(
app_argv(&[], &v(&["serve", "--no-grpc"])),
v(&["serve", "--no-grpc"]),
"the 0.1.7 `cmd` field"
);
assert_eq!(
app_argv(&v(&["korp.entry=gunnar", "korp.args=serve,--no-grpc"]), &[]),
v(&["serve", "--no-grpc"]),
"the comma-joined korp.args token"
);
assert_eq!(
app_argv(&v(&["korp.entry=gunnar", "--", "serve", "--no-grpc"]), &[]),
v(&["serve", "--no-grpc"]),
"boot_params after the `--` separator"
);
// A token that merely CONTAINS the words is not argv.
assert_eq!(
app_argv(&v(&["korp.env.GUNNAR_NOTE=serve--no-grpc"]), &[]),
Vec::<String>::new(),
"an env token is not the argv"
);
}
/// ★ THE RED THIS REWRITE EXISTS FOR — the argv published as v1.1.3, verbatim,
/// with the parameter roster that shipped beside it. The OLD rule
/// (`disables_control_plane && !declares_admin_key`) returned Ok on exactly
/// this input, which is how a medium that admits nobody reached a public
/// release page.
#[test]
fn the_published_isos_argv_and_roster_are_refused_by_name() {
let argv = app_argv(
&v(&[
"korp.entry=gunnar",
"--",
"serve",
"--no-grpc",
"--admin-key",
"@/data/admin.pub",
]),
&[],
);
// The old rule's verdict, asserted so this test fails loudly if anyone
// reintroduces it: the literal IS there, so text alone said "fine".
assert!(declares_admin_key(&argv), "the argv literal is present");
assert!(disables_control_plane(&argv));
let shipped = [staged("admin_key", "admin.pub", false)];
let err = check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&argv,
&shipped,
&[],
)
.expect_err("a medium whose admin key need not exist must not build");
let text = err.to_string();
assert!(
text.contains("gunnar-appliance-iso"),
"names the variant: {text}"
);
assert!(text.contains("/data/admin.pub"), "names the path: {text}");
assert!(text.contains("required = true"), "names the remedy: {text}");
assert!(
text.contains("admin_key (required = false, lands at /admin.pub)"),
"names the roster AND the wrong landing: {text}"
);
}
/// The I7 defect on its own: `required = true` is not enough if the file
/// lands one directory away from where the argv reads it. MEASURED — the ISO
/// staged `admin.pub` at the staging root (`/admin.pub`) while the argv read
/// `/data/admin.pub`, and the boot said `No such file or directory`.
#[test]
fn a_required_key_landing_one_directory_wrong_is_still_refused() {
let argv = v(&["serve", "--no-grpc", "--admin-key", "@/data/admin.pub"]);
let err = check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&argv,
&[staged("admin_key", "admin.pub", true)],
&[],
)
.expect_err("required, but landing at /admin.pub");
assert!(err.to_string().contains("/data/admin.pub"), "{err}");
// …and the SAME spec at the right relative path passes. Different input,
// not the same one asserted twice.
check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&argv,
&[staged("admin_key", "data/admin.pub", true)],
&[],
)
.expect("a required parameter landing at /data/admin.pub opens door A");
}
/// The two media graft their staging tree in two different places, so the
/// SAME `staged_file` is right on one and wrong on the other. A gate that
/// used one mapping for both would pass a broken medium half the time.
#[test]
fn the_two_media_land_the_same_staged_file_in_two_places() {
assert_eq!(
staged_runtime_path(ArtifactKind::Iso, "data/admin.pub").as_deref(),
Some("/data/admin.pub")
);
assert_eq!(
staged_runtime_path(ArtifactKind::Iso, "admin.pub").as_deref(),
Some("/admin.pub")
);
assert_eq!(
staged_runtime_path(ArtifactKind::Installer, "admin.pub").as_deref(),
Some("/data/admin.pub")
);
assert_eq!(
staged_runtime_path(ArtifactKind::Iso, " ").as_deref(),
None
);
assert_eq!(
staged_runtime_path(ArtifactKind::Exe, "admin.pub").as_deref(),
None
);
// The installer's flat name lands where the installer's argv reads it —
// which is why the shipped installer declaration is the green one.
check_reachable(
"gunnar-installer",
ArtifactKind::Installer,
&app_argv(
&v(&["korp.args=serve,--no-grpc,--admin-key,@/data/admin.pub"]),
&[],
),
&[staged("admin_key", "admin.pub", true)],
&[],
)
.expect("the installer's own declaration opens door A");
}
/// ★ DOOR B IS NOT "DROP `--no-grpc`". `--grpc-bind` defaults to
/// `127.0.0.1:50051` and gunnar has no shell and no port forwarding, so the
/// obvious fix ships a second medium that admits nobody. Every row here is a
/// DIFFERENT argv.
#[test]
fn tofu_needs_the_listener_off_the_loopback() {
// The naive fix: no --no-grpc, no --admin-key, default bind. REFUSED.
let err = check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&v(&["serve"]),
&[],
&[],
)
.expect_err("a loopback control plane on an appliance admits nobody");
assert!(err.to_string().contains("LOOPBACK"), "{err}");
assert!(err.to_string().contains("no shell"), "{err}");
// Spelled out as loopback: still refused, in both flag spellings.
for bind in ["127.0.0.1:50051", "localhost:50051", "[::1]:50051"] {
for argv in [
v(&["serve", "--grpc-bind", bind]),
v(&["serve", &format!("--grpc-bind={bind}")]),
] {
assert!(
check_reachable("gunnar-appliance-iso", ArtifactKind::Iso, &argv, &[], &[])
.is_err(),
"{argv:?} binds loopback"
);
}
}
// Off the loopback: door B is open, in both flag spellings.
for argv in [
v(&["serve", "--grpc-bind", "0.0.0.0:50051"]),
v(&["serve", "--grpc-bind=0.0.0.0:50051"]),
v(&["serve", "--grpc-bind", "[::]:50051"]),
v(&["serve", "--grpc-bind", "10.0.2.15:50051"]),
] {
check_reachable("gunnar-appliance-iso", ArtifactKind::Iso, &argv, &[], &[])
.unwrap_or_else(|e| panic!("{argv:?} is reachable: {e:#}"));
}
// And `--no-grpc` beats a public bind: there is no listener to reach.
assert!(check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&v(&["serve", "--no-grpc", "--grpc-bind", "0.0.0.0:50051"]),
&[],
&[],
)
.is_err());
}
/// **The interface spelling is door B, and it is the RIGHT door B.**
///
/// `eth1:50051` names an interface whose address is not known until the
/// box boots — the only value a medium mastered once and installed many
/// times can honestly carry. It must count as off-box, or the very
/// declaration that closes the hole would be refused by the gate that
/// exists to keep the door open.
#[test]
fn naming_a_private_interface_opens_door_b() {
// The medium must SAY it brings up the interface it binds — that is the
// arm below, and it is the reason this case carries boot params at all.
let net = v(&["korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp"]);
for argv in [
v(&["serve", "--grpc-bind", "eth1:50051"]),
v(&["serve", "--grpc-bind=eth1:50051"]),
v(&["serve", "--grpc-bind", "ens4:50051"]),
] {
check_reachable(
"gunnar-installer-open",
ArtifactKind::Installer,
&argv,
&[],
&net,
)
.unwrap_or_else(|e| panic!("{argv:?} is reachable: {e:#}"));
assert!(
binds_control_plane_offbox(&argv),
"{argv:?} must count as off the loopback"
);
}
}
/// **A PERSISTENT keyless medium may not open its door on every
/// interface; a LIVE one may.**
///
/// `binds_a_wildcard` existed for months with no caller — asserted only
/// over synthetic argv, so nothing ever asked the question about a real
/// cell, after `gunnar-installer-open` had already shipped `0.0.0.0` for
/// four days. All four combinations are driven here, because the rule is a
/// CONJUNCTION and a test of one half proves nothing about the other.
#[test]
fn a_persistent_keyless_medium_may_not_bind_every_interface() {
let wild = v(&["serve", "--grpc-bind", "0.0.0.0:50051"]);
let net = v(&["korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp"]);
// ① persistent + keyless -> REFUSED, and the refusal says both halves.
let err = check_reachable(
"gunnar-installer-open",
ArtifactKind::Installer,
&wild,
&[],
&net,
)
.expect_err("a persistent keyless medium must not bind a wildcard");
let text = err.to_string();
assert!(text.contains("WILDCARD"), "{text}");
assert!(
text.contains("PERSISTENT"),
"says WHY this cell and not another: {text}"
);
assert!(text.contains("2026-08-31"), "cites the measurement: {text}");
// ② LIVE + keyless -> allowed. `gunnar-appliance-iso` declares exactly
// this and must keep working: it brings up ONE interface, so there
// is no private one to name, and its state dies with the boot.
check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&wild,
&[],
&v(&["GUNNAR_NET=eth0:dhcp"]),
)
.expect("a live RAM-state medium is allowed the wildcard it needs");
// ③ persistent + KEYED -> allowed. The window is shut before anyone
// can reach it; exposing the control plane is a different question
// and this gate does not answer it.
let keyed = v(&[
"serve",
"--grpc-bind",
"0.0.0.0:50051",
"--admin-key",
"@/data/admin.pub",
]);
check_reachable(
"gunnar-installer",
ArtifactKind::Installer,
&keyed,
&[staged("admin_key", "admin.pub", true)],
&net,
)
.expect("a seeded key shuts the window, so the wildcard is not what decides admin");
// ④ persistent + keyless + a NAMED interface -> allowed, which is the
// remedy the refusal names. Without this the guard could be passing
// for the wrong reason (refusing every keyless installer).
check_reachable(
"gunnar-installer-open",
ArtifactKind::Installer,
&v(&["serve", "--grpc-bind", "eth1:50051"]),
&[],
&net,
)
.expect("naming the private interface is the way through");
}
/// **A door on an interface the medium never brings up is a shut door.**
///
/// The count comes off `GUNNAR_NET` — the one declaration that owns it —
/// and never off `target.nics`, which is what the CLOUD hands out. Both
/// halves are driven here: the same argv passes when the medium configures
/// the interface and is refused when it does not, so the arm cannot be
/// satisfied by the argv alone.
#[test]
fn binding_an_interface_the_medium_never_brings_up_is_refused() {
let argv = v(&["serve", "--grpc-bind", "eth1:50051"]);
// Configured -> open.
check_reachable(
"gunnar-installer-open",
ArtifactKind::Installer,
&argv,
&[],
&v(&["korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp"]),
)
.expect("a medium that brings up eth1 may bind eth1");
// Same argv, medium brings up ONE interface -> refused, by name.
for net in [
v(&["korp.env.GUNNAR_NET=eth0:dhcp"]),
// No GUNNAR_NET at all reads as one interface, never zero.
v(&["console=ttyS0"]),
] {
let err = check_reachable(
"gunnar-installer-open",
ArtifactKind::Installer,
&argv,
&[],
&net,
)
.expect_err("eth1 is not configured by {net:?}");
let text = err.to_string();
assert!(text.contains("eth1"), "names the interface: {text}");
assert!(
text.contains("1 interface(s)"),
"names what IS brought up: {text}"
);
assert!(
text.contains("authorize \nNOBODY") || text.contains("NOBODY"),
"says what the medium becomes: {text}"
);
}
}
/// **The gate refuses only names it can index.** `ens5` and `enp0s3` encode
/// a bus address, not an ordinal, so there is no honest mapping from the
/// name to "the medium must bring up N interfaces". Guessing one would
/// refuse correct declarations, so those pass untouched — and that is a
/// deliberate hole, stated rather than hidden.
#[test]
fn only_eth_n_names_carry_an_index() {
assert_eq!(
bound_interface_index(&v(&["serve", "--grpc-bind", "eth1:50051"])),
Some(("eth1".to_string(), 1))
);
assert_eq!(
bound_interface_index(&v(&["serve", "--grpc-bind=eth0:50051"])),
Some(("eth0".to_string(), 0))
);
for unindexable in [
"ens5:50051",
"enp0s3:50051",
"wg0:50051",
"eth:50051",
// A leading zero is a DIFFERENT string; reading it as 1 would let
// one spelling pass a gate the other fails.
"eth01:50051",
"eth1x:50051",
"10.0.0.5:50051",
] {
assert_eq!(
bound_interface_index(&v(&["serve", "--grpc-bind", unindexable])),
None,
"{unindexable} carries no index this gate may act on"
);
}
// `--no-grpc` means there is no listener at all, so there is no
// interface to check and no verdict to give.
assert_eq!(
bound_interface_index(&v(&["serve", "--no-grpc", "--grpc-bind", "eth1:50051"])),
None
);
}
/// **The interface count is read off `GUNNAR_NET` and nothing else.**
#[test]
fn the_interface_count_comes_off_gunnar_net() {
assert_eq!(
declared_interface_count(&v(&["korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp"])),
2
);
assert_eq!(
declared_interface_count(&v(&["korp.env.GUNNAR_NET=eth0:dhcp"])),
1
);
assert_eq!(
declared_interface_count(&v(&["korp.env.GUNNAR_NET=eth0:1.2.3.4:1.2.3.1,eth1:dhcp"])),
2
);
// Absent, empty, and trailing-comma spellings all read as one — never
// zero, which would make every bind refusable.
assert_eq!(declared_interface_count(&v(&["console=ttyS0"])), 1);
assert_eq!(declared_interface_count(&v(&["korp.env.GUNNAR_NET="])), 1);
assert_eq!(
declared_interface_count(&v(&["korp.env.GUNNAR_NET=eth0:dhcp,"])),
1
);
assert_eq!(declared_interface_count(&[]), 1);
}
/// **The predicate that can tell `eth1` from `0.0.0.0`.** Both are "off the
/// loopback"; only one of them is on the public interface. Until this
/// existed there was no way to ask, which is how `gunnar-installer-open`
/// shipped a wildcard.
#[test]
fn the_wildcard_predicate_separates_everywhere_from_somewhere() {
for wild in [
v(&["serve", "--grpc-bind", "0.0.0.0:50051"]),
v(&["serve", "--grpc-bind=0.0.0.0:50051"]),
v(&["serve", "--grpc-bind", "[::]:50051"]),
] {
assert!(binds_a_wildcard(&wild), "{wild:?} IS a wildcard");
}
for narrow in [
v(&["serve", "--grpc-bind", "eth1:50051"]),
v(&["serve", "--grpc-bind", "203.0.113.152:50051"]),
v(&["serve", "--grpc-bind", "127.0.0.1:50051"]),
v(&["serve"]),
v(&["serve", "--no-grpc", "--grpc-bind", "0.0.0.0:50051"]),
] {
assert!(!binds_a_wildcard(&narrow), "{narrow:?} is NOT a wildcard");
}
}
/// **The shipped declaration, by name.** `gunnar-installer-open` is the
/// medium the reference appliance is installed from, and this is the assertion
/// that goes red the day somebody puts the wildcard back.
#[test]
fn the_shipped_keyless_installer_names_an_interface_and_not_a_wildcard() {
let argv = app_argv(&v(&[crate::recipe::OPEN_KORP_ARGS]), &[]);
assert!(
binds_control_plane_offbox(&argv),
"the keyless installer must be reachable off the box: {argv:?}"
);
assert!(
!binds_a_wildcard(&argv),
"the keyless installer must NOT bind every interface — that is an anonymous admin \
surface on the public internet: {argv:?}"
);
assert_eq!(
flag_value(&argv, GRPC_BIND),
Some("utility:50051"),
"the private interface, by its ROLE (the lean estate, A19 row 17): eth1 on a box \
with two NICs, eth0 on a box laid behind its front with its utility NIC alone"
);
assert_eq!(
bound_interface_index(&argv),
None,
"a role names no index: it is always a NIC the spec brings up"
);
}
/// A literal key in the argv is a credential that is unconditionally there.
/// Both clap spellings, and neither needs a parameter behind it.
#[test]
fn a_literal_admin_key_needs_no_parameter() {
for argv in [
v(&["serve", "--no-grpc", "--admin-key", "ssh-ed25519 AAAAC3Nz…"]),
v(&["serve", "--no-grpc", "--admin-key=ssh-ed25519 AAAAC3Nz…"]),
] {
check_reachable("gunnar-appliance-iso", ArtifactKind::Iso, &argv, &[], &[])
.unwrap_or_else(|e| panic!("{argv:?} is reachable: {e:#}"));
}
}
/// A flag with nothing after it is a declaration bug, not "no admin key" —
/// refused by name rather than falling through to door B's message.
#[test]
fn a_dangling_admin_key_flag_is_refused_as_itself() {
let err = check_reachable(
"gunnar-appliance-iso",
ArtifactKind::Iso,
&v(&["serve", "--no-grpc", "--admin-key"]),
&[],
&[],
)
.expect_err("no value");
assert!(err.to_string().contains("with no value"), "{err}");
}
}