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

nordisk/gunnar

crates/gunnar-server/tests/repair_key_is_narrow.rs

//! **The repair key is narrow** (owner 2026-10-12: "a twin-only grant plus a fed restore key —
//! it may only drive the restore-from-twin path (and the fence/look it needs), never full
//! admin"). Over the real wire, with real credentials (no insecure mode):
//!
//! * fed as `principal.repair = "machine repair <key>"`, the primary installs it with NO grant;
//! * even GRANTED server admin by hand (the worst case), the `repair` principal reaches only
//!   `gunnar_grpc::REPAIR_METHODS`: it reads the log (`Mirror.LogHead`), it fences
//!   (`Mirror.SetRole(STANDBY)`) — and it cannot add a principal, read the server's stats,
//!   add a key, demote-and-promote (`SetRole(PRIMARY)`), or issue a twin certificate;
//! * it is a restore key of a fresh, fed box (`fed_principals::restore_keys`).
#![cfg(all(feature = "replication", feature = "wg-client"))]

use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};

use gunnar_core::ssh_key::PrivateKey;
use gunnar_grpc::client_signer::{authed, plain_channel};
use gunnar_grpc::{EventBus, ServerConfig, ServiceContext};
use gunnar_server::stores::ServerStores;
use gunnar_server::CoreControlPlane;

fn free_addr() -> SocketAddr {
    let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
    l.local_addr().expect("addr")
}

fn mint() -> (PrivateKey, String) {
    let pem = gunnar_core::mint_ed25519().expect("mint");
    let k = PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap()).expect("parse");
    let line = k.public_key().to_openssh().expect("line");
    (k, line)
}

#[test]
fn the_repair_key_reads_the_log_and_fences_and_does_nothing_an_admin_does() {
    let d = tempfile::tempdir().unwrap();
    let stores = Arc::new(ServerStores::open(d.path()).expect("stores"));
    let (_admin, admin_line) = mint();
    stores
        .auth()
        .bootstrap_admin(&admin_line)
        .expect("the first admin");
    let (repair, repair_line) = mint();
    let feed: BTreeMap<String, String> = [(
        format!("principal.{}", gunnar_grpc::REPAIR_PRINCIPAL),
        format!("machine repair {repair_line}"),
    )]
    .into_iter()
    .collect();
    let said =
        gunnar_server::fed_principals::install(&stores, &feed).expect("the primary installs it");
    assert!(
        said.iter().any(|l| l.starts_with("repair")
            && l.contains("key registered")
            && !l.contains("gunnar-admin")),
        "{said:?}"
    );
    assert!(
        stores
            .auth()
            .grants(Some(gunnar_grpc::REPAIR_PRINCIPAL))
            .unwrap()
            .is_empty(),
        "fed with NO grant"
    );
    assert!(
        gunnar_server::fed_principals::restore_keys(&feed).contains(&repair_line),
        "a fresh fed box takes its restore"
    );
    // A `repair` role on any other name is refused.
    let other: BTreeMap<String, String> = [(
        "principal.mallory".to_string(),
        format!("machine repair {repair_line}"),
    )]
    .into_iter()
    .collect();
    assert!(gunnar_server::fed_principals::parse(&other)
        .unwrap_err()
        .contains("repair"));

    // The worst case: somebody grants it server admin by hand. The narrowing is the name's.
    let plane_for_grant = CoreControlPlane::new(Arc::clone(&stores));
    gunnar_grpc::ControlPlane::grant(
        &plane_for_grant,
        gunnar_grpc::REPAIR_PRINCIPAL,
        "",
        "",
        gunnar_proto::GrantLevel::Admin,
    )
    .expect("granted by hand");

    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all()
        .build()
        .expect("rt");
    let addr = free_addr();
    let bus = Arc::new(EventBus::new(16));
    let slot = gunnar_server::mirror::ReplicationSlot::new(
        Arc::clone(&stores),
        Arc::clone(&bus),
        rt.handle().clone(),
    );
    let plane: Arc<dyn gunnar_grpc::ControlPlane> =
        Arc::new(CoreControlPlane::new(Arc::clone(&stores)).with_replication(slot));
    let ctx = ServiceContext::new(plane, bus, ServerConfig::default());
    let (tx, rx) = tokio::sync::oneshot::channel::<()>();
    rt.spawn(async move {
        let _ = gunnar_grpc::router(ctx, || 0)
            .serve_with_shutdown(addr, async {
                let _ = rx.await;
            })
            .await;
    });
    let t0 = Instant::now();
    while std::net::TcpStream::connect(addr).is_err() {
        assert!(t0.elapsed() < Duration::from_secs(10), "the box listens");
        std::thread::sleep(Duration::from_millis(25));
    }
    let at = addr.to_string();
    rt.block_on(async {
        let ch = plain_channel(&at).expect("a channel");
        let mut mirror = gunnar_proto::mirror_client::MirrorClient::new(ch.clone());
        let mut admin = gunnar_proto::admin_client::AdminClient::new(ch.clone());
        let mut access = gunnar_proto::access_client::AccessClient::new(ch.clone());

        // The restore path's look: allowed.
        mirror
            .log_head(authed(&repair, gunnar_proto::LogHeadRequest::default()))
            .await
            .expect("Mirror.LogHead: the repair key reads the log");
        // The fence: STANDBY allowed (the grant check passes; what the slot answers is the box's).
        let standby = mirror
            .set_role(authed(
                &repair,
                gunnar_proto::SetRoleRequest {
                    role: gunnar_proto::ReplicationRole::Standby as i32,
                },
            ))
            .await;
        assert!(
            standby
                .as_ref()
                .map(|_| true)
                .unwrap_or_else(|s| s.code() != tonic::Code::PermissionDenied),
            "the fence is the repair key's: {standby:?}"
        );

        // Nothing an admin does.
        let primary = mirror
            .set_role(authed(
                &repair,
                gunnar_proto::SetRoleRequest {
                    role: gunnar_proto::ReplicationRole::Primary as i32,
                },
            ))
            .await
            .unwrap_err();
        assert_eq!(
            primary.code(),
            tonic::Code::PermissionDenied,
            "it never promotes: {primary:?}"
        );
        let stats = admin
            .stats(authed(&repair, gunnar_proto::StatsRequest {}))
            .await
            .unwrap_err();
        assert_eq!(stats.code(), tonic::Code::PermissionDenied, "{stats:?}");
        assert!(stats.message().contains("narrowed"), "{}", stats.message());
        let add = access
            .add_principal(authed(
                &repair,
                gunnar_proto::AddPrincipalRequest {
                    name: "mallory".into(),
                    kind: gunnar_proto::PrincipalKind::User as i32,
                    ..Default::default()
                },
            ))
            .await
            .unwrap_err();
        assert_eq!(add.code(), tonic::Code::PermissionDenied, "{add:?}");
        let cert = admin
            .issue_twin_certificate(authed(
                &repair,
                gunnar_proto::IssueTwinCertificateRequest::default(),
            ))
            .await
            .unwrap_err();
        assert_eq!(cert.code(), tonic::Code::PermissionDenied, "{cert:?}");
    });
    assert!(
        stores.auth().principal("mallory").unwrap().is_none(),
        "nothing was added"
    );
    let _ = tx.send(());
}