From a2935328ce218dc53c091816160a7a76ee62fbe5 Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Fri, 22 May 2026 14:14:56 +0200 Subject: [PATCH] Test denied remote operations do not mutate state --- crates/geth/tests/bootstrap.rs | 159 +++++++++++++++++++++++++++++++++ docs/roadmap.md | 6 +- 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs index c71bc0e..21ceb8c 100644 --- a/crates/geth/tests/bootstrap.rs +++ b/crates/geth/tests/bootstrap.rs @@ -471,6 +471,165 @@ fn sync_now_completes_owner_approved_node_enrollment_flow() { assert!(sync_status_stdout.contains("auth")); } +#[test] +fn denied_remote_operations_do_not_mutate_serving_node_state() { + let left_home = tempfile::tempdir().expect("left tempdir"); + let right_home = tempfile::tempdir().expect("right tempdir"); + if !unix_sockets_available(left_home.path()) || !unix_sockets_available(right_home.path()) { + return; + } + assert!(run_geth(left_home.path(), &["init"]).status.success()); + assert!(run_geth(right_home.path(), &["init"]).status.success()); + for home in [left_home.path(), right_home.path()] { + std::fs::write( + home.join("config.toml"), + "[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n", + ) + .expect("write config"); + } + + let mut left_daemon = spawn_daemon(left_home.path()); + let mut right_daemon = spawn_daemon(right_home.path()); + wait_for_socket(&left_home.path().join("run/geth.sock")); + wait_for_socket(&right_home.path().join("run/geth.sock")); + + let left_card_path = left_home.path().join("left-peer-card.json"); + let right_card_path = right_home.path().join("right-peer-card.json"); + let left_export = run_geth( + left_home.path(), + &[ + "peer", + "export", + "--out", + left_card_path.to_str().expect("left card path"), + ], + ); + let right_export = run_geth( + right_home.path(), + &[ + "peer", + "export", + "--out", + right_card_path.to_str().expect("right card path"), + ], + ); + assert!( + left_export.status.success(), + "left export stderr: {}", + String::from_utf8_lossy(&left_export.stderr) + ); + assert!( + right_export.status.success(), + "right export stderr: {}", + String::from_utf8_lossy(&right_export.stderr) + ); + let right_card_json = std::fs::read_to_string(&right_card_path).expect("read right card"); + let right_card: geth_discovery::PeerCard = + serde_json::from_str(&right_card_json).expect("decode right card"); + + let import_right = run_geth( + left_home.path(), + &[ + "peer", + "import", + right_card_path.to_str().expect("right card path"), + ], + ); + let import_left = run_geth( + right_home.path(), + &[ + "peer", + "import", + left_card_path.to_str().expect("left card path"), + ], + ); + assert!( + import_right.status.success(), + "left import right stderr: {}", + String::from_utf8_lossy(&import_right.stderr) + ); + assert!( + import_left.status.success(), + "right import left stderr: {}", + String::from_utf8_lossy(&import_left.stderr) + ); + + let denied_publish = run_geth( + left_home.path(), + &[ + "pubsub", + "pub", + "ops", + "should-not-stick", + "--node", + right_card.node_id.as_str(), + ], + ); + assert!( + denied_publish.status.success(), + "denied publish stderr: {}", + String::from_utf8_lossy(&denied_publish.stderr) + ); + let denied_publish_stdout = String::from_utf8_lossy(&denied_publish.stdout); + assert!(denied_publish_stdout.contains("allowed: false")); + assert!(denied_publish_stdout.contains("pubsub publish denied")); + + let right_pubsub = run_geth(right_home.path(), &["pubsub", "sub", "ops"]); + assert!( + right_pubsub.status.success(), + "right pubsub sub stderr: {}", + String::from_utf8_lossy(&right_pubsub.stderr) + ); + assert!(String::from_utf8_lossy(&right_pubsub.stdout).contains("messages: 0")); + + let denied_pipe_listen = run_geth( + left_home.path(), + &[ + "pipe", + "listen", + "inbox", + "--node", + right_card.node_id.as_str(), + ], + ); + assert!( + denied_pipe_listen.status.success(), + "denied pipe listen stderr: {}", + String::from_utf8_lossy(&denied_pipe_listen.stderr) + ); + let denied_pipe_stdout = String::from_utf8_lossy(&denied_pipe_listen.stdout); + assert!(denied_pipe_stdout.contains("allowed: false")); + assert!(denied_pipe_stdout.contains("pipe listen denied")); + + let right_pipe_connect = run_geth(right_home.path(), &["pipe", "connect", "inbox"]); + assert!( + right_pipe_connect.status.success(), + "right pipe connect stderr: {}", + String::from_utf8_lossy(&right_pipe_connect.stderr) + ); + assert!( + String::from_utf8_lossy(&right_pipe_connect.stdout).contains("local_listener_found: false") + ); + + let denied_admin_shell = run_geth( + left_home.path(), + &["ssh", "admin-shell", right_card.node_id.as_str(), "status"], + ); + let _ = left_daemon.kill(); + let _ = right_daemon.kill(); + let _ = left_daemon.wait(); + let _ = right_daemon.wait(); + + assert!( + denied_admin_shell.status.success(), + "denied admin shell stderr: {}", + String::from_utf8_lossy(&denied_admin_shell.stderr) + ); + let denied_admin_stdout = String::from_utf8_lossy(&denied_admin_shell.stdout); + assert!(denied_admin_stdout.contains("allowed: false")); + assert!(denied_admin_stdout.contains("ssh admin shell denied")); +} + #[test] fn peer_card_export_import_and_list_are_candidate_only() { let source_home = tempfile::tempdir().expect("source tempdir"); diff --git a/docs/roadmap.md b/docs/roadmap.md index da74b44..d5a9364 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -18,7 +18,9 @@ Implementation order: 1. `[~]` Close remote authorization and replicated-state safety gaps. Acceptance criteria: - - `[ ]` Add tests proving denied remote operations do not mutate local state. + - `[x]` Add initial two-daemon tests proving denied remote pubsub publish, + remote pipe listen, and SSH admin shell requests do not mutate serving + node state. - `[ ]` Add tests proving unsigned, invalidly signed, and conflicting replicated records do not mutate trust/resource state. - `[ ]` Improve `auth explain` diagnostics enough for operators to @@ -74,6 +76,8 @@ Implementation order: check before mutating local state or opening a host service. - `[ ]` Tests cover denied and allowed paths for CAS, KV, DB, document, pubsub, pipe, SSH proxy/admin shell, SSH cert metadata, and revocations. + - `[x]` Initial two-daemon denied-mutation coverage exists for remote pubsub + publish, remote pipe listen, and SSH admin shell. - `[ ]` `auth explain` output can explain discovered-only peers, missing endpoint bindings, missing grants, matching grants, revocations, and bearer access.