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

nordisk/gunnar

crates/gunnar-cli/tests/bootstrap.rs

//! **Trust on first use, proved on served behaviour.**
//!
//! Every claim in here is asserted against a real `gunnar serve` on a real data
//! path, over the real gRPC transport and the real SSH transport, with the
//! **stock `git` binary** as the client. Nothing inspects the identity table to
//! decide whether a test passed: a test that only read the table would prove
//! nothing about the door, which is the whole point of the door.
//!
//! ```text
//!   1  a fresh data path, no identities   → Bootstrap.Status says window_open
//!   2  Bootstrap.Enroll, no credential    → one gunnar-admin, window shut
//!   3  that user pushes to <name>/<repo>  → accepted
//!   4  …and to somebody else's account    → refused
//!   5  a second Enroll                    → FAILED_PRECONDITION
//!   6  delete every identity, Enroll      → STILL FAILED_PRECONDITION
//!   7  SIGKILL the process, restart       → all of it survives
//! ```
//!
//! The two ed25519 keys below are **fixture data**. They are the real public
//! halves of two working keys, carried here because a test that enrolled a
//! freshly minted key would never notice a bug in how a real one is parsed and
//! fingerprinted. They authorise nothing anywhere: this file only ever puts
//! them into a temp directory it deletes.
//!
//! `clippy::result_large_err` is allowed for the reason `gunnar-grpc`'s own
//! modules allow it: `tonic::Status` is a large error type by design, and this
//! file's job is to return the server's real status codes rather than to map
//! them into something smaller and lose the distinction it is testing.
#![allow(clippy::result_large_err)]

mod common;

use std::path::Path;
use std::sync::Arc;

use gunnar_cli::server::{ServeConfig, Server, ServerStores};
use gunnar_core::{HashKind, Level, Principal, PrincipalKind, RepoRef, Visibility};
use gunnar_proto::bootstrap_client::BootstrapClient;
use gunnar_proto::{BootstrapStatusRequest, EnrollRequest};

/// The first fixture user's laptop key. Public half; a public key is not a secret.
const ALICE_PUB: &str =
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN/xwBtUndMAcVY53UIcZacYaiZR3Ij62q0Gc2w1hcuj user@host";
/// The second fixture user's key, the second identity in the same story.
const BOB_PUB: &str =
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN9ZtPXJv8cvroO3mfgbSGCZUaLMBF8+w9t28ACtdXrt bob";

// ───────────────────────────────────────────────────────────────────────────
// A server with NOTHING in it
// ───────────────────────────────────────────────────────────────────────────

/// A running gunnar on a data path the caller owns, with **no identity at
/// all**.
///
/// Deliberately not `common::Harness`: that one provisions a principal, a key
/// and two grants before it binds, which is exactly the state this file has to
/// start without. A server that had been provisioned would report the window
/// shut for the wrong reason and every assertion below would be vacuous.
struct Fresh {
    runtime: tokio::runtime::Runtime,
    server: Option<Server>,
    stores: Arc<ServerStores>,
    grpc: std::net::SocketAddr,
    ssh_port: u16,
}

impl Fresh {
    fn start(root: &Path) -> Fresh {
        Fresh::start_as(root, false)
    }

    /// `twin_role`: started `--role twin`.
    fn start_as(root: &Path, twin_role: bool) -> Fresh {
        Fresh::start_on(root, twin_role, None)
    }

    /// `ssh_bind`: a NAMED git port to ask for, rather than "any port".
    ///
    /// It matters for one assertion and only that one: `serving_git` compares
    /// the address the listener was asked for with the address it took, and
    /// `ServeConfig::ephemeral`'s `127.0.0.1:0` is a request a twin's
    /// loopback-only bind satisfies by accident. An operator's appliance asks
    /// for `0.0.0.0:2222`; a test that asks for nothing in particular cannot
    /// observe being given something else.
    fn start_on(root: &Path, twin_role: bool, ssh_bind: Option<std::net::SocketAddr>) -> Fresh {
        if std::env::var_os("RUST_LOG").is_some() {
            gunnar_cli::init_tracing(false);
        }
        let stores = Arc::new(ServerStores::open(root).expect("open the stores"));
        let runtime = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("a tokio runtime");
        let ephemeral = ServeConfig::ephemeral();
        let server = runtime
            .block_on(Server::start(
                Arc::clone(&stores),
                ServeConfig {
                    // The listener that matters here. `Bootstrap` is on it.
                    no_grpc: false,
                    twin_role,
                    ssh_bind: ssh_bind.unwrap_or(ephemeral.ssh_bind),
                    ..ephemeral
                },
            ))
            .expect("a server with no identities must START; refusing to start is the defect");
        let grpc = server
            .grpc_addr()
            .expect("the control plane bound, or nobody could ever enrol");
        let ssh_port = server.ssh_addr().port();
        Fresh {
            runtime,
            server: Some(server),
            stores,
            grpc,
            ssh_port,
        }
    }

    fn client(&self) -> BootstrapClient<tonic::transport::Channel> {
        let url = format!("http://{}", self.grpc);
        let channel = self
            .runtime
            .block_on(
                tonic::transport::Channel::from_shared(url)
                    .expect("a well-formed endpoint")
                    .connect(),
            )
            .expect("connect to the control plane");
        BootstrapClient::new(channel)
    }

    /// `Bootstrap.Status`, over the wire, with **no credential at all**.
    fn status(&self) -> gunnar_proto::BootstrapStatusResponse {
        let mut client = self.client();
        self.runtime
            .block_on(client.status(BootstrapStatusRequest {}))
            .expect("Status is servable without a credential")
            .into_inner()
    }

    /// `Bootstrap.Enroll`, over the wire, with **no credential at all**.
    fn enroll(&self, name: &str, key: &str) -> Result<gunnar_proto::EnrollResponse, tonic::Status> {
        let mut client = self.client();
        self.runtime
            .block_on(client.enroll(EnrollRequest {
                name: name.to_owned(),
                openssh_public_key: key.to_owned(),
            }))
            .map(|r| r.into_inner())
    }

    fn shutdown(mut self) {
        if let Some(server) = self.server.take() {
            self.runtime.block_on(server.shutdown()).expect("shutdown");
        }
    }
}

impl Drop for Fresh {
    fn drop(&mut self) {
        if let Some(server) = self.server.take() {
            let _ = self.runtime.block_on(server.shutdown());
        }
    }
}

// ───────────────────────────────────────────────────────────────────────────
// 1 + 2: the exact call a first-time user makes
// ───────────────────────────────────────────────────────────────────────────

#[test]
fn an_unenrolled_server_starts_says_so_and_enrols_without_a_credential() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let fresh = Fresh::start(&dir.path().join("data"));

    // ── it SAYS SO, over the wire, before anybody has a credential ─────────
    let before = fresh.status();
    assert!(
        before.window_open,
        "a server nothing has ever been enrolled on must report window_open, or an operator has \
         to guess whether their box is standing wide open"
    );
    assert!(!before.bootstrapped);
    assert_eq!((before.principals, before.keys), (0, 0));
    assert!(
        before.detail.contains("NO IDENTITY HAS EVER BEEN ENROLLED"),
        "the status has to say it in words a human reads, not only in a bool: {:?}",
        before.detail
    );

    // ── the call ───────────────────────────────────────────────────────────
    let enrolled = fresh
        .enroll("alice", ALICE_PUB)
        .expect("the first enrolment needs no credential");
    let principal = enrolled.principal.expect("a principal came back");
    assert_eq!(principal.id, "alice");
    assert_eq!(
        enrolled.account, "alice",
        "the user owns the account named after them: <host>/<account>/<repo>"
    );
    let key = enrolled.key.expect("a key came back");
    assert!(
        key.fingerprint.starts_with("SHA256:"),
        "the server derives the fingerprint itself: {:?}",
        key.fingerprint
    );
    assert!(
        enrolled.bootstrapped_at_unix_ms > 0,
        "the moment the door shut is recorded, not left at zero"
    );

    // ── and the window is shut, over the wire, immediately ─────────────────
    let after = fresh.status();
    assert!(
        !after.window_open,
        "the window must close on the first success"
    );
    assert!(after.bootstrapped);
    assert_eq!(after.bootstrapped_principal, "alice");
    // Two credentials, one principal: the enrolled ssh key AND the client
    // certificate the appliance mints for the bootstrap admin (c25257f9). Both
    // land in the key table, which is what `keys` counts.
    assert_eq!((after.principals, after.keys), (1, 2));

    // ── 5: a second call is refused ────────────────────────────────────────
    let second = fresh
        .enroll("bob", BOB_PUB)
        .expect_err("a second enrolment must be refused");
    assert_eq!(second.code(), tonic::Code::FailedPrecondition);
    assert!(
        second.message().contains("bootstrapped"),
        "and it must say why: {:?}",
        second.message()
    );
    assert_eq!(
        fresh.status().keys,
        2,
        "the refused call must not have installed anything on its way out; the two credentials \
         are the winner's ssh key and bootstrap certificate"
    );

    // The gunnar-admin flag is a real Server/Admin grant, reachable by the
    // ordinary ACL, not a special case.
    assert!(
        fresh
            .stores
            .auth()
            .may_server("alice", Level::Admin)
            .expect("acl"),
        "the first user is gunnar-admin"
    );
    fresh.shutdown();
}

/// **Enrolment attempted with an identity already present is refused**, even
/// when no bootstrap record exists — the upgrade case, which is a different
/// refusal and has to be seen separately.
#[test]
fn a_server_that_already_holds_identities_refuses_to_be_bootstrapped() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let root = dir.path().join("data");
    {
        // Provision a key the way `gunnar key add` does, offline, and write no
        // bootstrap record at all.
        let stores = ServerStores::open(root.clone()).expect("open");
        stores
            .auth()
            .add_principal(Principal::new("existing", "existing", PrincipalKind::User).expect("ok"))
            .expect("add");
        stores
            .auth()
            .register_key("existing", BOB_PUB, None)
            .expect("register");
    }
    let fresh = Fresh::start(&root);
    let status = fresh.status();
    assert!(
        !status.window_open,
        "a table with identities in it is not a fresh server, record or no record"
    );
    assert!(
        !status.bootstrapped,
        "…and it honestly reports that no bootstrap record exists, rather than inventing one"
    );
    let refused = fresh
        .enroll("alice", ALICE_PUB)
        .expect_err("enrolment against a provisioned table must be refused");
    assert_eq!(refused.code(), tonic::Code::FailedPrecondition);
    assert!(
        refused.message().contains("not a fresh server"),
        "the two refusals must be distinguishable: {:?}",
        refused.message()
    );
    fresh.shutdown();
}

// ───────────────────────────────────────────────────────────────────────────
// The concurrency boundary
// ───────────────────────────────────────────────────────────────────────────

/// **Two enrolments racing: exactly one wins.**
///
/// Driven, not reasoned about. Both calls go over the real transport from real
/// OS threads onto a server whose control plane has its own runtime, so they
/// genuinely overlap.
#[test]
fn two_concurrent_enrolments_produce_exactly_one_admin() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let fresh = Fresh::start(&dir.path().join("data"));
    let url = format!("http://{}", fresh.grpc);

    let barrier = Arc::new(std::sync::Barrier::new(2));
    let mut handles = Vec::new();
    for (name, key) in [("alice", ALICE_PUB), ("bob", BOB_PUB)] {
        let url = url.clone();
        let barrier = Arc::clone(&barrier);
        handles.push(std::thread::spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("a runtime");
            rt.block_on(async move {
                let channel = tonic::transport::Channel::from_shared(url)
                    .expect("a well-formed endpoint")
                    .connect()
                    .await
                    .expect("connect");
                let mut client = BootstrapClient::new(channel);
                // Connected first, so the barrier releases into the RPC itself
                // and not into a TCP handshake.
                barrier.wait();
                client
                    .enroll(EnrollRequest {
                        name: name.to_owned(),
                        openssh_public_key: key.to_owned(),
                    })
                    .await
                    .map(|r| r.into_inner().account)
                    .map_err(|s| (s.code(), s.message().to_owned()))
            })
        }));
    }
    let outcomes: Vec<_> = handles
        .into_iter()
        .map(|h| h.join().expect("joined"))
        .collect();
    let winners: Vec<_> = outcomes.iter().filter_map(|o| o.as_ref().ok()).collect();
    let losers: Vec<_> = outcomes.iter().filter_map(|o| o.as_ref().err()).collect();
    assert_eq!(
        winners.len(),
        1,
        "exactly one of two concurrent enrolments must win; got {outcomes:?}"
    );
    assert_eq!(losers.len(), 1);
    assert_eq!(
        losers[0].0,
        tonic::Code::FailedPrecondition,
        "the loser must be refused as a closed window, not as an internal error: {:?}",
        losers[0]
    );

    // And the SERVER agrees: one principal, one admin, holding the winner's
    // ssh key plus the bootstrap certificate.
    let status = fresh.status();
    assert_eq!((status.principals, status.keys), (1, 2));
    assert_eq!(&status.bootstrapped_principal, winners[0]);
    assert_eq!(
        fresh.stores.auth().principals().expect("read").len(),
        1,
        "a second principal would mean both calls partly landed, which is the whole failure"
    );
    fresh.shutdown();
}

// ───────────────────────────────────────────────────────────────────────────
// Persistence: the process is KILLED, not asked nicely
// ───────────────────────────────────────────────────────────────────────────

/// **Everything the ACL knows survives, and it survives a SIGKILL.**
///
/// The three facts, each read back from a *different process's* stores after
/// the first one was killed: the user and its key, the account, and the
/// gunnar-admin flag. Plus the fourth, which is the one this design turns on:
/// bootstrap-has-happened.
///
/// `SIGKILL` and not a clean shutdown: `Drop` does not run, no shutdown hook
/// runs, and nothing gets a chance to flush. If any of this depended on a
/// graceful exit it would not be persistence.
#[test]
fn every_acl_fact_survives_a_sigkill_and_a_restart() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let root = dir.path().join("data");

    // ── boot 1: enrol, add a second user, hand out gunnar-admin ───────────
    {
        let fresh = Fresh::start(&root);
        fresh.enroll("alice", ALICE_PUB).expect("enrol");
        // Ordinary authenticated administration, through the SAME verbs an
        // operator uses. Driven in-process against the live stores, which is
        // what `Access.AddPrincipal` reaches.
        let plane = gunnar_cli::server::CoreControlPlane::new(Arc::clone(&fresh.stores));
        use gunnar_grpc::ControlPlane as _;
        plane
            .add_principal("bob", "bob", gunnar_proto::PrincipalKind::User)
            .expect("the gunnar-admin creates a second user");
        plane
            .add_key("bob", BOB_PUB, "", None, "", None)
            .expect("with a key");
        plane
            .grant("bob", "", "", gunnar_proto::GrantLevel::Admin)
            .expect("and gunnar-admin, which any gunnar-admin may hand to anybody");
        fresh.shutdown();
    }

    // ── the kill ───────────────────────────────────────────────────────────
    //
    // A whole process, so there is a real SIGKILL and a real un-run `Drop`.
    // `gunnar serve --check` would exit cleanly; this one is killed mid-serve.
    let killed = kill_a_serving_process(&root);
    assert!(
        killed,
        "the test must actually have SIGKILLed a serving gunnar; a skipped kill would make \
         every assertion below prove only that a clean shutdown works"
    );

    // ── boot 2: a brand-new process against the same data path ────────────
    let fresh = Fresh::start(&root);
    let status = fresh.status();
    assert!(
        !status.window_open,
        "the door is still shut after a SIGKILL"
    );
    assert!(status.bootstrapped);
    assert_eq!(status.bootstrapped_principal, "alice");
    assert_eq!(
        (status.principals, status.keys),
        (2, 3),
        "both users and all three credentials survived: alice's ssh key, alice's bootstrap \
         certificate, bob's ssh key"
    );

    let auth = fresh.stores.auth();
    // users + keys
    assert!(auth.principal("alice").expect("read").is_some());
    let bobs = auth.keys_of("bob").expect("read");
    assert_eq!(bobs.len(), 1, "bob's key survived");
    // accounts — intrinsic, and it is a namespace that really exists
    assert!(auth.may_namespace("bob", "bob", Level::Admin).expect("acl"));
    // alice is the box's first identity (bootstrap enrolled her): her
    // namespace is the estate's own, not a customer's account, so the
    // 2026-10-05 shield does not stand between it and a server grant.
    assert!(
        auth.may_namespace("bob", "alice", Level::Admin)
            .expect("acl"),
        "bob reaches alice's account because the gunnar-admin grant SURVIVED, not because \
         of the account rule"
    );
    assert!(
        !auth
            .may_namespace("stranger", "alice", Level::Read)
            .expect("acl"),
        "…and an unknown principal reaches nothing, which is what keeps the line above from          being an ACL that says yes to everybody"
    );
    assert!(
        fresh
            .stores
            .catalog()
            .namespaces()
            .expect("read")
            .iter()
            .any(|ns| ns.name == "bob"),
        "the account namespace was created with the user and survived"
    );
    // the gunnar-admin flag, on BOTH
    assert!(auth.may_server("alice", Level::Admin).expect("acl"));
    assert!(auth.may_server("bob", Level::Admin).expect("acl"));
    // and the record itself
    let record = auth.bootstrap_record().expect("read").expect("a record");
    assert_eq!(record.principal_id, "alice");
    assert_eq!(record.account, "alice");

    let refused = fresh
        .enroll("mallory", BOB_PUB)
        .expect_err("and the window is still shut across the restart");
    assert_eq!(refused.code(), tonic::Code::FailedPrecondition);
    fresh.shutdown();
}

/// **The attack, against the real files.** Delete every identity on disk,
/// restart, and try again.
#[test]
fn deleting_every_identity_on_disk_does_not_reopen_the_door() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let root = dir.path().join("data");
    {
        let fresh = Fresh::start(&root);
        fresh.enroll("alice", ALICE_PUB).expect("enrol");
        fresh.shutdown();
    }
    // Offline, with the server down: exactly what an attacker with the data
    // path, or an operator with `gunnar principal remove`, can do.
    {
        let stores = ServerStores::open(root.clone()).expect("open");
        stores.auth().remove_principal("alice").expect("remove");
        assert!(
            stores.auth().is_empty().expect("read"),
            "the identity table really is empty on disk now"
        );
    }
    let fresh = Fresh::start(&root);
    let status = fresh.status();
    assert!(
        !status.window_open,
        "wiping the identity table must NOT reopen the enrolment window"
    );
    assert!(status.bootstrapped, "the record outlived the identities");
    assert_eq!((status.principals, status.keys), (0, 0));
    assert!(
        status.detail.contains("nobody can authenticate"),
        "and the server says what state it is in: {:?}",
        status.detail
    );
    let refused = fresh
        .enroll("mallory", BOB_PUB)
        .expect_err("and enrolment is still refused");
    assert_eq!(refused.code(), tonic::Code::FailedPrecondition);
    fresh.shutdown();
}

/// **A data path that cannot be read is not an empty one.**
///
/// The single worst failure this design can have is a corrupt or unreadable
/// identity document being served as "nobody has enrolled here yet". Corrupt
/// the blob the auth reference points at and assert the server says
/// `UNAVAILABLE` rather than opening the door.
#[test]
fn an_unreadable_identity_table_is_never_served_as_an_open_window() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let root = dir.path().join("data");
    {
        let fresh = Fresh::start(&root);
        fresh.enroll("alice", ALICE_PUB).expect("enrol");
        fresh.shutdown();
    }
    // Point the identity document at bytes that are not a document. The
    // document exists, so this is "unreadable", not "absent".
    //
    // Through the control store's own compare-and-swap (certificate identity moved this
    // document out of `control.git`), so it is a write another writer could
    // have made rather than a hand-edited file the code never produces.
    let db = root.join(gunnar_cli::server::CONTROL_DB);
    assert!(
        db.exists(),
        "the control store must be a real file on the data path: {}",
        db.display()
    );
    {
        let store = gunnar_core::open_control_store(&db).expect("open the control store");
        let before = store
            .read_doc(gunnar_core::AUTH_REF)
            .expect("read the identity document");
        assert!(
            before.is_some(),
            "the identity table must have been written by the enrolment above"
        );
        let junk = store
            .put_blob(b"this is not an identity table")
            .expect("store the junk");
        assert!(store
            .commit_doc(gunnar_core::AUTH_REF, before, junk)
            .expect("commit the junk"));
    }

    let stores = ServerStores::open(root.clone()).expect("the stores still open");
    let err = stores
        .auth()
        .bootstrap_window_open()
        .expect_err("an unreadable identity table must be an ERROR, never `open`");
    let text = format!("{err}");
    assert!(
        !text.is_empty(),
        "and it must name what went wrong rather than being a bare unit"
    );
    // And the server refuses to start rather than starting with the door open.
    let started = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("runtime");
        rt.block_on(Server::start(
            Arc::new(stores),
            ServeConfig {
                no_grpc: false,
                ..ServeConfig::ephemeral()
            },
        ))
        .is_ok()
    }));
    assert!(
        matches!(started, Ok(false)),
        "a server whose identity table cannot be read must refuse to start, not start with the \
         enrolment window open"
    );
}

// ───────────────────────────────────────────────────────────────────────────
// The proof that matters: real git, real SSH, real accounts
// ───────────────────────────────────────────────────────────────────────────

/// **The enrolled user pushes to their own account and is refused on
/// somebody else's.**
///
/// Over stock `git` and the real SSH listener, with the key that was enrolled
/// over gRPC. A test that read the grant table instead would prove nothing
/// about what the server serves.
#[test]
fn the_enrolled_user_pushes_to_their_own_account_and_not_to_another() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let root = dir.path().join("data");

    // The private half of a key we then enrol the public half of. Minted, so
    // this test carries no private material.
    let private = gunnar_core::mint_ed25519().expect("mint");
    let parsed =
        gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("the minted key parses");
    let public = parsed.public_key().to_openssh().expect("render");
    let key_path = dir.path().join("id_ed25519");
    std::fs::write(&key_path, &private).expect("write");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt as _;
        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
            .expect("ssh refuses a world-readable private key");
    }

    let fresh = Fresh::start(&root);
    fresh.enroll("alice", &public).expect("enrol over gRPC");

    // alice creates a repository IN HIS OWN ACCOUNT. He is admin of it by the
    // intrinsic rule; the namespace was created with him.
    let own = RepoRef::new("alice", "hello").expect("path");
    let created = fresh
        .stores
        .catalog()
        .create(
            gunnar_core::NewRepo::new(own.clone())
                .hash_kind(HashKind::Sha256)
                .visibility(Visibility::Private),
        )
        .expect("create in his own account");
    fresh.stores.repo_store(&created).expect("materialise");

    // Somebody else's account, which he must not reach. Created directly, the
    // way a second enrolled user's would be.
    fresh
        .stores
        .auth()
        .add_principal(Principal::new("bob", "bob", PrincipalKind::User).expect("ok"))
        .expect("add");
    fresh
        .stores
        .catalog()
        .create_namespace("bob")
        .expect("bob's account");
    let theirs = RepoRef::new("bob", "hello").expect("path");
    let other = fresh
        .stores
        .catalog()
        .create(
            gunnar_core::NewRepo::new(theirs)
                .hash_kind(HashKind::Sha256)
                .visibility(Visibility::Private),
        )
        .expect("create in bob's account");
    fresh.stores.repo_store(&other).expect("materialise");

    // alice is gunnar-admin, which WOULD let him reach bob's namespace.
    // Take that away, so what is being measured is the ACCOUNT rule and not the
    // server grant. Without this the "refused" assertion below could never fire
    // and the whole test would be an identity value.
    fresh
        .stores
        .auth()
        .revoke_grant("alice", &gunnar_core::Scope::Server)
        .expect("revoke the server grant");

    let work = dir.path().join("work");
    std::fs::create_dir_all(&work).expect("mkdir");
    let ssh = format!(
        "ssh -i {} -o IdentitiesOnly=yes -o IdentityAgent=none -o StrictHostKeyChecking=no \
         -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o LogLevel=ERROR",
        key_path.display()
    );
    let git = |args: &[&str]| {
        let out = std::process::Command::new("git")
            .current_dir(&work)
            .env("GIT_SSH_COMMAND", &ssh)
            .env("GIT_TERMINAL_PROMPT", "0")
            .env("GIT_CONFIG_NOSYSTEM", "1")
            .env("HOME", dir.path())
            .env("GIT_AUTHOR_NAME", "Gunnar Test")
            .env("GIT_AUTHOR_EMAIL", "gunnar@example.invalid")
            .env("GIT_COMMITTER_NAME", "Gunnar Test")
            .env("GIT_COMMITTER_EMAIL", "gunnar@example.invalid")
            .args(args)
            .output()
            .expect("git is on PATH");
        (
            out.status.success(),
            String::from_utf8_lossy(&out.stderr).into_owned(),
        )
    };
    // sha256, because a new gunnar repository is sha256.
    assert!(git(&["init", "--object-format=sha256", "."]).0);
    std::fs::write(work.join("README"), "gunnar\n").expect("write");
    assert!(git(&["add", "README"]).0);
    assert!(git(&["commit", "-m", "first"]).0);

    let mine = format!("ssh://git@127.0.0.1:{}/alice/hello.git", fresh.ssh_port);
    let theirs = format!("ssh://git@127.0.0.1:{}/bob/hello.git", fresh.ssh_port);

    // ── his own account: ACCEPTED ─────────────────────────────────────────
    let (ok, err) = git(&["push", &mine, "HEAD:refs/heads/main"]);
    assert!(
        ok,
        "the enrolled user must be able to push to their own account with no grant at all, \
         because account admin is intrinsic: {err}"
    );

    // ── somebody else's account: REFUSED ──────────────────────────────────
    let (ok, err) = git(&["push", &theirs, "HEAD:refs/heads/main"]);
    assert!(
        !ok,
        "the enrolled user must NOT be able to push into another account's namespace; the push \
         was accepted"
    );
    assert!(
        err.to_lowercase().contains("denied")
            || err.to_lowercase().contains("permission")
            || err.to_lowercase().contains("access"),
        "and the refusal must say so: {err}"
    );
    fresh.shutdown();
}

// ───────────────────────────────────────────────────────────────────────────
// The SIGKILL, as a real process
// ───────────────────────────────────────────────────────────────────────────

/// Start `gunnar serve` on `root` as a **child process**, wait until it is
/// listening, then `SIGKILL` it. `true` when a process really was killed.
///
/// Not `Server::shutdown`: this file's whole claim is that persistence does not
/// depend on anything running at exit, and a graceful stop cannot show that.
/// `kill -9` runs no `Drop`, no shutdown hook and no flush.
fn kill_a_serving_process(root: &Path) -> bool {
    let Some(bin) = gunnar_binary() else {
        // Under `cargo test` the binary is always built beside the test, so
        // this is a hard failure and never a silent skip: a skipped kill would
        // leave the caller asserting that a CLEAN shutdown persists, which is
        // the thing this is written not to prove.
        panic!(
            "the `gunnar` binary was not found beside this test; the SIGKILL leg cannot be \
             skipped, because a skipped kill turns the persistence assertions into a test of a \
             graceful shutdown"
        );
    };
    let mut child = std::process::Command::new(&bin)
        .arg("--data-root")
        .arg(root)
        .arg("serve")
        .arg("--ssh-bind")
        .arg("127.0.0.1:0")
        .arg("--grpc-bind")
        .arg("127.0.0.1:0")
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("spawn gunnar serve");

    // Wait for the banner on stdout: `serve` prints `ssh <addr>` once both
    // listeners are bound. Polling `/proc` for liveness would only prove the
    // process exists, not that it got as far as opening its data path.
    use std::io::BufRead as _;
    let stdout = child.stdout.take().expect("piped");
    let mut reader = std::io::BufReader::new(stdout);
    let mut line = String::new();
    let bound = {
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        let mut seen = false;
        while std::time::Instant::now() < deadline {
            line.clear();
            match reader.read_line(&mut line) {
                Ok(0) => break,
                Ok(_) if line.starts_with("ssh ") => {
                    seen = true;
                    break;
                }
                Ok(_) => continue,
                Err(_) => break,
            }
        }
        seen
    };
    assert!(
        bound,
        "the child gunnar never announced its listeners, so there was nothing to kill"
    );

    // SIGKILL. `Child::kill` is exactly that on unix.
    child.kill().expect("SIGKILL the serving gunnar");
    let status = child.wait().expect("reap");
    assert!(
        !status.success(),
        "a SIGKILLed process must not report a clean exit; got {status:?}"
    );
    true
}

/// The `gunnar` binary cargo built beside this test.
fn gunnar_binary() -> Option<std::path::PathBuf> {
    let mut dir = std::env::current_exe().ok()?;
    dir.pop(); // …/deps
    dir.pop(); // …/debug
    let candidate = dir.join("gunnar-cli");
    candidate.is_file().then_some(candidate)
}

// ───────────────────────────────────────────────────────────────────────────
// Everything after the first user is ORDINARY AUTHENTICATED administration
// ───────────────────────────────────────────────────────────────────────────

/// **A non-admin user may not create users and may not hand out gunnar-admin.**
///
/// Driven through the real `Access` handlers against the real `AuthTable`, with
/// a verified caller attached exactly as `AuthGate` attaches one, so what is
/// being measured is the grant decision and not a missing credential.
///
/// Both directions: the gunnar-admin succeeds at the same two calls the
/// ordinary user is refused. A suite that only asserted the refusals would pass
/// just as well against a control plane that refuses everybody.
#[test]
fn an_ordinary_user_cannot_create_users_or_grant_gunnar_admin() {
    use gunnar_grpc::{AuthMode, Caller, EventBus, ServerConfig, ServiceContext};
    use gunnar_proto::access_server::Access as _;

    let dir = tempfile::tempdir().expect("a temp dir");
    let fresh = Fresh::start(&dir.path().join("data"));
    fresh.enroll("alice", ALICE_PUB).expect("enrol");

    let plane = Arc::new(gunnar_cli::server::CoreControlPlane::new(Arc::clone(
        &fresh.stores,
    )));
    {
        use gunnar_grpc::ControlPlane as _;
        // alice, the gunnar-admin, creates bob. This is the second step of
        // the model and it is the ORDINARY verb, not a bootstrap path.
        plane
            .add_principal("bob", "bob", gunnar_proto::PrincipalKind::User)
            .expect("a gunnar-admin creates a user");
        plane
            .add_key("bob", BOB_PUB, "", None, "", None)
            .expect("key");
    }
    let control: Arc<dyn gunnar_grpc::ControlPlane> = plane;
    let ctx = ServiceContext::new(
        control,
        Arc::new(EventBus::new(16)),
        ServerConfig::default(),
    );
    assert_eq!(
        ctx.config.auth,
        AuthMode::Required,
        "the escape hatch would serve every call to everybody and make this whole test vacuous"
    );
    let access = gunnar_grpc::AccessService::new(ctx);
    let as_caller = |id: &str, message: gunnar_proto::AddPrincipalRequest| {
        let mut request = tonic::Request::new(message);
        request.extensions_mut().insert(Caller {
            principal_id: id.to_owned(),
            fingerprint: "SHA256:test".to_owned(),
            machine: false,
            authenticated: true,
        });
        request
    };
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime");

    // ── bob is NOT gunnar-admin, so he may not create a user ───────────
    let refused = rt
        .block_on(access.add_principal(as_caller(
            "bob",
            gunnar_proto::AddPrincipalRequest {
                name: "mallory".into(),
                kind: gunnar_proto::PrincipalKind::User as i32,
                id: String::new(),
            },
        )))
        .expect_err("an ordinary user must not be able to create users");
    assert_eq!(refused.code(), tonic::Code::PermissionDenied);

    // …nor hand out gunnar-admin, which is a Server-scoped Admin grant.
    let mut grant = tonic::Request::new(gunnar_proto::GrantRequest {
        principal_id: "bob".into(),
        namespace: String::new(),
        repo: String::new(),
        level: gunnar_proto::GrantLevel::Admin as i32,
    });
    grant.extensions_mut().insert(Caller {
        principal_id: "bob".to_owned(),
        fingerprint: "SHA256:test".to_owned(),
        machine: false,
        authenticated: true,
    });
    let refused = rt
        .block_on(access.grant(grant))
        .expect_err("an ordinary user must not be able to grant gunnar-admin to himself");
    assert_eq!(refused.code(), tonic::Code::PermissionDenied);

    // ── owning an account does NOT make you a server admin ────────────────
    assert!(
        fresh
            .stores
            .auth()
            .may_namespace("bob", "bob", Level::Admin)
            .expect("acl"),
        "bob is admin of his own account…"
    );
    assert!(
        !fresh
            .stores
            .auth()
            .may_server("bob", Level::Admin)
            .expect("acl"),
        "…and that must NOT leak into gunnar-admin"
    );

    // ── the other direction: alice succeeds at both ─────────────────────
    rt.block_on(access.add_principal(as_caller(
        "alice",
        gunnar_proto::AddPrincipalRequest {
            name: "mallory".into(),
            kind: gunnar_proto::PrincipalKind::User as i32,
            id: String::new(),
        },
    )))
    .expect("the gunnar-admin may create users");
    let mut grant = tonic::Request::new(gunnar_proto::GrantRequest {
        principal_id: "bob".into(),
        namespace: String::new(),
        repo: String::new(),
        level: gunnar_proto::GrantLevel::Admin as i32,
    });
    grant.extensions_mut().insert(Caller {
        principal_id: "alice".to_owned(),
        fingerprint: "SHA256:test".to_owned(),
        machine: false,
        authenticated: true,
    });
    rt.block_on(access.grant(grant))
        .expect("any gunnar-admin may hand gunnar-admin to anybody");
    assert!(
        fresh
            .stores
            .auth()
            .may_server("bob", Level::Admin)
            .expect("acl"),
        "and bob is now gunnar-admin too — it is not a singleton and not the first user's \
         private property"
    );
    fresh.shutdown();
}

/// **The unauthenticated surface is exactly three RPCs, and they are not in
/// the authenticated list.**
///
/// A guard on the shape rather than on a comment: adding a door means editing
/// `UNAUTHENTICATED_METHODS`, which is where the reason for each is written
/// down — and this list, in the same commit.
#[test]
fn exactly_three_rpcs_are_served_without_a_credential() {
    let open: std::collections::BTreeSet<_> = gunnar_grpc::UNAUTHENTICATED_METHODS
        .iter()
        .copied()
        .collect();
    assert_eq!(
        open,
        // `GrowStatus` (2026-09-14): the runtime data-set growth's phase, for
        // the actor OUTSIDE the box that owns the volumes and has nothing to
        // sign with while /data is unmounted (T6 in DATA-SET-GROWTH-FLOW.md).
        //
        // `TwinCsr` / `InstallTwinCertificate` (2026-09-21, lane T9): a twin from
        // the one medium holds nothing to present when it is paired, exactly as
        // `BecomeTwin`'s box does. Guarded in the seam instead — refused unless
        // the box IS a twin, the certificate is for its own key, and a pinned
        // issuer is never swapped. Reasons in `auth.rs`, beside the list.
        [
            "Bootstrap.Enroll",
            "Bootstrap.Status",
            "Bootstrap.GrowStatus",
            "Bootstrap.BecomeTwin",
            "Bootstrap.Restore",
            "Bootstrap.AssignTunnelAddress",
            "Bootstrap.TwinCsr",
            "Bootstrap.InstallTwinCertificate",
        ]
        .into_iter()
        .collect::<std::collections::BTreeSet<_>>(),
        "the unauthenticated surface changed; every entry is a door onto an unenrolled server \
         and needs its own reason in auth.rs"
    );
    let audited = gunnar_grpc::auth::audited_methods();
    assert!(
        audited.is_disjoint(&open),
        "a method cannot be both audited-as-authenticated and served without a credential: {:?}",
        audited.intersection(&open).collect::<Vec<_>>()
    );
    assert!(
        open.iter().all(|m| m.starts_with("Bootstrap.")),
        "only the Bootstrap service may be unauthenticated"
    );
}

/// ★ **A wrong clock is diagnosable without a credential.** The appliance
/// booted two hours fast on 2026-09-14 and refused every credential as stale —
/// `Admin.Health` included — so the only call that could say which clock was
/// wrong is the one that needs no credential. It must carry the server's own
/// clock and the last sync report, over the wire, to a caller holding nothing.
#[test]
fn bootstrap_status_serves_the_server_clock_and_the_last_clock_sync_without_a_credential() {
    use estate_clock::report::{Outcome, Report, SourceReport, Trigger};

    let dir = tempfile::tempdir().expect("a temp dir");
    let server = Fresh::start(dir.path());
    estate_clock::report::record(Report {
        trigger: Trigger::Boot,
        at_unix_ms: estate_clock::now_unix_ms(),
        outcome: Outcome::NoQuorum,
        offset_ms: 0,
        detail: "clock unchanged: 0 usable answer(s) from 1 configured source(s), 1 needed".into(),
        sources: vec![SourceReport {
            source: "peer://10.13.8.99:50123".into(),
            offset_ms: None,
            error: Some("peer://10.13.8.99:50123 did not accept a connection: timed out".into()),
        }],
    });

    let before = estate_clock::now_unix_ms();
    let status = server.status();
    let after = estate_clock::now_unix_ms();
    assert!(
        (before - 1_000..=after + 1_000).contains(&status.server_unix_ms),
        "server_unix_ms must be the server's clock at answer time: {} not in {before}..{after}",
        status.server_unix_ms
    );
    let sync = status
        .clock_last
        .expect("the last clock report rides the credential-free call");
    assert_eq!(
        (sync.trigger.as_str(), sync.outcome.as_str()),
        ("boot", "no-quorum")
    );
    assert_eq!(sync.sources.len(), 1);
    assert!(!sync.sources[0].answered);
    assert!(sync.sources[0]
        .error
        .contains("did not accept a connection"));
    server.shutdown();
}

// ───────────────────────────────────────────────────────────────────────────
// WHO answers: the build stamp and the role, without a credential
// ───────────────────────────────────────────────────────────────────────────

/// A twin binds its forge listener to loopback and so has no SSH banner to
/// name its build. The installer door (`--grpc-only`) and the re-image verify
/// (`--verify-control`) read the build and the role off `Bootstrap.Status`
/// instead — red-first: before the fields existed both read as empty.
#[test]
fn status_names_the_build_and_the_role_with_no_credential() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let primary = Fresh::start(&dir.path().join("primary"));
    let s = primary.status();
    assert_eq!(
        s.build,
        gunnar_cli::BUILD_REF,
        "the banner's own stamp, not a second one"
    );
    assert!(!s.build.is_empty());
    assert_eq!(s.role, "primary");
    primary.shutdown();

    let twin = Fresh::start_as(&dir.path().join("twin"), true);
    let t = twin.status();
    assert_eq!(
        t.role, "twin",
        "a --role twin box says so BEFORE the join, when twin.json does not exist yet"
    );
    assert_eq!(t.build, gunnar_cli::BUILD_REF);
    twin.shutdown();
}

/// ★ **A FRESH box hands the twin election its hardware facts without a credential; an
/// enrolled box does not** (T17, 2026-09-21 — T15's TD8).
///
/// `xtask mock elect twin` stopped with `elect/box-silent`: the facts it ranks by live on
/// `Admin.Capacity`, which is authenticated, and a fresh box accepts nobody's key. Enrolling
/// an admin first is not a way round it — `BecomeTwin` requires a FRESH box. So while the
/// bootstrap window is open, `Bootstrap.Status` carries `facts`: cores, RAM, the data root's
/// size and the start-up disk test with its honesty flag — read-only, nothing that names a
/// tenant, and the SAME numbers `Admin.Capacity` answers (one source). Once the window shuts
/// they are withheld and `facts_detail` sends the caller to `Admin.Capacity`.
///
/// **Fails before the fix**: `BootstrapStatusResponse` has no `facts` / `facts_detail`.
#[test]
fn a_fresh_box_answers_the_election_facts_unauthenticated_and_an_enrolled_box_does_not() {
    let dir = tempfile::tempdir().expect("a temp dir");
    let fresh = Fresh::start(&dir.path().join("data"));

    let before = fresh.status();
    assert!(before.window_open);
    let facts = before.facts.as_ref().unwrap_or_else(|| {
        panic!(
            "a fresh box must hand out its facts: {:?}",
            before.facts_detail
        )
    });
    let cpus = std::thread::available_parallelism()
        .expect("this test box has CPUs")
        .get() as u64;
    assert_eq!(
        facts.cpu_millicores_total,
        Some(cpus * 1000),
        "the same cores Admin.Capacity counts"
    );
    assert!(
        facts.ram_bytes_total.is_some_and(|b| b > 0),
        "RAM measured: {}",
        facts.machine_reason
    );
    assert!(
        facts.data_total_bytes.is_some_and(|b| b > 0),
        "the data root's size, never a 0 for unknown"
    );
    // No start-up disk test runs outside the appliance's PID 1: the flag says so by name,
    // and no speed field pretends to be a measurement.
    assert_eq!(
        facts.disk_test,
        gunnar_proto::DiskTest::DisktestNotRun as i32,
        "{}",
        facts.disk_reason
    );
    assert_eq!(
        (
            facts.disk_seq_write_bps,
            facts.disk_seq_read_bps,
            facts.disk_sync_milliops_per_s
        ),
        (None, None, None)
    );
    assert!(!facts.disk_reason.is_empty() && !facts.machine_reason.is_empty());
    assert!(
        before.facts_detail.contains("fresh"),
        "{}",
        before.facts_detail
    );

    fresh
        .enroll("alice", ALICE_PUB)
        .expect("the first enrolment needs no credential");
    let after = fresh.status();
    assert!(!after.window_open);
    assert!(
        after.facts.is_none(),
        "an ENROLLED box must not hand out its machine without a credential"
    );
    assert!(
        after.facts_detail.contains("Admin.Capacity"),
        "and says where they are: {}",
        after.facts_detail
    );
    fresh.shutdown();
}

/// **`serving_git` is a reading of the git listener, and a twin has none.**
///
/// 🔴 MEASURED 2026-09-20 on the live estate, and it is the eighth health line
/// of this shape found in a day: `gunnar control status --at 10.13.8.101:50051`
/// answered
///
/// ```text
/// build          v1.4.0-362-g7e15069
/// role           twin
/// git            serving
/// ```
///
/// while every replication push to that box failed
/// `ssh connect 10.13.8.101:2222 … Connection refused (os error 111)` — an RST,
/// so nothing was bound there at all. The field was wired to
/// `readiness::is_ready()`, a **boot latch** that starts `true` and is closed
/// only by a boot task that opens a window: a twin opens none, so the flag was
/// `true` from its first instruction to its last and could not have said
/// anything else. The listener itself was never consulted.
///
/// A twin does not serve git ON PURPOSE (`serve.rs`: "**No forge listener on a
/// twin.** It serves no git to anyone: a read would grow the caches the twin
/// must not hold"), so this assertion is not that the twin is broken — it is
/// that the status door must say what the twin IS.
#[test]
fn serving_git_reports_the_listener_and_a_twin_reports_no() {
    let dir = tempfile::tempdir().expect("a temp dir");

    // A free port to ASK for, so "what was asked" and "what was taken" are
    // different questions. The listener is dropped before the servers start.
    let port = {
        let probe = std::net::TcpListener::bind("127.0.0.1:0").expect("a free port");
        probe.local_addr().expect("its address").port()
    };
    let asked = std::net::SocketAddr::from(([127, 0, 0, 1], port));

    let primary = Fresh::start_on(&dir.path().join("primary"), false, Some(asked));
    let s = primary.status();
    assert_eq!(s.role, "primary");
    assert_eq!(
        primary.ssh_port, port,
        "a primary takes the port it was asked for"
    );
    assert!(
        s.serving_git,
        "a primary whose listener is on the asked-for port serves git: {}",
        s.readiness
    );
    assert!(s.readiness.contains("serving git on"), "{}", s.readiness);
    primary.shutdown();

    let twin = Fresh::start_on(&dir.path().join("twin"), true, Some(asked));
    let t = twin.status();
    assert_eq!(t.role, "twin");
    assert_ne!(
        twin.ssh_port, port,
        "serve.rs binds a twin's git listener to loopback:0 whatever was asked for"
    );
    assert!(
        !t.serving_git,
        "THE DEFECT: the twin said `git serving` while its git port refused every connection"
    );
    assert!(
        t.readiness.contains("NOT serving git") && t.readiness.contains("TWIN"),
        "the twin must say what it is, not merely no: {}",
        t.readiness
    );
    assert!(
        !t.readiness.contains("starting"),
        "a twin is not STARTING, it is never going to serve git: {}",
        t.readiness
    );
    twin.shutdown();
}