Add SSH proxy authorization probe

This commit is contained in:
Eric Wendland 2026-05-19 15:51:11 +02:00
commit e145bb47cd
11 changed files with 333 additions and 17 deletions

View file

@ -28,6 +28,7 @@ geth-pubsub = { path = "../geth-pubsub" }
geth-resource = { path = "../geth-resource" }
geth-secrets = { path = "../geth-secrets" }
geth-ssh-identity = { path = "../geth-ssh-identity" }
geth-ssh-proxy = { path = "../geth-ssh-proxy" }
geth-store = { path = "../geth-store" }
geth-types = { path = "../geth-types" }
iroh.workspace = true

View file

@ -31,6 +31,7 @@ use geth_ssh_identity::{
cert_request_id, certificate_id, openssh_krl_spec, parse_openssh_krl_spec, revocation_id,
ssh_public_key_fingerprint, write_openssh_krl,
};
use geth_ssh_proxy::SshProxyConnection;
use geth_store::{
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileConflict,
StoredFileRoot, StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredModuleState,
@ -281,6 +282,9 @@ pub async fn handle_request_async(
target,
node: Some(peer_node),
} => pipe_connect_to_peer(node, &peer_node, target).await,
ControlRequest::SshProxyConnect { node: peer_node } => {
ssh_proxy_connect_to_peer(node, &peer_node).await
}
ControlRequest::DocumentSync {
node: peer_node,
name,
@ -566,6 +570,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to ping request".to_owned(),
@ -680,6 +685,7 @@ async fn peer_auth_check(
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to auth-check request".to_owned(),
@ -824,6 +830,7 @@ async fn cas_fetch_from_peer(
| PeerControlResponse::PubsubPublished { .. }
| PeerControlResponse::PubsubSubscribed { .. }
| PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to CAS fetch".to_owned(),
@ -1194,6 +1201,41 @@ async fn pipe_connect_to_peer(
}
}
async fn ssh_proxy_connect_to_peer(
node: &LocalNode,
peer_node: &str,
) -> Result<ControlResponse, NodeError> {
let response =
request_peer_control(node, peer_node, "ssh-proxy-connect", |peer_card, nonce| {
PeerControlRequest::SshProxyConnect { peer_card, nonce }
})
.await?;
match response {
PeerControlResponse::SshProxyConnected {
node_id,
agent_id,
endpoint_id,
connection,
allowed,
reason,
note,
..
} => Ok(ControlResponse::SshProxyConnected {
peer_node_id: node_id,
peer_agent_id: agent_id,
endpoint_id,
connection,
allowed,
reason,
note,
}),
PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)),
_ => Err(NodeError::IrohPeer(
"peer returned wrong response type to SSH proxy connect".to_owned(),
)),
}
}
async fn document_sync_from_peer(
node: &LocalNode,
peer_node: &str,
@ -1610,6 +1652,10 @@ async fn request_peer_control(
nonce: response_nonce,
..
}
| PeerControlResponse::SshProxyConnected {
nonce: response_nonce,
..
}
| PeerControlResponse::DocumentSynced {
nonce: response_nonce,
..
@ -2234,6 +2280,52 @@ async fn handle_iroh_control_connection(
note: "pipe connect authenticated endpoint/card binding and required pipe.connect on the remote pipe resource; byte streams are not implemented yet".to_owned(),
}
}
PeerControlRequest::SshProxyConnect { peer_card, nonce } => {
peer_card.validate_candidate()?;
ensure_peer_card_matches_endpoint(&peer_card, &remote_endpoint_id)?;
let discovered = DiscoveredPeer::candidate(
peer_card.clone(),
UnixMillis(geth_store::now_ms()),
DiscoverySource::PeerExchange,
)?;
let store = Store::open(&node.paths.metadata_db())?;
store.upsert_peer_card(&StoredPeerCard {
peer_id: peer_card.node_id.to_string(),
card_json: serde_json::to_string(&peer_card)?,
updated_at_ms: discovered.discovered_at.0,
})?;
let resource = "resource:ssh-proxy:local".to_owned();
let capability = "ssh_proxy.connect".to_owned();
let explanation = geth_auth::explain_auth_ops(
&load_auth_ops_for_resource(&store, &resource)?,
PrincipalId::new(peer_card.node_id.to_string()),
ResourceId::new(resource),
Capability::new(capability),
);
let connection = if explanation.allowed {
Some(SshProxyConnection {
target_node: NodeId::new(node.node_id.clone()),
connected_at: UnixMillis(geth_store::now_ms()),
local_sshd_target: Some("127.0.0.1:22".to_owned()),
admin_shell_available: false,
note: "authorized SSH proxy control-plane check passed; byte proxying and sshd/admin-shell connection are not implemented yet".to_owned(),
})
} else {
None
};
PeerControlResponse::SshProxyConnected {
node_id: node.node_id.clone(),
agent_id: node.agent_id.clone(),
endpoint_id: node.iroh_status.endpoint_id.clone().unwrap_or_default(),
remote_endpoint_id,
connection,
allowed: explanation.allowed,
reason: explanation.reason,
evaluated_ops: explanation.evaluated_ops,
nonce,
note: "SSH proxy authenticated endpoint/card binding and required ssh_proxy.connect on resource:ssh-proxy:local; SSH is not a geth transport and byte proxying is not implemented yet".to_owned(),
}
}
PeerControlRequest::DocumentSync {
peer_card,
name,
@ -3500,6 +3592,7 @@ pub fn handle_request(
ControlRequest::PipeConnect { node: Some(_), .. } => {
Err(NodeError::IrohEndpointUnavailable)
}
ControlRequest::SshProxyConnect { .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::ModuleStub { module, command } => {
Ok(ControlResponse::NotImplemented { module, command })
}
@ -4584,6 +4677,28 @@ mod tests {
other => panic!("unexpected denied pipe connect response: {other:?}"),
}
let denied_ssh_proxy = handle_request_async(
&left,
ControlRequest::SshProxyConnect {
node: right_card.node_id.to_string(),
},
)
.await
.expect("denied SSH proxy connect");
match denied_ssh_proxy {
ControlResponse::SshProxyConnected {
allowed,
reason,
connection,
..
} => {
assert!(!allowed);
assert!(connection.is_none());
assert!(reason.contains("no active direct or group grant"));
}
other => panic!("unexpected denied SSH proxy response: {other:?}"),
}
let denied_document = handle_request_async(
&left,
ControlRequest::DocumentSync {
@ -4683,6 +4798,16 @@ mod tests {
},
)
.expect("grant left pipe connect");
handle_request(
&right,
ControlRequest::AuthGrant {
subject: left.node_id.clone(),
resource: "resource:ssh-proxy:local".to_owned(),
capability: "ssh_proxy.connect".to_owned(),
grant_id: Some("grant:left-ssh-proxy-connect".to_owned()),
},
)
.expect("grant left ssh proxy connect");
handle_request(
&right,
ControlRequest::AuthGrant {
@ -4899,6 +5024,35 @@ mod tests {
other => panic!("unexpected allowed pipe connect response: {other:?}"),
}
let ssh_proxy = handle_request_async(
&left,
ControlRequest::SshProxyConnect {
node: right_card.node_id.to_string(),
},
)
.await
.expect("allowed SSH proxy connect");
match ssh_proxy {
ControlResponse::SshProxyConnected {
allowed,
connection,
reason,
note,
..
} => {
assert!(allowed);
let connection = connection.expect("proxy connection metadata");
assert_eq!(connection.target_node.to_string(), right.node_id);
assert_eq!(
connection.local_sshd_target.as_deref(),
Some("127.0.0.1:22")
);
assert!(reason.contains("direct grant"));
assert!(note.contains("SSH is not a geth transport"));
}
other => panic!("unexpected allowed SSH proxy response: {other:?}"),
}
let document_sync = handle_request_async(
&left,
ControlRequest::DocumentSync {