Skip to main content

boatramp_node/
auth.rs

1//! Control-plane auth assembly: build the [`Auth`](boatramp_server::Auth) from
2//! the resolved root-key settings, and the fail-closed bind guard that refuses
3//! to expose an unauthenticated control plane on a public listener.
4//!
5//! Moved out of the `boatramp` binary's `serve` path so an in-process embedder —
6//! or a fidelity test — assembles auth exactly as `boatramp serve` does.
7
8use std::net::SocketAddr;
9use std::sync::Arc;
10
11use boatramp_core::kv::KvStore;
12
13use crate::error::{Error, Result};
14
15/// Build the control-plane [`Auth`](boatramp_server::Auth) from the resolved
16/// root-key settings (flag/env > `serve` config). For an issuing node (a
17/// private key or an external signer) it also sets `options.issuer` so the
18/// token-create and OIDC-exchange routes can mint. No key ⇒ auth disabled (dev).
19pub async fn configure_auth(
20    signer: Option<&crate::config::AuthSignerConfig>,
21    private_key: Option<String>,
22    public_key: Option<String>,
23    options: &mut boatramp_server::ServerOptions,
24    kv: Arc<dyn KvStore>,
25) -> Result<boatramp_server::Auth> {
26    use boatramp_core::cose::{LocalSigner, Signer, TokenPublicKey};
27    // An external signer (KMS/HSM/Vault) issues *and* provides the trust anchor:
28    // it resolves its own public key at connect.
29    if let Some(cfg) = signer {
30        let issuer = boatramp_server::signer::build_signer(&cfg.to_signer_config())
31            .await
32            .map_err(|e| Error::AuthPrivKey(e.to_string()))?;
33        let public = issuer.public_key();
34        options.issuer = Some(issuer);
35        return Ok(boatramp_server::Auth::with_key(public, kv));
36    }
37    if let Some(hex) = private_key {
38        let signer =
39            LocalSigner::from_private_hex(&hex).map_err(|e| Error::AuthPrivKey(e.to_string()))?;
40        let public = signer.public_key();
41        options.issuer = Some(Arc::new(signer) as Arc<dyn Signer>);
42        return Ok(boatramp_server::Auth::with_key(public, kv));
43    }
44    if let Some(hex) = public_key {
45        let public =
46            TokenPublicKey::from_hex(&hex).map_err(|e| Error::AuthPubKey(e.to_string()))?;
47        return Ok(boatramp_server::Auth::with_key(public, kv));
48    }
49    Ok(boatramp_server::Auth::disabled())
50}
51
52/// Fail-closed bind guard: refuse to expose an unauthenticated control plane on a
53/// non-loopback listener unless the posture explicitly allows it, and warn loudly
54/// for any auth-disabled listener.
55pub fn enforce_auth_bind(
56    addr: SocketAddr,
57    auth: &boatramp_server::Auth,
58    posture: &boatramp_core::security::SecurityPosture,
59) -> Result<()> {
60    if auth.is_disabled() {
61        if !addr.ip().is_loopback() && !posture.allow_unauthenticated_public_bind {
62            return Err(Error::UnauthenticatedPublicBind { addr });
63        }
64        tracing::warn!(
65            %addr,
66            "control-plane auth is DISABLED — do not expose this listener to an untrusted network"
67        );
68    }
69    Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use boatramp_core::security::SecurityProfile;
76
77    /// An auth-disabled non-loopback bind is refused under the strict
78    /// posture, allowed on loopback, and allowed when the posture opts in.
79    #[test]
80    fn fail_closed_refuses_unauthenticated_public_bind() {
81        let disabled = boatramp_server::Auth::disabled();
82        let strict = SecurityProfile::MultiTenant.preset();
83        let dev = SecurityProfile::Dev.preset();
84        let public: SocketAddr = "0.0.0.0:8080".parse().unwrap();
85        let loopback: SocketAddr = "127.0.0.1:8080".parse().unwrap();
86
87        // Auth disabled + public + strict → refused.
88        assert!(matches!(
89            enforce_auth_bind(public, &disabled, &strict),
90            Err(Error::UnauthenticatedPublicBind { .. })
91        ));
92        // Loopback is always permitted (local-dev convenience).
93        assert!(enforce_auth_bind(loopback, &disabled, &strict).is_ok());
94        // The `dev` posture opts into an unauthenticated public bind.
95        assert!(enforce_auth_bind(public, &disabled, &dev).is_ok());
96    }
97}