gunnar-server 2.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
//! **The twin's mutual-TLS credential, minted at PAIRING and never on the medium.**
//!
//! # Why this module exists
//!
//! `agent/wg-mtls` made the twin refuse to follow a primary on another machine
//! in the clear — correctly: the log it follows carries the KEK as seq 1. What
//! it did not do was give the twin anything to follow WITH. T15's bench, on a
//! medium carrying that change, measured the result: every twin refused
//! ("no client certificate is configured … It will not") and none ever
//! followed. A refusal with no way to satisfy it is a brick with a good error
//! message.
//!
//! The one-medium ruling decides where the certificate may come from: a twin is
//! installed from the SAME image as its primary with NO per-box value in it, so
//! the certificate cannot be on the medium. It is minted when the pair is
//! formed — the same shape as the twin's ssh key (`Bootstrap.BecomeTwin` mints
//! `twin.key` on the box and hands out only the public half):
//!
//! ```text
//!   twin      Bootstrap.TwinCsr               mint twin-tls.key HERE, answer the CSR
//!   primary   Admin.IssueTwinCertificate      issuer signs it for the twin's principal,
//!                                             registers it, answers cert + issuer + port
//!   twin      Bootstrap.InstallTwinCertificate  check it is for OUR key, pin the issuer
//! ```
//!
//! `gunnar control twin certify` is the courier. The private key never leaves
//! the twin; what crosses is a request (public key + proof of possession) and a
//! certificate (public).
//!
//! # What the twin then does with it
//!
//! Dials the primary's **mutual replication listener** — its own port, served
//! only with a client certificate its issuer signed (`serve.rs`,
//! `--grpc-replication-bind`) — at the primary's host and the port the primary
//! reported, verifying the server against the PINNED issuer under the fixed name
//! [`gunnar_grpc::client_signer::REPLICATION_SERVER_NAME`]. Once paired it uses
//! mutual TLS even to a loopback primary: the credential is there, and a lane
//! that encrypts only sometimes is a lane whose tests prove the other branch.
//!
//! # Re-pointing is refused; renewal is not
//!
//! A second install with a DIFFERENT issuer is refused by name — a twin is never
//! re-pointed, and an unauthenticated door that could swap the CA it pins would
//! be a door for redirecting the replication stream. A second install under the
//! SAME issuer for the SAME key is a renewal and is taken: that is the path by
//! which a box with no shell gets a fresh certificate before the old one lapses
//! (`gunnar_core::expiry`), without anyone ever logging in.

use std::path::Path;

use anyhow::{anyhow, bail, Context, Result};

/// The twin's TLS private key (PKCS#8 PEM, 0600). Born on this box.
pub const TLS_KEY: &str = "twin-tls.key";
/// The request for it, kept so a repeated `TwinCsr` answers the same one.
pub const TLS_CSR: &str = "twin-tls.csr";
/// The certificate the primary's issuer signed for [`TLS_KEY`].
pub const TLS_CERT: &str = "twin-tls.crt";
/// The primary's issuer, PINNED: the only CA the twin accepts the primary's
/// replication listener from.
pub const TLS_CA: &str = "twin-tls-ca.pem";
/// The primary's mutual replication port, as the primary reported it.
pub const TLS_PORT: &str = "twin-tls.port";

/// On a PRIMARY: the port its mutual replication listener is bound on, written
/// by `serve` at every start and removed when the listener is off, so
/// `Admin.IssueTwinCertificate` can tell a twin where to dial — and can refuse
/// by name to certify a twin it has no listener for.
pub const REPLICATION_PORT_FILE: &str = "replication-listener.port";
/// PRIMARY side: the address the replication listener is bound on WHEN it is not
/// the one the twin was joined to — the WireGuard tunnel address, once the
/// listener moved onto `wg0` (owner, 2026-09-22). Absent = the joined host.
pub const REPLICATION_HOST_FILE: &str = "replication-listener.host";
/// TWIN side: the host `twin certify` said to dial the primary's replication
/// port at. Absent = the host this twin was joined to (`twin.json.primary`).
pub const TLS_HOST: &str = "twin-tls.host";

fn now_ms() -> i64 {
    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0)
}

fn require_twin(root: &Path) -> Result<()> {
    if crate::twin::TwinConfig::load(root)?.is_none() {
        bail!(
            "this box is not a twin (no twin.json): a replication certificate is only minted and \
             installed on a box that `gunnar control twin join` has made a twin"
        );
    }
    Ok(())
}

fn write_private(root: &Path, name: &str, bytes: &[u8]) -> Result<()> {
    crate::durable::write_atomic_unframed(root, name, bytes)?;
    use std::os::unix::fs::PermissionsExt as _;
    std::fs::set_permissions(root.join(name), std::fs::Permissions::from_mode(0o600))
        .with_context(|| format!("restricting {name} to its owner"))?;
    Ok(())
}

/// **`Bootstrap.TwinCsr`**: this twin's certificate signing request, minting
/// the key on first call. Idempotent — the same key, the same request — so a
/// courier that dies half-way can simply run again.
pub fn request(root: &Path) -> Result<String> {
    require_twin(root)?;
    let (key, csr) = (root.join(TLS_KEY), root.join(TLS_CSR));
    if key.exists() && csr.exists() {
        return std::fs::read_to_string(&csr).with_context(|| format!("reading {}", csr.display()));
    }
    let (key_pem, csr_pem) = gunnar_core::issuer::mint_client_request("gunnar-twin")?;
    // The key first: a request on disk whose key is not is a request nobody can
    // ever use, while a key without its request is re-requested next call.
    write_private(root, TLS_KEY, key_pem.as_bytes())?;
    crate::durable::write_atomic_unframed(root, TLS_CSR, csr_pem.as_bytes())?;
    Ok(csr_pem)
}

/// **`Bootstrap.InstallTwinCertificate`**: take the certificate the primary's
/// issuer signed, and the issuer to pin.
///
/// Refused, each by name: not a twin; no key here (the request was never
/// made); a certificate for ANOTHER key; a certificate the expiry rule already
/// refuses; a different issuer from the one already pinned. Answers the grade,
/// so the courier prints how long the pairing is good for.
pub fn install(root: &Path, cert_pem: &str, ca_pem: &str, replication_port: u16) -> Result<gunnar_core::expiry::Expiry> {
    install_at(root, cert_pem, ca_pem, replication_port, None)
}

/// [`install`], with the HOST the primary said to dial its replication port at
/// (its tunnel address) — `None` keeps the joined host, as before. Written
/// before the certificate, which stays the "paired" marker, and cleared when
/// absent so a renewal from a primary that left the tunnel does not dial a
/// dead address.
pub fn install_at(root: &Path, cert_pem: &str, ca_pem: &str, replication_port: u16, host: Option<&str>) -> Result<gunnar_core::expiry::Expiry> {
    require_twin(root)?;
    if let Some(h) = host {
        // One IP, or `<tunnel>,<utility>` — every one of them an IP.
        if hosts(h).is_empty() || hosts(h).iter().any(|one| one.parse::<std::net::IpAddr>().is_err()) {
            bail!("the primary reported replication host {h:?}, which is not an IP address; nothing was installed");
        }
    }
    if replication_port == 0 {
        bail!("the primary reported no replication port; there is no mutual listener for this twin to dial");
    }
    let key_pem = std::fs::read_to_string(root.join(TLS_KEY)).map_err(|err| {
        anyhow!(
            "this twin has no replication key ({TLS_KEY}: {err}); `Bootstrap.TwinCsr` mints it — \
             a certificate cannot be installed for a key that was never made"
        )
    })?;
    let ours = gunnar_core::issuer::public_key_info_of_key(&key_pem)?;
    let theirs = gunnar_core::issuer::public_key_info_of_cert(cert_pem)?;
    if ours != theirs {
        bail!(
            "this certificate was issued for a DIFFERENT key than the one this twin minted, so it \
             could never complete a handshake here. Nothing was installed. Re-run `gunnar control \
             twin certify` against this twin — the request it signs is this box's own."
        );
    }
    // The CA must parse, and it must be what the leaf names as its issuer: a
    // pinned CA that did not sign the leaf would make every dial fail with a
    // handshake error that names neither.
    if let Err((named, handed)) = gunnar_core::issuer::issued_by(cert_pem, ca_pem)? {
        bail!(
            "the certificate names issuer `{named}` and the CA handed with it is `{handed}`: this \
             twin would pin a CA that did not sign its own credential"
        );
    }
    let cert_der = gunnar_core::issuer::pem_to_der(cert_pem)?;
    if let Ok(pinned) = std::fs::read(root.join(TLS_CA)) {
        if pinned != ca_pem.as_bytes() {
            bail!(
                "this twin already pins a DIFFERENT issuer. A twin is never re-pointed, and a door \
                 that could swap the CA it trusts is a door for redirecting the stream that carries \
                 the KEK. A renewal comes from the SAME issuer; a new primary needs a fresh twin."
            );
        }
    }
    let expiry = gunnar_core::expiry::grade_der(now_ms(), &cert_der)?;
    if expiry.is_refusal() {
        bail!("{}", expiry.line("the certificate offered to this twin"));
    }
    crate::durable::write_atomic_unframed(root, TLS_CA, ca_pem.as_bytes())?;
    crate::durable::write_atomic_unframed(root, TLS_PORT, replication_port.to_string().as_bytes())?;
    match host {
        Some(h) => crate::durable::write_atomic_unframed(root, TLS_HOST, h.as_bytes())?,
        None => match std::fs::remove_file(root.join(TLS_HOST)) {
            Err(err) if err.kind() != std::io::ErrorKind::NotFound => return Err(anyhow!("removing {TLS_HOST}: {err}")),
            _ => {}
        },
    }
    // The certificate LAST: its presence is what `load` treats as "paired", so
    // a cut before this line leaves an unpaired twin, never a paired one with
    // no CA.
    crate::durable::write_atomic_unframed(root, TLS_CERT, cert_pem.as_bytes())?;
    Ok(expiry)
}

/// The paired credential, when this twin has one: the trust to dial with and
/// the primary's replication port. `None` before pairing.
pub fn load(root: &Path) -> Result<Option<(gunnar_grpc::client_signer::ClientTrust, u16)>> {
    let cert = match std::fs::read(root.join(TLS_CERT)) {
        Ok(cert) => cert,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(anyhow!("reading {TLS_CERT}: {err}")),
    };
    let read = |name: &str| std::fs::read(root.join(name)).with_context(|| format!("{TLS_CERT} is installed but {name} is not readable"));
    let key = read(TLS_KEY)?;
    let ca = read(TLS_CA)?;
    let port: u16 = String::from_utf8_lossy(&read(TLS_PORT)?)
        .trim()
        .parse()
        .map_err(|err| anyhow!("{TLS_PORT} does not hold a port: {err}"))?;
    Ok(Some((gunnar_grpc::client_signer::ClientTrust { ca, cert, key }, port)))
}

/// Twin: the host `twin certify` said to dial the primary's replication port at,
/// when it named one.
pub fn load_host(root: &Path) -> Option<String> {
    std::fs::read_to_string(root.join(TLS_HOST)).ok().map(|s| s.trim().to_owned()).filter(|s| !s.is_empty())
}

/// Primary: record (or clear) the hosts the replication doors answer on, in
/// the twin's order of preference — `<tunnel>,<utility>` while a door is open
/// on the tunnel; cleared (the twin dials the host it joined) otherwise.
pub fn record_replication_host(root: &Path, hosts: &[std::net::IpAddr]) -> Result<()> {
    if hosts.is_empty() {
        return match std::fs::remove_file(root.join(REPLICATION_HOST_FILE)) {
            Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(anyhow!("removing {REPLICATION_HOST_FILE}: {err}")),
            _ => Ok(()),
        };
    }
    let list = hosts.iter().map(|h| h.to_string()).collect::<Vec<_>>().join(",");
    crate::durable::write_atomic_unframed(root, REPLICATION_HOST_FILE, list.as_bytes())
}

/// Twin: the hosts `twin certify` named, in order (one, or `<tunnel>,<utility>`).
pub fn hosts(recorded: &str) -> Vec<String> {
    recorded.split(',').map(str::trim).filter(|h| !h.is_empty()).map(str::to_owned).collect()
}

/// Primary: the tunnel address the replication listener is on, if it is on one.
pub fn replication_host(root: &Path) -> Option<String> {
    std::fs::read_to_string(root.join(REPLICATION_HOST_FILE)).ok().map(|s| s.trim().to_owned()).filter(|s| !s.is_empty())
}

/// Primary: record (or clear) the bound replication port.
pub fn record_replication_port(root: &Path, port: Option<u16>) -> Result<()> {
    match port {
        Some(port) => crate::durable::write_atomic_unframed(root, REPLICATION_PORT_FILE, port.to_string().as_bytes()),
        None => match std::fs::remove_file(root.join(REPLICATION_PORT_FILE)) {
            Err(err) if err.kind() != std::io::ErrorKind::NotFound => Err(anyhow!("removing {REPLICATION_PORT_FILE}: {err}")),
            _ => Ok(()),
        },
    }
}

/// Primary: the bound replication port, if the listener is on.
pub fn replication_port(root: &Path) -> Option<u16> {
    std::fs::read_to_string(root.join(REPLICATION_PORT_FILE)).ok()?.trim().parse().ok()
}

/// **`Admin.IssueTwinCertificate`, on the PRIMARY**: sign `csr_pem` for
/// `principal`, register it (with its expiry, so a lapsed certificate stops
/// authenticating in the same table a revocation writes), and answer the
/// certificate, the issuer to pin and the replication port.
///
/// Refused by name when this primary has no mutual replication listener — a
/// certificate for a door that does not exist is a pairing that looks complete
/// and follows nothing — and when `principal` is not a member of group `twin`:
/// this mints a credential into the stream that carries the KEK, and only a
/// principal already enrolled as a twin may hold one.
pub fn issue_for_twin(stores: &crate::stores::ServerStores, principal: &str, csr_pem: &str) -> Result<(String, String, u16)> {
    let port = replication_port(stores.root()).ok_or_else(|| {
        anyhow!(
            "this primary serves no mutual replication listener, so a twin certificate would pair it \
             to a door that does not exist. Start it with --grpc-replication-bind (it is on by \
             default whenever the control plane binds off loopback)"
        )
    })?;
    let groups = stores.auth().groups_of(principal).with_context(|| format!("reading the groups of {principal}"))?;
    if !groups.iter().any(|g| g == gunnar_grpc::TWIN_GROUP) {
        bail!(
            "{principal} is not a member of group `{}`. A replication certificate opens the stream \
             that carries every sealed secret; it is issued only to a principal already enrolled as \
             a twin (`gunnar control group member add {} {principal}`)",
            gunnar_grpc::TWIN_GROUP,
            gunnar_grpc::TWIN_GROUP
        );
    }
    let pki = stores.pki()?;
    let issuer = pki.issuer()?.ok_or_else(|| anyhow!("this primary has no issuer, so it cannot certify a twin"))?;
    let issued = pki.issue_from_csr(principal, csr_pem, gunnar_core::DEFAULT_LEAF_DAYS)?;
    let not_after = gunnar_core::issuer::not_after_ms(&issued.cert_der)?;
    stores.auth().register_certificate(principal, &issued.cert_der, Some(not_after))?;
    // **A certificate for a NEW key replaces, it does not sit beside** (twin
    // reborn, 2026-09-30). A twin's key is minted on its box and never leaves
    // it, so a request for another key under the same principal is another box
    // — the dead twin's replacement. Its predecessor's certificate is revoked
    // here, or a copy of the nuked twin's disk would keep reading the stream
    // that carries the KEK beside the new twin. The same key again is a
    // renewal and keeps its older certificate until it is installed.
    let spki = gunnar_core::issuer::public_key_info_of_der(&issued.cert_der)?;
    let superseded = stores.auth().supersede_certificates(principal, &spki)?;
    if !superseded.is_empty() {
        let fingerprints: Vec<&str> = superseded.iter().map(|k| k.fingerprint_sha256.as_str()).collect();
        tracing::warn!(
            target: gunnar_core::observe::TARGET_TWIN,
            principal,
            superseded = ?fingerprints,
            "a twin certificate for a NEW key was issued: the {} certificate(s) {principal} held for another key are revoked (the box they belonged to is replaced)",
            superseded.len()
        );
    }
    Ok((issued.cert_pem, issuer.cert_pem, port))
}

#[cfg(test)]
mod replication_host_tests {
    /// The recorded list round-trips in the twin's order of preference.
    #[test]
    fn the_recorded_hosts_are_tunnel_then_utility() {
        let dir = tempfile::tempdir().unwrap();
        let hosts: Vec<std::net::IpAddr> = vec!["10.44.0.3".parse().unwrap(), "10.13.9.90".parse().unwrap()];
        super::record_replication_host(dir.path(), &hosts).unwrap();
        let got = super::replication_host(dir.path()).unwrap();
        assert_eq!(super::hosts(&got), ["10.44.0.3", "10.13.9.90"]);
        assert_eq!(super::hosts("10.44.0.3"), ["10.44.0.3"], "a single host (an older primary) still reads");
        super::record_replication_host(dir.path(), &[]).unwrap();
        assert_eq!(super::replication_host(dir.path()), None);
    }
}