gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! Listener configuration: the loopback default, and the authentication mode.
//!
//! The loopback default (the two-listener split) predates gRPC authentication: it was what kept
//! the allow-all hole *local* rather than *remote*. Authentication has landed, and the
//! default stays anyway. Defence in depth costs nothing here, every real
//! deployment reaches gRPC over an SSH tunnel or from the same host, and
//! `tests/loopback_default.rs` still goes red if anyone widens it.

use std::net::{IpAddr, Ipv4Addr, SocketAddr};

pub use crate::auth::AuthMode;

/// **The default gRPC bind address. It is loopback, and that is load-bearing.**
///
/// v1 gRPC is unauthenticated by decision. Anyone who can reach this port
/// is an admin of the control plane. Binding to `127.0.0.1` makes that hole a
/// *local* one rather than a *remote* one, and it costs nothing; every real
/// deployment reaches gRPC over an SSH tunnel or from a process on the same
/// host anyway.
///
/// Changing this to `0.0.0.0` turns a known, contained hole into a remotely
/// exploitable one. `default_bind_is_loopback` in `tests/loopback_default.rs`
/// goes red if anyone does.
pub const DEFAULT_BIND: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 50051);

/// Whether this build of `gunnar-grpc` implements gRPC authentication.
///
/// **`true` since the allow-all hole was closed.** It is a statement about the *crate*, not
/// about one listener: a server started through
/// [`ServerConfig::insecure_allow_all_for_local_development`] still has the
/// code and still refuses nothing, which is why `Admin.Health` reports
/// [`ServerConfig::authentication_enabled`], the runtime answer, rather than
/// this constant.
pub const AUTHENTICATION_ENABLED: bool = true;

/// How many events the event broadcast bus retains for subscribers that have
/// not read them yet. This is a *hard* bound: past it, the oldest events are
/// dropped and the affected subscriber is handed an `EventGap`. It is never a
/// growth budget.
pub const DEFAULT_EVENT_BUFFER: usize = 1024;

#[derive(Debug, Clone)]
pub struct ServerConfig {
    /// Where the gRPC control plane listens. Loopback by default; see
    /// [`DEFAULT_BIND`].
    pub bind: SocketAddr,
    /// Capacity of the event bus ring. See [`DEFAULT_EVENT_BUFFER`].
    pub event_buffer: usize,
    /// Informational only: where the SSH listener is bound, so `Admin.Health`
    /// can report both listeners in one place (they are separate, and an
    /// operator should be able to see that). `gunnar-cli` fills it in; this
    /// crate never binds it.
    pub ssh_bind_hint: String,
    /// Whether a credential is required. [`AuthMode::Required`] by default and
    /// in every deployment; see
    /// [`ServerConfig::insecure_allow_all_for_local_development`] for the one
    /// way to get anything else.
    pub auth: AuthMode,
    /// **Does this listener demand a client certificate?** (`--grpc-tls-self`.)
    ///
    /// A `bool` and deliberately nothing more. The trust root itself — two
    /// certificates and a private key — is consumed once by
    /// [`crate::router_with_trust`] and never enters this struct, which is
    /// cloned into every [`crate::ServiceContext`] and printed by `Debug`;
    /// that argument is written out at `router_with_trust`. What an operator
    /// needs on the wire is not the material but the *fact*, because the
    /// transition to TLS is a restart with a changed flag and the only way to
    /// confirm it took is to ask the server.
    ///
    /// `false` here on a listener that IS mutual would be a lie of exactly the
    /// kind `grpc_authentication_enabled` was told for months, so it is set in
    /// the same `match` that builds the trust root and from the same value.
    pub mutual_tls: bool,
    /// **Must the peer's client certificate be REGISTERED to a principal**, not
    /// only signed by the issuer the listener pins?
    ///
    /// `true` on the mutual REPLICATION door only (`serve::replication_door`),
    /// where the certificate is the twin's pairing: the handshake alone accepts
    /// any certificate the box's issuer ever signed, and a request that also
    /// carries a header is judged by the header alone — so a revoked twin
    /// certificate (a nuked twin's, superseded when its replacement was
    /// certified, twin reborn 2026-09-30) still opened the door as long as the
    /// dead twin's ssh key signed the call. With this on, a certificate no
    /// principal holds (revoked, superseded, expired) is refused at the gate
    /// whatever else the call carries.
    pub require_registered_client_certificate: bool,
    /// **The WebAuthn relying parties this server answers for**
    /// (`--web-relying-party`, stinky row 13). Empty: the relay's `rp_id` and
    /// `origin` are taken as stated, as before. Otherwise every ceremony's
    /// `(rp_id, origin)` must be one of these, so a relay cannot run one for a
    /// relying party it merely names.
    pub web_relying_parties: Vec<crate::webauthn::RelyingParty>,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            bind: DEFAULT_BIND,
            event_buffer: DEFAULT_EVENT_BUFFER,
            ssh_bind_hint: String::new(),
            // Deny by default. Not derived from anything, not read from the
            // environment, not conditional on the bind address: a config that
            // has been constructed and not spoken to requires a credential.
            auth: AuthMode::Required,
            mutual_tls: false,
            require_registered_client_certificate: false,
            web_relying_parties: Vec::new(),
        }
    }
}

impl ServerConfig {
    /// Override the bind address.
    ///
    /// A non-loopback address is *allowed*, since an operator behind a firewall
    /// may genuinely want it, but while [`AUTHENTICATION_ENABLED`] is false it
    /// is logged at `error!` on every construction, because it turns the v1
    /// hole into a remote one. The log is not decoration: it is the only thing
    /// between this decision and an open admin port.
    pub fn with_bind(mut self, bind: SocketAddr) -> Self {
        self.bind = bind;
        if !self.authentication_enabled() && !self.is_loopback_bind() {
            tracing::error!(
                bind = %self.bind,
                "gunnar-grpc bound to a NON-LOOPBACK address with the allow-all development \
                 escape hatch ON. Every control-plane mutation — principals, keys, grants, \
                 branch protection, repo create/delete — is now reachable by anyone who can \
                 reach this port. Git push/fetch are NOT affected (separate SSH listener, \
                 publickey auth), but this is an open admin surface. Drop the escape hatch, \
                 or bind loopback."
            );
        }
        self
    }

    /// **DEVELOPMENT ONLY: turn gRPC authentication OFF for this listener.**
    ///
    /// This restores the v1 allow-all behaviour: no credential, no grant check,
    /// every RPC served to every caller. It exists because a developer with a
    /// freshly built server and no key registered still has to be able to reach
    /// it, and a named switch is a better answer than an undocumented hole.
    ///
    /// It is loud in four places, by design:
    ///
    /// 1. `warn!` here, the moment it is constructed;
    /// 2. `warn!` again from [`crate::warn_authentication_disabled`] at every
    ///    server start;
    /// 3. `Admin.Health` reports `grpc_authentication_enabled: false`;
    /// 4. every audit record it produces carries
    ///    `auth_mode="allow-all-insecure"` and
    ///    `principal="(unauthenticated: allow-all-insecure)"`.
    ///
    /// It is **never** the default, is not reachable from an environment
    /// variable, and there is no way to reach it from a config file. An
    /// operator has to write this method name into a program.
    pub fn insecure_allow_all_for_local_development(mut self) -> Self {
        self.auth = AuthMode::AllowAllInsecure;
        tracing::warn!(
            bind = %self.bind,
            "gRPC AUTHENTICATION HAS BEEN TURNED OFF for this listener \
             (insecure_allow_all_for_local_development). Every control-plane mutation \
             (principals, keys, grants, branch protection, repo create/delete) is served to \
             any caller that can reach this port. This is a LOCAL DEVELOPMENT switch and must \
             never appear in a deployment."
        );
        self
    }

    /// Whether **this listener** requires a credential.
    ///
    /// This, not [`AUTHENTICATION_ENABLED`], is what `Admin.Health` reports: a
    /// build that has authentication and a listener that is serving allow-all
    /// are different facts, and an operator needs the second one.
    pub fn authentication_enabled(&self) -> bool {
        self.auth.is_required()
    }

    /// The WebAuthn relying parties this server answers for
    /// (`--web-relying-party`). See [`ServerConfig::web_relying_parties`].
    pub fn with_web_relying_parties(mut self, parties: Vec<crate::webauthn::RelyingParty>) -> Self {
        self.web_relying_parties = parties;
        self
    }

    /// Refuse a peer whose client certificate is not registered to a
    /// principal, whatever header it sends (the replication door).
    pub fn with_registered_client_certificate_required(mut self, require: bool) -> Self {
        self.require_registered_client_certificate = require;
        self
    }

    /// Say that this listener was built with a [`crate::Trust`]
    /// (`--grpc-tls-self`).
    ///
    /// Reported, not enforced: the enforcement is `client_ca_root` on the
    /// transport, and this is how an operator sees that it is there.
    pub fn with_mutual_tls(mut self, mutual: bool) -> Self {
        self.mutual_tls = mutual;
        self
    }

    pub fn with_event_buffer(mut self, event_buffer: usize) -> Self {
        self.event_buffer = event_buffer.max(1);
        self
    }

    pub fn with_ssh_bind_hint(mut self, hint: impl Into<String>) -> Self {
        self.ssh_bind_hint = hint.into();
        self
    }

    /// True when [`Self::bind`] is a loopback address.
    ///
    /// This is the predicate the loopback test asserts on. It must be able to
    /// answer *false*. A predicate that is true for every input would pass the
    /// test while proving nothing (the identity-value trap: measure, never assert), so the
    /// test asserts both directions.
    pub fn is_loopback_bind(&self) -> bool {
        self.bind.ip().is_loopback()
    }
}