gunnar-server 3.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 installer's feed, at rest** (h-feed-at-rest, 2026-09-28).
//!
//! `<data>/pairing/joined.toml` is the whole `JoinReply` a box was fed: on vm1
//! that is `secret.stripe_key`, `secret.stripe_webhook`, `secret.upcloud_token`,
//! `console_admin_token` and every `tls.*.key`; on main and twin the admin and
//! machine principals. It was written with `durable::write_atomic_unframed` —
//! the process umask's mode (0644 on the appliance), plaintext.
//!
//! Now it is written SEALED and 0600: AES-256-GCM under the pairing KEK
//! ([`gunnar_core::Kek::for_control_dir`] on `<data>/pairing` — the operator's
//! `GUNNAR_SECRET_KEK` when the medium carries one, else `secrets.kek` beside it,
//! 0600, minted on first use), the file's relative path bound as the AEAD's
//! associated data. This is the same seal and the same key custody gunnar's own
//! secret store has (`stores.rs`): no TPM exists on the boxes, so a key the box
//! can read unattended at boot is a key on the box. What it buys is that the
//! feed's secrets are no longer one `cat`, one log line, one support tarball or
//! one world-readable file away, and that a copy of the file alone reads as
//! noise.
//!
//! A PLAINTEXT file — every box fed before this change, and the test fixtures —
//! is still read, and is sealed the next time the feed is written.

use std::path::Path;

/// The pairing directory the KEK lives in, under the data root.
pub const PAIRING_DIR_REL: &str = "pairing";

/// **Write the feed**: sealed, 0600, durably.
pub fn write(root: &Path, rel: &str, text: &str) -> anyhow::Result<()> {
    let kek = kek(root)?;
    let sealed = gunnar_core::seal::seal(&kek, rel, text.as_bytes())
        .map_err(|e| anyhow::anyhow!("sealing {rel}: {e}"))?;
    if let Some(parent) = root.join(rel).parent() {
        std::fs::create_dir_all(parent)?;
    }
    crate::durable::write_atomic_unframed(root, rel, &sealed)?;
    use std::os::unix::fs::PermissionsExt as _;
    std::fs::set_permissions(root.join(rel), std::fs::Permissions::from_mode(0o600))?;
    Ok(())
}

/// The pairing KEK: `GUNNAR_SECRET_KEK` when the medium carries one, else
/// `<data>/pairing/secrets.kek` (0600, minted on first use).
fn kek(root: &Path) -> anyhow::Result<gunnar_core::Kek> {
    let dir = root.join(PAIRING_DIR_REL);
    std::fs::create_dir_all(&dir)?;
    gunnar_core::Kek::for_control_dir(&dir)
        .map_err(|e| anyhow::anyhow!("the pairing KEK under {}: {e}", dir.display()))
}

/// **Read the feed**: unsealed, or a legacy plaintext file as it is. `None`
/// when there is no file, or it cannot be opened (logged by name).
pub fn read(root: &Path, rel: &str) -> Option<String> {
    match read_checked(root, rel) {
        Ok(t) => t,
        Err(e) => {
            tracing::error!(file = rel, error = %format!("{e:#}"), "the fed file cannot be read — taken as absent");
            None
        }
    }
}

/// [`read`], for the callers that must tell ABSENT (`Ok(None)`) from PRESENT
/// AND UNREADABLE (`Err`): a persisted answer that lies must not read as "nobody".
pub fn read_checked(root: &Path, rel: &str) -> anyhow::Result<Option<String>> {
    let path = root.join(rel);
    let raw = match std::fs::read(&path) {
        Ok(r) => r,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => anyhow::bail!("{}: {e}", path.display()),
    };
    if !gunnar_core::seal::is_sealed(&raw) {
        // A feed from before h-feed-at-rest: plaintext, sealed on its next write.
        return String::from_utf8(raw)
            .map(Some)
            .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()));
    }
    // Never mint a key to read with: a sealed file with no key beside it is
    // unreadable, not a reason to invent one.
    let kek_file = root.join(PAIRING_DIR_REL).join(gunnar_core::seal::KEK_FILE);
    let kek = match gunnar_core::Kek::from_env().map_err(|e| anyhow::anyhow!("{e}"))? {
        Some(k) => k,
        None if kek_file.exists() => {
            gunnar_core::Kek::from_file(&kek_file).map_err(|e| anyhow::anyhow!("{e}"))?
        }
        None => anyhow::bail!(
            "{} is sealed and this box holds no key for it ({} is absent)",
            path.display(),
            kek_file.display()
        ),
    };
    let plain = gunnar_core::seal::unseal(&kek, rel, &raw).map_err(|e| {
        anyhow::anyhow!("{} does not open under this box's key: {e}", path.display())
    })?;
    String::from_utf8(plain)
        .map(Some)
        .map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::os::unix::fs::PermissionsExt as _;

    const REL: &str = crate::pairing::JOINED_REL;
    const SECRET: &str = "sk_test_feedAtRestNEVERlive0123456789";

    fn text() -> String {
        format!("estate = \"e\"\nrole = \"front\"\npeers = []\n\n[me]\nname = \"vm1\"\noverlay = \"10.13.0.1\"\n\n[feed]\n\"secret.stripe_key\" = \"{SECRET}\"\nconsole_admin_token = \"{}\"\n", "a".repeat(64))
    }

    #[test]
    fn the_feed_is_sealed_and_owner_only_on_disk_and_reads_back_whole() {
        let d = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(d.path().join("pairing")).unwrap();
        write(d.path(), REL, &text()).unwrap();
        let raw = std::fs::read(d.path().join(REL)).unwrap();
        assert!(
            !raw.windows(SECRET.len()).any(|w| w == SECRET.as_bytes()),
            "the stripe key is readable in the file on disk"
        );
        assert!(
            !String::from_utf8_lossy(&raw).contains("console_admin_token"),
            "the feed's keys are readable on disk"
        );
        assert!(
            gunnar_core::seal::is_sealed(&raw),
            "the file is a sealed value"
        );
        let mode = std::fs::metadata(d.path().join(REL))
            .unwrap()
            .permissions()
            .mode()
            & 0o777;
        assert_eq!(mode, 0o600, "the feed is the owner's alone");
        assert_eq!(
            read(d.path(), REL).as_deref(),
            Some(text().as_str()),
            "and it reads back byte for byte"
        );
    }

    #[test]
    fn a_plaintext_feed_from_before_is_still_read_and_sealed_on_the_next_write() {
        let d = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(d.path().join("pairing")).unwrap();
        std::fs::write(d.path().join(REL), text()).unwrap();
        assert_eq!(read(d.path(), REL).as_deref(), Some(text().as_str()));
        write(d.path(), REL, &text()).unwrap();
        assert!(gunnar_core::seal::is_sealed(
            &std::fs::read(d.path().join(REL)).unwrap()
        ));
    }

    #[test]
    fn a_sealed_feed_is_bound_to_its_file_and_its_key() {
        let d = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(d.path().join("pairing")).unwrap();
        write(d.path(), REL, &text()).unwrap();
        // Moved under another name: the associated data no longer matches.
        std::fs::copy(d.path().join(REL), d.path().join("pairing/other.toml")).unwrap();
        assert_eq!(read(d.path(), "pairing/other.toml"), None);
        // Another box's KEK: nothing.
        let e = tempfile::tempdir().unwrap();
        std::fs::create_dir_all(e.path().join("pairing")).unwrap();
        std::fs::copy(d.path().join(REL), e.path().join(REL)).unwrap();
        assert_eq!(
            read(e.path(), REL),
            None,
            "a copy of the file without its box's key reads as nothing"
        );
    }
}