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/sso_egress_private.rs

//! **An account's own IdP CA and the operator's internal networks, at the
//! egress** (owner 2026-10-06: OIDC to a company IdP behind its own CA, on a
//! private gunnar whose IdP lives on the company's network).
//!
//! * The CA an account admin pasted is trusted for THAT call alone, beside
//!   the public roots: without it — or with another CA — the IdP's
//!   certificate is refused, with it the IdP answers.
//! * A network the operator listed admits the addresses inside it, live,
//!   through the same pinned resolver; nothing beside it moves.
//!
//! No real DNS (a test resolver hook) and no real CA (rcgen, minted here).
#![cfg(feature = "sso-egress")] // A32-18: the airgap default carries no HTTP client; the appliance rows and CI name `sso-egress`.

use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::sync::{Arc, RwLock};

use gunnar_grpc::sso::{Egress as _, PrivateNets};
use gunnar_server::sso_egress::{GuardedResolver, Lookup, UreqEgress};

fn hook(ip: &str) -> Lookup {
    let ip: std::net::IpAddr = ip.parse().expect("an address");
    Arc::new(move |_host: &str, port: u16| Ok(vec![SocketAddr::new(ip, port)]))
}

/// A CA, and a server certificate for `name` issued under it: (CA DER, leaf DER, leaf key DER).
fn issued(name: &str) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
    let ca_key = rcgen::KeyPair::generate().expect("a key");
    let mut ca = rcgen::CertificateParams::new(Vec::<String>::new()).expect("params");
    ca.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    ca.key_usages = vec![
        rcgen::KeyUsagePurpose::KeyCertSign,
        rcgen::KeyUsagePurpose::CrlSign,
    ];
    ca.distinguished_name
        .push(rcgen::DnType::CommonName, "a company's own CA");
    let ca_cert = ca.self_signed(&ca_key).expect("the CA");
    let issuer = rcgen::Issuer::new(ca, ca_key);
    let leaf_key = rcgen::KeyPair::generate().expect("a key");
    let leaf = rcgen::CertificateParams::new(vec![name.to_owned()]).expect("params");
    let leaf_cert = leaf.signed_by(&leaf_key, &issuer).expect("the leaf");
    (
        ca_cert.der().to_vec(),
        leaf_cert.der().to_vec(),
        leaf_key.serialize_der(),
    )
}

/// An https IdP on loopback presenting `leaf`: every request is answered
/// `200 ok`; a handshake the client refuses is dropped.
fn https_idp(leaf: Vec<u8>, key: Vec<u8>) -> u16 {
    let provider = Arc::new(rustls::crypto::ring::default_provider());
    let config = rustls::ServerConfig::builder_with_provider(provider)
        .with_safe_default_protocol_versions()
        .expect("versions")
        .with_no_client_auth()
        .with_single_cert(
            vec![leaf.into()],
            rustls::pki_types::PrivateKeyDer::Pkcs8(key.into()),
        )
        .expect("the server certificate");
    let config = Arc::new(config);
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().expect("an address").port();
    std::thread::spawn(move || {
        for stream in listener.incoming() {
            let Ok(stream) = stream else { continue };
            let config = Arc::clone(&config);
            std::thread::spawn(move || {
                let Ok(conn) = rustls::ServerConnection::new(config) else {
                    return;
                };
                let mut tls = rustls::StreamOwned::new(conn, stream);
                let mut reader = BufReader::new(&mut tls);
                let mut length = 0usize;
                loop {
                    let mut line = String::new();
                    if reader.read_line(&mut line).map_or(true, |n| n == 0) {
                        return;
                    }
                    if let Some(v) = line.to_ascii_lowercase().strip_prefix("content-length:") {
                        length = v.trim().parse().unwrap_or(0);
                    }
                    if line == "\r\n" {
                        break;
                    }
                }
                let mut body = vec![0u8; length];
                let _ = reader.read_exact(&mut body);
                let _ = tls.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok");
                let _ = tls.flush();
            });
        }
    });
    port
}

/// ★ **The account's CA, and only for its call**: the public roots alone do
/// not trust a company's own CA; another company's CA does not either; the
/// CA the account admin pasted does — for discovery and for the token POST.
#[test]
fn an_accounts_own_ca_is_trusted_for_its_idp_call_and_no_other() {
    let name = "idp.ca-test.example";
    let (ca, leaf, key) = issued(name);
    let (other_ca, _, _) = issued(name);
    let port = https_idp(leaf, key);
    // The test door admits this box's loopback address; https is still https.
    let egress = UreqEgress::with_lookup(true, hook("127.0.0.1"));
    let url = format!("https://{name}:{port}/realms/acme/.well-known/openid-configuration");
    let public_only = egress
        .get(&url)
        .expect_err("the public roots do not know a company's CA");
    assert!(
        !public_only.contains("resolves to"),
        "refused by TLS, not the guard: {public_only}"
    );
    egress
        .get_trusting(&url, &[other_ca])
        .expect_err("another company's CA is not this IdP's");
    let (status, body) = egress
        .get_trusting(&url, std::slice::from_ref(&ca))
        .expect("the account's CA");
    assert_eq!((status, body.as_str()), (200, "ok"));
    let (status, _) = egress
        .post_form_trusting(
            &format!("https://{name}:{port}/realms/acme/token"),
            "",
            "grant_type=authorization_code",
            &[ca],
        )
        .expect("the token endpoint, with the account's CA");
    assert_eq!(status, 200);
    egress
        .get(&url)
        .expect_err("the CA was for that call alone");
}

/// ★ **A network the operator listed admits its addresses — live — and
/// nothing beside it**; an empty list is today's guard.
#[test]
fn a_listed_internal_network_admits_its_addresses_and_nothing_beside() {
    let nets = Arc::new(RwLock::new(PrivateNets::default()));
    let resolver = GuardedResolver::with_nets(hook("10.20.30.40"), false, Arc::clone(&nets));
    let err = resolver
        .resolve_checked("idp.corp.example", 443)
        .expect_err("no list: refused");
    assert!(err.contains("10.20.30.40"), "{err}");
    *nets.write().unwrap() = PrivateNets::parse("10.20.30.40/32").expect("a private network");
    let addrs = resolver
        .resolve_checked("idp.corp.example", 443)
        .expect("listed, live");
    assert_eq!(
        addrs,
        vec!["10.20.30.40:443".parse::<SocketAddr>().unwrap()],
        "connected to exactly what was checked"
    );
    let beside = GuardedResolver::with_nets(hook("10.20.30.41"), false, Arc::clone(&nets));
    assert!(
        beside.resolve_checked("idp.corp.example", 443).is_err(),
        "the next address is not listed"
    );
    let metadata = GuardedResolver::with_nets(hook("169.254.169.254"), false, Arc::clone(&nets));
    assert!(metadata.resolve_checked("idp.corp.example", 443).is_err());
    // A literal in the URL is judged by the same list.
    let egress = UreqEgress::with_nets(false, hook("10.20.30.41"), Arc::clone(&nets));
    let literal = egress
        .get("https://10.20.30.41/realms/acme/.well-known/openid-configuration")
        .expect_err("not listed");
    assert!(literal.contains("private"), "{literal}");
}