Add restricted SSH admin shell
This commit is contained in:
parent
64e0168dbf
commit
59ccf6c748
9 changed files with 358 additions and 14 deletions
|
|
@ -552,6 +552,11 @@ pub async fn handle_request_async(
|
|||
node: peer_node,
|
||||
bearer_secret,
|
||||
} => ssh_proxy_connect_to_peer(node, &peer_node, bearer_secret).await,
|
||||
ControlRequest::SshAdminShell {
|
||||
node: peer_node,
|
||||
command,
|
||||
bearer_secret,
|
||||
} => ssh_admin_shell_to_peer(node, &peer_node, command, bearer_secret).await,
|
||||
ControlRequest::DocumentSync {
|
||||
node: peer_node,
|
||||
name,
|
||||
|
|
@ -869,6 +874,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
|
|||
| PeerControlResponse::PipeConnected { .. }
|
||||
| PeerControlResponse::PipeListening { .. }
|
||||
| PeerControlResponse::SshProxyConnected { .. }
|
||||
| PeerControlResponse::SshAdminShellOutput { .. }
|
||||
| PeerControlResponse::DocumentSynced { .. }
|
||||
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
|
||||
"peer returned wrong response type to ping request".to_owned(),
|
||||
|
|
@ -986,6 +992,7 @@ async fn peer_auth_check(
|
|||
| PeerControlResponse::PipeConnected { .. }
|
||||
| PeerControlResponse::PipeListening { .. }
|
||||
| PeerControlResponse::SshProxyConnected { .. }
|
||||
| PeerControlResponse::SshAdminShellOutput { .. }
|
||||
| PeerControlResponse::DocumentSynced { .. }
|
||||
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
|
||||
"peer returned wrong response type to auth-check request".to_owned(),
|
||||
|
|
@ -1135,6 +1142,7 @@ async fn cas_fetch_from_peer(
|
|||
| PeerControlResponse::PipeConnected { .. }
|
||||
| PeerControlResponse::PipeListening { .. }
|
||||
| PeerControlResponse::SshProxyConnected { .. }
|
||||
| PeerControlResponse::SshAdminShellOutput { .. }
|
||||
| PeerControlResponse::DocumentSynced { .. }
|
||||
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
|
||||
"peer returned wrong response type to CAS fetch".to_owned(),
|
||||
|
|
@ -2114,6 +2122,54 @@ async fn ssh_proxy_connect_to_peer(
|
|||
}
|
||||
}
|
||||
|
||||
async fn ssh_admin_shell_to_peer(
|
||||
node: &LocalNode,
|
||||
peer_node: &str,
|
||||
command: String,
|
||||
bearer_secret: Option<String>,
|
||||
) -> Result<ControlResponse, NodeError> {
|
||||
let response = request_peer_control(node, peer_node, "ssh-admin-shell", |peer_card, nonce| {
|
||||
PeerControlRequest::SshAdminShell {
|
||||
peer_card,
|
||||
command: command.clone(),
|
||||
nonce: nonce.clone(),
|
||||
bearer_proof: bearer_proof(
|
||||
bearer_secret,
|
||||
"resource:ssh-proxy:local",
|
||||
"ssh_proxy.admin_shell",
|
||||
&nonce,
|
||||
),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
match response {
|
||||
PeerControlResponse::SshAdminShellOutput {
|
||||
node_id,
|
||||
agent_id,
|
||||
endpoint_id,
|
||||
command,
|
||||
output,
|
||||
allowed,
|
||||
reason,
|
||||
note,
|
||||
..
|
||||
} => Ok(ControlResponse::SshAdminShellOutput {
|
||||
peer_node_id: node_id,
|
||||
peer_agent_id: agent_id,
|
||||
endpoint_id,
|
||||
command,
|
||||
output,
|
||||
allowed,
|
||||
reason,
|
||||
note,
|
||||
}),
|
||||
PeerControlResponse::Error { message } => Err(NodeError::IrohPeer(message)),
|
||||
_ => Err(NodeError::IrohPeer(
|
||||
"peer returned wrong response type to SSH admin shell".to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_local_ssh_proxy_stream(
|
||||
node: LocalNode,
|
||||
peer_node: &str,
|
||||
|
|
@ -3706,7 +3762,7 @@ async fn handle_iroh_control_connection(
|
|||
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,
|
||||
admin_shell_available: true,
|
||||
note: "authorized SSH proxy metadata check passed; run geth ssh proxy for the dedicated byte stream".to_owned(),
|
||||
})
|
||||
} else {
|
||||
|
|
@ -3725,6 +3781,54 @@ async fn handle_iroh_control_connection(
|
|||
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 uses the dedicated /geth/ssh-proxy/1 stream".to_owned(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::SshAdminShell {
|
||||
peer_card,
|
||||
command,
|
||||
nonce,
|
||||
bearer_proof,
|
||||
} => {
|
||||
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.admin_shell".to_owned();
|
||||
let explanation = explain_peer_or_bearer(
|
||||
&store,
|
||||
peer_card.node_id.as_str(),
|
||||
&resource,
|
||||
&capability,
|
||||
&nonce,
|
||||
bearer_proof.as_ref(),
|
||||
)?;
|
||||
let output = if explanation.allowed {
|
||||
restricted_admin_shell_output(&node, &command)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
PeerControlResponse::SshAdminShellOutput {
|
||||
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,
|
||||
command,
|
||||
output,
|
||||
allowed: explanation.allowed,
|
||||
reason: explanation.reason,
|
||||
evaluated_ops: explanation.evaluated_ops,
|
||||
nonce,
|
||||
note: "restricted geth admin shell authenticated endpoint/card binding and required ssh_proxy.admin_shell; no host shell commands are executed".to_owned(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::DocumentSync {
|
||||
peer_card,
|
||||
name,
|
||||
|
|
@ -3925,7 +4029,7 @@ async fn handle_ssh_proxy_wire_connection(
|
|||
target_node: node.node_id.clone().into(),
|
||||
connected_at: UnixMillis(geth_store::now_ms()),
|
||||
local_sshd_target: Some("127.0.0.1:22".to_owned()),
|
||||
admin_shell_available: false,
|
||||
admin_shell_available: true,
|
||||
note: "authorized SSH proxy byte stream over Iroh; SSH is not a geth transport"
|
||||
.to_owned(),
|
||||
})
|
||||
|
|
@ -4410,6 +4514,26 @@ fn ensure_peer_card_matches_endpoint(card: &PeerCard, endpoint_id: &str) -> Resu
|
|||
}
|
||||
}
|
||||
|
||||
fn restricted_admin_shell_output(node: &LocalNode, command: &str) -> Result<String, NodeError> {
|
||||
match command {
|
||||
"help" => Ok("commands: help, status, node-id".to_owned()),
|
||||
"node-id" => Ok(node.node_id.clone()),
|
||||
"status" => Ok(format!(
|
||||
"node_id={}\nagent_id={}\nendpoint_id={}\nhome={}",
|
||||
node.node_id,
|
||||
node.agent_id,
|
||||
node.iroh_status
|
||||
.endpoint_id
|
||||
.as_deref()
|
||||
.unwrap_or("unavailable"),
|
||||
node.paths.home().display()
|
||||
)),
|
||||
other => Err(NodeError::IrohPeer(format!(
|
||||
"unsupported restricted admin shell command {other:?}; supported commands: help, status, node-id"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn display_alpn(alpn: Vec<u8>) -> String {
|
||||
String::from_utf8_lossy(&alpn).into_owned()
|
||||
}
|
||||
|
|
@ -5819,9 +5943,9 @@ pub fn handle_request(
|
|||
note: geth_pipe::local_pipe_runtime_note().to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::SshProxyConnect { .. } | ControlRequest::SshProxyStream { .. } => {
|
||||
Err(NodeError::IrohEndpointUnavailable)
|
||||
}
|
||||
ControlRequest::SshProxyConnect { .. }
|
||||
| ControlRequest::SshProxyStream { .. }
|
||||
| ControlRequest::SshAdminShell { .. } => Err(NodeError::IrohEndpointUnavailable),
|
||||
ControlRequest::ModuleStub { module, command } => {
|
||||
Ok(ControlResponse::NotImplemented { module, command })
|
||||
}
|
||||
|
|
@ -7924,6 +8048,30 @@ mod tests {
|
|||
other => panic!("unexpected denied SSH proxy response: {other:?}"),
|
||||
}
|
||||
|
||||
let denied_admin_shell = handle_request_async(
|
||||
&left,
|
||||
ControlRequest::SshAdminShell {
|
||||
node: right_card.node_id.to_string(),
|
||||
command: "status".to_owned(),
|
||||
bearer_secret: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("denied SSH admin shell");
|
||||
match denied_admin_shell {
|
||||
ControlResponse::SshAdminShellOutput {
|
||||
allowed,
|
||||
output,
|
||||
reason,
|
||||
..
|
||||
} => {
|
||||
assert!(!allowed);
|
||||
assert!(output.is_empty());
|
||||
assert!(reason.contains("no active direct or group grant"));
|
||||
}
|
||||
other => panic!("unexpected denied SSH admin shell response: {other:?}"),
|
||||
}
|
||||
|
||||
let denied_document = handle_request_async(
|
||||
&left,
|
||||
ControlRequest::DocumentSync {
|
||||
|
|
@ -8099,6 +8247,16 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.expect("grant left ssh proxy connect");
|
||||
handle_request(
|
||||
&right,
|
||||
ControlRequest::AuthGrant {
|
||||
subject: left.node_id.clone(),
|
||||
resource: "resource:ssh-proxy:local".to_owned(),
|
||||
capability: "ssh_proxy.admin_shell".to_owned(),
|
||||
grant_id: Some("grant:left-ssh-admin-shell".to_owned()),
|
||||
},
|
||||
)
|
||||
.expect("grant left ssh admin shell");
|
||||
handle_request(
|
||||
&right,
|
||||
ControlRequest::AuthGrant {
|
||||
|
|
@ -8634,6 +8792,33 @@ mod tests {
|
|||
other => panic!("unexpected allowed SSH proxy response: {other:?}"),
|
||||
}
|
||||
|
||||
let admin_shell = handle_request_async(
|
||||
&left,
|
||||
ControlRequest::SshAdminShell {
|
||||
node: right_card.node_id.to_string(),
|
||||
command: "status".to_owned(),
|
||||
bearer_secret: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("allowed SSH admin shell");
|
||||
match admin_shell {
|
||||
ControlResponse::SshAdminShellOutput {
|
||||
allowed,
|
||||
output,
|
||||
reason,
|
||||
note,
|
||||
..
|
||||
} => {
|
||||
assert!(allowed);
|
||||
assert!(output.contains(&format!("node_id={}", right.node_id)));
|
||||
assert!(output.contains("agent_id="));
|
||||
assert!(reason.contains("direct grant"));
|
||||
assert!(note.contains("no host shell commands"));
|
||||
}
|
||||
other => panic!("unexpected allowed SSH admin shell response: {other:?}"),
|
||||
}
|
||||
|
||||
let document_sync = handle_request_async(
|
||||
&left,
|
||||
ControlRequest::DocumentSync {
|
||||
|
|
|
|||
Loading…
Reference in a new issue