1use std::future::Future;
8use std::io;
9use std::os::unix::fs::PermissionsExt as _;
10use std::path::Path;
11use std::sync::Arc;
12
13use basil_proto::broker::v1::admin_service_server::AdminServiceServer;
14use basil_proto::broker::v1::aead_service_server::AeadServiceServer;
15use basil_proto::broker::v1::invocation_service_server::InvocationServiceServer;
16use basil_proto::broker::v1::minting_service_server::MintingServiceServer;
17use basil_proto::broker::v1::nats_service_server::NatsServiceServer;
18use basil_proto::broker::v1::secret_service_server::SecretServiceServer;
19use basil_proto::broker::v1::signing_service_server::SigningServiceServer;
20use basil_proto::envoy::service::secret::v3::secret_discovery_service_server::SecretDiscoveryServiceServer;
21use basil_proto::spiffe::spiffe_workload_api_server::SpiffeWorkloadApiServer;
22use tokio::net::UnixListener;
23use tokio::signal::unix::{SignalKind, signal};
24use tokio_stream::wrappers::UnixListenerStream;
25use tonic::transport::Server;
26use tracing::{info, warn};
27
28use crate::grpc::BrokerGrpc;
29use crate::sds::EnvoySdsGrpc;
30use crate::service::broker::InvocationRuntimeConfig;
31use crate::spiffe::SpiffeWorkloadGrpc;
32use crate::state::BrokerState;
33
34pub const DEFAULT_SOCKET_MODE: u32 = 0o600;
36
37#[derive(Debug, Clone)]
39pub struct ServerConfig {
40 pub socket_path: String,
42 pub socket_mode: u32,
44 pub socket_group: Option<String>,
46 pub invocation: InvocationRuntimeConfig,
48}
49
50pub async fn run(config: ServerConfig, state: Arc<BrokerState>) -> std::io::Result<()> {
55 serve_with_shutdown(config, state, shutdown_signal()).await
56}
57
58async fn serve_with_shutdown(
59 config: ServerConfig,
60 state: Arc<BrokerState>,
61 shutdown: impl Future<Output = ()>,
62) -> std::io::Result<()> {
63 let path = config.socket_path;
64 if Path::new(&path).exists() {
65 std::fs::remove_file(&path)?;
66 warn!(%path, "removed stale socket");
67 }
68
69 let listener = UnixListener::bind(&path)?;
70 apply_socket_permissions(&path, config.socket_mode, config.socket_group.as_deref())?;
71
72 info!(
73 %path,
74 mode = %format_socket_mode(config.socket_mode),
75 group = ?config.socket_group,
76 backend = state.backend_label(),
77 "basil gRPC agent listening"
78 );
79 let incoming = UnixListenerStream::new(listener);
80 let broker = BrokerGrpc::new_with_invocation_config(state.clone(), config.invocation);
81
82 let server = Server::builder()
83 .add_service(InvocationServiceServer::new(broker.clone()))
84 .add_service(SigningServiceServer::new(broker.clone()))
85 .add_service(AeadServiceServer::new(broker.clone()))
86 .add_service(SecretServiceServer::new(broker.clone()))
87 .add_service(MintingServiceServer::new(broker.clone()))
88 .add_service(NatsServiceServer::new(broker.clone()))
89 .add_service(AdminServiceServer::new(broker));
90 let server = server
91 .add_service(SpiffeWorkloadApiServer::new(SpiffeWorkloadGrpc::new(
92 state.clone(),
93 )))
94 .add_service(SecretDiscoveryServiceServer::new(EnvoySdsGrpc::new(state)));
95 let result = server
96 .serve_with_incoming_shutdown(incoming, shutdown)
97 .await;
98
99 if let Err(e) = std::fs::remove_file(&path)
100 && e.kind() != std::io::ErrorKind::NotFound
101 {
102 warn!(?e, %path, "could not remove socket on shutdown");
103 }
104
105 result.map_err(std::io::Error::other)
106}
107
108fn apply_socket_permissions(path: &str, mode: u32, group: Option<&str>) -> io::Result<()> {
109 if let Some(group) = group {
110 let gid = resolve_group(group)?;
111 std::os::unix::fs::chown(path, None, Some(gid))?;
112 }
113 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
114}
115
116fn resolve_group(group: &str) -> io::Result<u32> {
117 if let Ok(gid) = group.parse::<u32>() {
118 return Ok(gid);
119 }
120 resolve_group_from(group, "/etc/group")
121}
122
123fn resolve_group_from(group: &str, group_file: impl AsRef<Path>) -> io::Result<u32> {
124 let body = std::fs::read_to_string(group_file)?;
125 for line in body.lines() {
126 let mut fields = line.split(':');
127 let Some(name) = fields.next() else {
128 continue;
129 };
130 if name != group {
131 continue;
132 }
133 let _passwd = fields.next();
134 let Some(gid) = fields.next() else {
135 break;
136 };
137 return gid.parse::<u32>().map_err(|err| {
138 io::Error::new(
139 io::ErrorKind::InvalidData,
140 format!("group `{group}` has invalid gid `{gid}`: {err}"),
141 )
142 });
143 }
144 Err(io::Error::new(
145 io::ErrorKind::NotFound,
146 format!("group `{group}` not found"),
147 ))
148}
149
150fn format_socket_mode(mode: u32) -> String {
151 format!("{mode:04o}")
152}
153
154async fn shutdown_signal() {
155 let mut int = signal(SignalKind::interrupt()).ok();
156 let mut quit = signal(SignalKind::quit()).ok();
157 let mut term = signal(SignalKind::terminate()).ok();
158
159 tokio::select! {
160 () = async {
161 if let Some(sig) = int.as_mut() {
162 sig.recv().await;
163 } else {
164 std::future::pending::<()>().await;
165 }
166 } => {}
167 () = async {
168 if let Some(sig) = quit.as_mut() {
169 sig.recv().await;
170 } else {
171 std::future::pending::<()>().await;
172 }
173 } => {}
174 () = async {
175 if let Some(sig) = term.as_mut() {
176 sig.recv().await;
177 } else {
178 std::future::pending::<()>().await;
179 }
180 } => {}
181 }
182}
183
184#[cfg(test)]
185#[allow(clippy::significant_drop_tightening)]
186mod tests {
187 use std::collections::BTreeMap;
188 use std::path::{Path, PathBuf};
189 use std::sync::Arc;
190 use std::time::Duration;
191
192 use async_trait::async_trait;
193 use basil::Client;
194 use basil_proto::KeyType;
195 use basil_proto::broker::v1::SealedRequest;
196 use basil_proto::broker::v1::StatusRequest;
197 use basil_proto::broker::v1::admin_service_client::AdminServiceClient;
198 use basil_proto::broker::v1::invocation_service_client::InvocationServiceClient;
199 use basil_proto::envoy::service::discovery::v3::DiscoveryRequest;
200 use basil_proto::envoy::service::secret::v3::secret_discovery_service_client::SecretDiscoveryServiceClient;
201 use basil_proto::spiffe::X509BundlesRequest;
202 use basil_proto::spiffe::spiffe_workload_api_client::SpiffeWorkloadApiClient;
203 use hyper_util::rt::TokioIo;
204 use tokio::net::UnixStream;
205 use tokio::sync::oneshot;
206 use tonic::Code;
207 use tonic::Request;
208 use tonic::metadata::MetadataValue;
209 use tonic::transport::{Channel, Endpoint, Uri};
210 use tower::service_fn;
211
212 use super::*;
213 use crate::backend::{Backend, BackendError, NewKey};
214 use crate::catalog::load;
215 use crate::manager::BackendManager;
216
217 struct DummyBackend;
218
219 #[async_trait]
220 impl Backend for DummyBackend {
221 fn kind(&self) -> &'static str {
222 "dummy"
223 }
224
225 async fn new_key(&self, key_type: KeyType) -> Result<NewKey, BackendError> {
226 let _ = key_type;
227 Err(BackendError::Unsupported("new_key"))
228 }
229
230 async fn public_key(&self, key_id: &str) -> Result<Vec<u8>, BackendError> {
231 let _ = key_id;
232 Err(BackendError::Unsupported("public_key"))
233 }
234
235 async fn sign(&self, key_id: &str, message: &[u8]) -> Result<Vec<u8>, BackendError> {
236 let _ = (key_id, message);
237 Err(BackendError::Unsupported("sign"))
238 }
239
240 async fn verify(
241 &self,
242 key_id: &str,
243 message: &[u8],
244 signature: &[u8],
245 ) -> Result<bool, BackendError> {
246 let _ = (key_id, message, signature);
247 Err(BackendError::Unsupported("verify"))
248 }
249 }
250
251 fn state() -> Arc<BrokerState> {
252 let catalog = r#"{
253 "schemaVersion": 1,
254 "backends": { "bao": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
255 "keys": {}
256 }"#;
257 let policy = r#"{
258 "roles": {},
259 "rules": [],
260 "config": { "names": { "users": {}, "groups": {} }, "memberships": {} }
261 }"#;
262 let (catalog, policy, config, warnings) = load(catalog, policy).expect("fixture loads");
263 assert!(warnings.is_empty());
264 let mut backends: BTreeMap<String, Box<dyn Backend>> = BTreeMap::new();
265 backends.insert("bao".to_string(), Box::new(DummyBackend));
266 let manager = BackendManager::new(catalog.clone(), backends).expect("manager builds");
267 Arc::new(BrokerState::new(catalog, policy, config, manager, "dummy"))
268 }
269
270 async fn spawn_server(socket: PathBuf) -> oneshot::Sender<()> {
271 let (tx, rx) = oneshot::channel();
272 let config = ServerConfig {
273 socket_path: socket.to_string_lossy().into_owned(),
274 socket_mode: DEFAULT_SOCKET_MODE,
275 socket_group: None,
276 invocation: InvocationRuntimeConfig::default(),
277 };
278 tokio::spawn(async move {
279 serve_with_shutdown(config, state(), async {
280 let _ = rx.await;
281 })
282 .await
283 .expect("server exits cleanly");
284 });
285 wait_for_socket(&socket).await;
286 tx
287 }
288
289 async fn wait_for_socket(socket: &Path) {
290 for _ in 0..100 {
291 if socket.exists() {
292 return;
293 }
294 tokio::time::sleep(Duration::from_millis(10)).await;
295 }
296 panic!("server socket did not appear: {}", socket.display());
297 }
298
299 fn socket_path(name: &str) -> PathBuf {
300 std::env::temp_dir().join(format!("basil-{name}-{}.sock", uuid::Uuid::new_v4()))
301 }
302
303 #[test]
304 fn group_resolution_accepts_numeric_gid_and_group_file_name() {
305 assert_eq!(resolve_group("4242").expect("numeric gid"), 4242);
306 let path = std::env::temp_dir().join(format!(
307 "basil-group-test-{}-{}",
308 std::process::id(),
309 uuid::Uuid::new_v4()
310 ));
311 std::fs::write(&path, "root:x:0:\nbasil-edge:x:9876:edge\n").expect("write group fixture");
312 assert_eq!(
313 resolve_group_from("basil-edge", &path).expect("named group"),
314 9876
315 );
316 let err = resolve_group_from("missing", &path).expect_err("missing group");
317 assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
318 std::fs::remove_file(path).expect("remove group fixture");
319 }
320
321 #[tokio::test]
322 async fn configured_socket_mode_is_applied_before_serving() {
323 let socket = socket_path("mode");
324 let (tx, rx) = oneshot::channel();
325 let config = ServerConfig {
326 socket_path: socket.to_string_lossy().into_owned(),
327 socket_mode: 0o660,
328 socket_group: None,
329 invocation: InvocationRuntimeConfig::default(),
330 };
331 tokio::spawn(async move {
332 serve_with_shutdown(config, state(), async {
333 let _ = rx.await;
334 })
335 .await
336 .expect("server exits cleanly");
337 });
338 wait_for_socket(&socket).await;
339 let mode = std::fs::metadata(&socket)
340 .expect("stat socket")
341 .permissions()
342 .mode()
343 & 0o777;
344 assert_eq!(mode, 0o660);
345 let _ = tx.send(());
346 }
347
348 #[test]
349 fn active_runtime_sources_have_no_legacy_json_wire_symbols() {
350 let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
351 let banned = [
352 ["json", "_codec"].concat(),
353 ["Client", "Request"].concat(),
354 ["Client", "Response"].concat(),
355 ["core", "::", "handler"].concat(),
356 ["core", "::", "server"].concat(),
357 ];
358 let mut stack = vec![src];
359 while let Some(path) = stack.pop() {
360 for entry in std::fs::read_dir(&path).expect("source directory readable") {
361 let entry = entry.expect("source entry readable");
362 let path = entry.path();
363 if path.is_dir() {
364 stack.push(path);
365 continue;
366 }
367 if path.extension().and_then(std::ffi::OsStr::to_str) != Some("rs") {
368 continue;
369 }
370 let source = std::fs::read_to_string(&path).expect("source file readable");
371 for needle in &banned {
372 assert!(
373 !source.contains(needle),
374 "legacy JSON wire symbol `{needle}` remains in {}",
375 path.display()
376 );
377 }
378 }
379 }
380 }
381
382 async fn uds_channel(path: &Path) -> Channel {
383 let path = path.to_path_buf();
384 Endpoint::try_from("http://[::]:50051")
385 .expect("endpoint")
386 .connect_with_connector(service_fn(move |_: Uri| {
387 let path = path.clone();
388 async move { UnixStream::connect(path).await.map(TokioIo::new) }
389 }))
390 .await
391 .expect("connect")
392 }
393
394 #[tokio::test]
395 async fn broker_grpc_serves_status_on_unix_socket() {
396 let socket = socket_path("broker-only");
397 let shutdown = spawn_server(socket.clone()).await;
398 {
399 let mut client = Client::connect(socket.to_str().expect("utf8 path"))
400 .await
401 .expect("broker client connects");
402 let status = client.status().await.expect("status");
403 assert_eq!(status.backend, "dummy");
404 assert_eq!(status.protocol, 1);
405 }
406 let _ = shutdown.send(());
407 }
408
409 #[tokio::test]
410 async fn invocation_service_is_registered_but_disabled_by_default() {
411 let socket = socket_path("invocation-disabled");
412 let shutdown = spawn_server(socket.clone()).await;
413
414 let channel = uds_channel(&socket).await;
415 let mut invocation = InvocationServiceClient::new(channel);
416 let status = invocation
417 .invoke(SealedRequest::default())
418 .await
419 .expect_err("invocation is disabled by default");
420 assert_eq!(status.code(), Code::FailedPrecondition);
421
422 let mut broker = Client::connect(socket.to_str().expect("utf8 path"))
423 .await
424 .expect("broker client still connects");
425 let status = broker
426 .status()
427 .await
428 .expect("typed status remains available");
429 assert_eq!(status.protocol, 1);
430
431 let _ = shutdown.send(());
432 }
433 #[tokio::test]
434 async fn broker_and_spiffe_services_share_one_unix_socket() {
435 let socket = socket_path("broker-spiffe");
436 let shutdown = spawn_server(socket.clone()).await;
437
438 {
439 let mut broker = Client::connect(socket.to_str().expect("utf8 path"))
440 .await
441 .expect("broker client connects");
442 let status = broker.status().await.expect("status");
443 assert_eq!(status.backend, "dummy");
444 assert_eq!(status.protocol, 1);
445 }
446
447 {
448 let channel = uds_channel(&socket).await;
449 let mut broker = AdminServiceClient::new(channel.clone());
450 let mut request = Request::new(StatusRequest {});
451 request
452 .metadata_mut()
453 .insert("workload.spiffe.io", "true".parse().expect("metadata"));
454 let status = broker
455 .status(request)
456 .await
457 .expect("broker RPC ignores Workload API metadata")
458 .into_inner();
459 assert_eq!(status.backend, "dummy");
460 assert_eq!(status.protocol, 1);
461 }
462
463 {
464 let channel = uds_channel(&socket).await;
465 let mut spiffe = SpiffeWorkloadApiClient::new(channel);
466 let status = spiffe
467 .fetch_x509_bundles(X509BundlesRequest {})
468 .await
469 .expect_err("registered SPIFFE service rejects missing workload header");
470 assert_eq!(status.code(), Code::InvalidArgument);
471 assert_eq!(
472 status.message(),
473 "SPIFFE Workload API requests require workload.spiffe.io=true"
474 );
475 }
476
477 {
478 let channel = uds_channel(&socket).await;
479 let mut spiffe = SpiffeWorkloadApiClient::new(channel);
480 let mut request = Request::new(X509BundlesRequest {});
481 request
482 .metadata_mut()
483 .append("workload.spiffe.io", "true".parse().expect("metadata"));
484 request
485 .metadata_mut()
486 .append("workload.spiffe.io", "false".parse().expect("metadata"));
487 let status = spiffe
488 .fetch_x509_bundles(request)
489 .await
490 .expect_err("duplicate Workload API metadata is fail-closed");
491 assert_eq!(status.code(), Code::InvalidArgument);
492 }
493
494 {
495 let channel = uds_channel(&socket).await;
496 let mut spiffe = SpiffeWorkloadApiClient::new(channel);
497 let mut request = Request::new(X509BundlesRequest {});
498 request
499 .metadata_mut()
500 .insert_bin("workload.spiffe.io-bin", MetadataValue::from_bytes(b"true"));
501 let status = spiffe
502 .fetch_x509_bundles(request)
503 .await
504 .expect_err("binary Workload API metadata is fail-closed");
505 assert_eq!(status.code(), Code::InvalidArgument);
506 }
507
508 {
509 let channel = uds_channel(&socket).await;
510 let mut sds = SecretDiscoveryServiceClient::new(channel);
511 let status = sds
512 .fetch_secrets(DiscoveryRequest {
513 version_info: String::new(),
514 node: None,
515 resource_names: vec!["default".to_string()],
516 type_url: crate::sds::SECRET_TYPE_URL.to_string(),
517 response_nonce: String::new(),
518 error_detail: None,
519 })
520 .await
521 .expect_err("registered SDS service has no configured resources");
522 assert_eq!(status.code(), Code::NotFound);
523 }
524 let _ = shutdown.send(());
525 }
526}