Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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}");
    }
}