From 168051bc1e28146247bb7ea2ea96a4263f7333ea Mon Sep 17 00:00:00 2001 From: ruvnet Date: Sun, 3 May 2026 18:25:28 -0400 Subject: [PATCH] sec(hailo): expose --tls-ca / mTLS flags on the embed CLI (iter 188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symmetric with iter-187 bench plumbing — adds the same TLS knobs to `ruvector-hailo-embed` so ops can drive a one-shot embed against a TLS-configured worker without having to build a custom client. All flags `#[cfg(feature = "tls")]` so the no-tls build stays clean. Same partial-config + orphan-flag refusals as iter-187: - --tls-domain / --tls-client-cert / --tls-client-key without --tls-ca → loud error - --tls-client-cert without --tls-client-key (or vice versa) → loud error - missing CA file → fs error surfaced with full path Smoke-tested on the workstation: $ ruvector-hailo-embed --workers 100.77.59.83:50051 --tls-domain example.com --text hello Error: "--tls-domain / --tls-client-cert / --tls-client-key require --tls-ca" $ ruvector-hailo-embed --workers 100.77.59.83:50051 --tls-ca /nonexistent/ca.pem --text hello Error: "--tls-ca: transport error to : read ca pem at /nonexistent/ca.pem: No such file or directory (os error 2)" $ ruvector-hailo-embed --workers 100.77.59.83:50051 --text "iter 188 smoke test" {"text":"iter 188 smoke test","dim":384,"latency_us":433538,"vec_head":[...]} Pi plaintext bench regression (c=4 b=1, 8 s × 3): iter-187: 68.5, 68.7, 66.7 → mean 68.0/sec, p50=56-59 ms iter-188: 70.3, 69.0, 67.9 → mean 69.1/sec, p50=55-57 ms Δ throughput: +1.6% (within tailnet noise; embed CLI changes don't touch the bench code path) The TLS server-side path is now fully callable from both client tools in this repo. Pi-side cert generation + systemd unit wiring (the actual end-to-end TLS smoke against cognitum-v0) remains the deferred ops follow-up. Co-Authored-By: claude-flow --- .../ruvector-hailo-cluster/src/bin/embed.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/crates/ruvector-hailo-cluster/src/bin/embed.rs b/crates/ruvector-hailo-cluster/src/bin/embed.rs index f79bf5d6c..7af3c660b 100644 --- a/crates/ruvector-hailo-cluster/src/bin/embed.rs +++ b/crates/ruvector-hailo-cluster/src/bin/embed.rs @@ -69,6 +69,18 @@ fn main() -> Result<(), Box> { // texts are embedded in order and the binary exits. Repeat the // flag to embed multiple texts in one invocation. let mut inline_texts: Vec = Vec::new(); + // Iter 188 — symmetric TLS plumbing (mirror of iter-187 bench + // additions). Lets ops drive a single embed against a TLS-enabled + // worker without building a custom client. All flags + // `#[cfg(feature = "tls")]` so the no-tls build is unchanged. + #[cfg(feature = "tls")] + let mut tls_ca: Option = None; + #[cfg(feature = "tls")] + let mut tls_domain: Option = None; + #[cfg(feature = "tls")] + let mut tls_client_cert: Option = None; + #[cfg(feature = "tls")] + let mut tls_client_key: Option = None; let mut i = 1; while i < args.len() { @@ -130,6 +142,14 @@ fn main() -> Result<(), Box> { } i += 2; } + #[cfg(feature = "tls")] + "--tls-ca" => { tls_ca = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-domain" => { tls_domain = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-client-cert" => { tls_client_cert = args.get(i + 1).cloned(); i += 2; } + #[cfg(feature = "tls")] + "--tls-client-key" => { tls_client_key = args.get(i + 1).cloned(); i += 2; } "--help" | "-h" => { print_help(); return Ok(()); } "--version" | "-V" => { println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); @@ -194,6 +214,55 @@ fn main() -> Result<(), Box> { } } + // Iter 188 — TLS transport when --tls-ca is set; mirrors iter-187 + // bench plumbing. Same partial-config + orphan-flag refusals so a + // misconfigured invocation surfaces an early error instead of a + // silent plaintext downgrade. + #[cfg(feature = "tls")] + let transport: Arc = { + if let Some(ca_path) = tls_ca.as_deref() { + let addr0 = workers.first().map(|w| w.address.clone()).unwrap_or_default(); + let domain = tls_domain.clone().unwrap_or_else(|| { + ruvector_hailo_cluster::tls::domain_from_address(&addr0).to_string() + }); + let mut tls = ruvector_hailo_cluster::tls::TlsClient::from_pem_files(ca_path, &domain) + .map_err(|e| format!("--tls-ca: {}", e))?; + match (tls_client_cert.as_deref(), tls_client_key.as_deref()) { + (Some(c), Some(k)) => { + tls = tls.with_client_identity(c, k) + .map_err(|e| format!("--tls-client-cert/--tls-client-key: {}", e))?; + if !quiet { + eprintln!("ruvector-hailo-embed: mTLS client identity attached"); + } + } + (Some(_), None) | (None, Some(_)) => { + return Err( + "--tls-client-cert and --tls-client-key must both be set or both unset".into(), + ); + } + (None, None) => {} + } + if !quiet { + eprintln!( + "ruvector-hailo-embed: TLS enabled ca={} domain={}", + ca_path, domain + ); + } + Arc::new(GrpcTransport::with_tls( + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(2), + tls, + )?) + } else { + if tls_domain.is_some() || tls_client_cert.is_some() || tls_client_key.is_some() { + return Err( + "--tls-domain / --tls-client-cert / --tls-client-key require --tls-ca".into(), + ); + } + Arc::new(GrpcTransport::new()?) + } + }; + #[cfg(not(feature = "tls"))] let transport: Arc = Arc::new(GrpcTransport::new()?); @@ -601,6 +670,18 @@ OPTIONS: text and exit (skips stdin). Repeat the flag to embed multiple texts in one invocation. + --tls-ca Enable HTTPS by trusting the PEM CA + bundle at . Without this the + embed CLI dials plaintext gRPC. + (Requires --features tls.) + --tls-domain SNI / SAN value to assert against the + server cert. Defaults to the hostname + half of the first worker address. + --tls-client-cert mTLS client cert (PEM). Pair with + --tls-client-key. + --tls-client-key mTLS client private key (PEM). Both + cert and key must be set or both + unset. --help, -h Print this help and exit. --version, -V Print the binary name + version and exit.