Add restricted SSH admin shell

This commit is contained in:
Eric Wendland 2026-05-21 01:49:48 +02:00
commit 59ccf6c748
9 changed files with 358 additions and 14 deletions

View file

@ -198,7 +198,10 @@ Roadmap items should be actionable and checkable:
CLI streams through the local daemon, the daemon uses `/geth/ssh-proxy/1` over CLI streams through the local daemon, the daemon uses `/geth/ssh-proxy/1` over
Iroh, the remote daemon requires `ssh_proxy.connect` on Iroh, the remote daemon requires `ssh_proxy.connect` on
`resource:ssh-proxy:local`, and only then connects to `127.0.0.1:22`. SSH is `resource:ssh-proxy:local`, and only then connects to `127.0.0.1:22`. SSH is
still not a geth transport backend. still not a geth transport backend. `geth ssh admin-shell <node-id>
<help|status|node-id>` is a restricted geth admin workflow over the protected
Iroh control path, requires `ssh_proxy.admin_shell`, and never executes host
shell commands.
- Resource secret epoch metadata can be created, rotated, and listed locally. - Resource secret epoch metadata can be created, rotated, and listed locally.
Bearer access metadata can be created/listed/revoked as resource-scoped auth Bearer access metadata can be created/listed/revoked as resource-scoped auth
ops and must not allow trust graph mutation capabilities. Bearer ops and must not allow trust graph mutation capabilities. Bearer

View file

@ -150,7 +150,9 @@ The bootstrap implementation provides:
- `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec|openssh-krl] [--subject <principal>]` - `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec|openssh-krl] [--subject <principal>]`
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec] [--subject <principal>]` - `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec] [--subject <principal>]`
- `geth ssh revocation sync <node-id> [--bearer-secret <secret>]` - `geth ssh revocation sync <node-id> [--bearer-secret <secret>]`
- SSH proxy over Iroh: `geth ssh proxy <node-id> [--bearer-secret <secret>]` - SSH proxy/admin over Iroh:
`geth ssh proxy <node-id> [--bearer-secret <secret>]` and
`geth ssh admin-shell <node-id> <help|status|node-id> [--bearer-secret <secret>]`
- pipe registry/message commands: - pipe registry/message commands:
`geth pipe listen <name> [--node <node-id>] [--bearer-secret <secret>]`, `geth pipe listen <name> [--node <node-id>] [--bearer-secret <secret>]`,
`geth pipe connect <name> [--node <node-id>] [--bearer-secret <secret>]`, `geth pipe connect <name> [--node <node-id>] [--bearer-secret <secret>]`,
@ -242,6 +244,10 @@ ALPN, the remote daemon validates the caller's endpoint/card binding and
requires `ssh_proxy.connect` on `resource:ssh-proxy:local`, and only then requires `ssh_proxy.connect` on `resource:ssh-proxy:local`, and only then
connects the stream to `127.0.0.1:22`. SSH remains normal OpenSSH on top of that connects the stream to `127.0.0.1:22`. SSH remains normal OpenSSH on top of that
byte stream; SSH is not a geth transport backend. byte stream; SSH is not a geth transport backend.
`geth ssh admin-shell <node-id> <command>` is a restricted geth admin workflow
over the protected Iroh control path. It requires `ssh_proxy.admin_shell` on the
same resource and supports only built-in commands (`help`, `status`, `node-id`);
it does not execute host shell commands.
Document sync is a bootstrap JSON last-writer-wins path before Automerge: Document sync is a bootstrap JSON last-writer-wins path before Automerge:
manual `geth document sync <node-id> <name>` and background live-sync require manual `geth document sync <node-id> <name>` and background live-sync require
`document.read` on `resource:document:<name>` and import only state that is not `document.read` on `resource:document:<name>` and import only state that is not

View file

@ -500,6 +500,12 @@ pub enum SshCommand {
#[arg(long)] #[arg(long)]
bearer_secret: Option<String>, bearer_secret: Option<String>,
}, },
AdminShell {
node: String,
command: String,
#[arg(long)]
bearer_secret: Option<String>,
},
Cert { Cert {
#[command(subcommand)] #[command(subcommand)]
command: SshCertCommand, command: SshCertCommand,
@ -1050,6 +1056,15 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
node, node,
bearer_secret, bearer_secret,
}, },
SshCommand::AdminShell {
node,
command,
bearer_secret,
} => ControlRequest::SshAdminShell {
node,
command,
bearer_secret,
},
SshCommand::Cert { command } => match command { SshCommand::Cert { command } => match command {
SshCertCommand::Request { SshCertCommand::Request {
public_key, public_key,
@ -2209,6 +2224,29 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
println!("reason: {reason}"); println!("reason: {reason}");
println!("note: {note}"); println!("note: {note}");
} }
ControlResponse::SshAdminShellOutput {
peer_node_id,
peer_agent_id,
endpoint_id,
command,
output,
allowed,
reason,
note,
} => {
if allowed {
println!("{output}");
} else {
println!("ssh admin shell denied by {peer_node_id}");
}
println!("command: {command}");
println!("peer: {peer_node_id}");
println!("agent: {peer_agent_id}");
println!("endpoint: {endpoint_id}");
println!("allowed: {allowed}");
println!("reason: {reason}");
println!("note: {note}");
}
ControlResponse::NotImplemented { module, command } => { ControlResponse::NotImplemented { module, command } => {
println!("{module} {command}: not implemented yet"); println!("{module} {command}: not implemented yet");
} }

View file

@ -236,6 +236,11 @@ pub enum ControlRequest {
node: String, node: String,
bearer_secret: Option<String>, bearer_secret: Option<String>,
}, },
SshAdminShell {
node: String,
command: String,
bearer_secret: Option<String>,
},
DbAdd { DbAdd {
name: String, name: String,
path: PathBuf, path: PathBuf,
@ -724,6 +729,16 @@ pub enum ControlResponse {
reason: String, reason: String,
note: String, note: String,
}, },
SshAdminShellOutput {
peer_node_id: String,
peer_agent_id: String,
endpoint_id: String,
command: String,
output: String,
allowed: bool,
reason: String,
note: String,
},
NotImplemented { NotImplemented {
module: String, module: String,
command: String, command: String,
@ -857,6 +872,12 @@ pub enum PeerControlRequest {
nonce: String, nonce: String,
bearer_proof: Option<BearerProof>, bearer_proof: Option<BearerProof>,
}, },
SshAdminShell {
peer_card: PeerCard,
command: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
DocumentSync { DocumentSync {
peer_card: PeerCard, peer_card: PeerCard,
name: String, name: String,
@ -1038,6 +1059,19 @@ pub enum PeerControlResponse {
nonce: String, nonce: String,
note: String, note: String,
}, },
SshAdminShellOutput {
node_id: String,
agent_id: String,
endpoint_id: String,
remote_endpoint_id: String,
command: String,
output: String,
allowed: bool,
reason: String,
evaluated_ops: usize,
nonce: String,
note: String,
},
DocumentSynced { DocumentSynced {
node_id: String, node_id: String,
agent_id: String, agent_id: String,
@ -1571,6 +1605,16 @@ mod tests {
request request
); );
let request = ControlRequest::SshAdminShell {
node: "node:peer".to_owned(),
command: "status".to_owned(),
bearer_secret: None,
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
request
);
let response = ControlResponse::SshProxyConnected { let response = ControlResponse::SshProxyConnected {
peer_node_id: "node:peer".to_owned(), peer_node_id: "node:peer".to_owned(),
peer_agent_id: "agent:peer".to_owned(), peer_agent_id: "agent:peer".to_owned(),
@ -1591,6 +1635,21 @@ mod tests {
response response
); );
let response = ControlResponse::SshAdminShellOutput {
peer_node_id: "node:peer".to_owned(),
peer_agent_id: "agent:peer".to_owned(),
endpoint_id: "endpoint:peer".to_owned(),
command: "status".to_owned(),
output: "node_id=node:peer".to_owned(),
allowed: true,
reason: "direct grant".to_owned(),
note: "restricted admin shell".to_owned(),
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let request = ControlRequest::DbChanges { let request = ControlRequest::DbChanges {
name: "notes".to_owned(), name: "notes".to_owned(),
after_db_version: Some(7), after_db_version: Some(7),
@ -2039,6 +2098,34 @@ mod tests {
request request
); );
let request = PeerControlRequest::SshAdminShell {
peer_card: PeerCard {
node_id: "node:caller".into(),
agent_id: "agent:caller".into(),
endpoints: Vec::new(),
issued_at: geth_types::UnixMillis(1),
signature: geth_discovery::SignatureMetadata {
namespace: "geth.peer-card.v1@geth.local".to_owned(),
signer: "agent:caller".to_owned(),
public_key: "key".to_owned(),
signature: "sig".to_owned(),
},
},
command: "status".to_owned(),
nonce: "nonce".to_owned(),
bearer_proof: Some(BearerProof {
secret: "bearer:test".into(),
resource: "resource:ssh-proxy:local".into(),
capabilities: vec!["ssh_proxy.admin_shell".into()],
nonce: "nonce".to_owned(),
response: "response".to_owned(),
}),
};
assert_eq!(
decode_peer_request(&encode_peer_request(&request).expect("encode")).expect("decode"),
request
);
let request = PeerControlRequest::PipeListen { let request = PeerControlRequest::PipeListen {
peer_card: PeerCard { peer_card: PeerCard {
node_id: "node:caller".into(), node_id: "node:caller".into(),
@ -2209,6 +2296,25 @@ mod tests {
response response
); );
let response = PeerControlResponse::SshAdminShellOutput {
node_id: "node:peer".to_owned(),
agent_id: "agent:peer".to_owned(),
endpoint_id: "endpoint:peer".to_owned(),
remote_endpoint_id: "endpoint:caller".to_owned(),
command: "status".to_owned(),
output: "node_id=node:peer".to_owned(),
allowed: true,
reason: "direct grant".to_owned(),
evaluated_ops: 1,
nonce: "nonce".to_owned(),
note: "restricted admin shell".to_owned(),
};
assert_eq!(
decode_peer_response(&encode_peer_response(&response).expect("encode"))
.expect("decode"),
response
);
let response = PeerControlResponse::DbSynced { let response = PeerControlResponse::DbSynced {
node_id: "node:peer".to_owned(), node_id: "node:peer".to_owned(),
agent_id: "agent:peer".to_owned(), agent_id: "agent:peer".to_owned(),

View file

@ -552,6 +552,11 @@ pub async fn handle_request_async(
node: peer_node, node: peer_node,
bearer_secret, bearer_secret,
} => ssh_proxy_connect_to_peer(node, &peer_node, bearer_secret).await, } => 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 { ControlRequest::DocumentSync {
node: peer_node, node: peer_node,
name, name,
@ -869,6 +874,7 @@ async fn peer_ping(node: &LocalNode, peer_node: &str) -> Result<ControlResponse,
| PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. } | PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::SshAdminShellOutput { .. }
| PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to ping request".to_owned(), "peer returned wrong response type to ping request".to_owned(),
@ -986,6 +992,7 @@ async fn peer_auth_check(
| PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. } | PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::SshAdminShellOutput { .. }
| PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to auth-check request".to_owned(), "peer returned wrong response type to auth-check request".to_owned(),
@ -1135,6 +1142,7 @@ async fn cas_fetch_from_peer(
| PeerControlResponse::PipeConnected { .. } | PeerControlResponse::PipeConnected { .. }
| PeerControlResponse::PipeListening { .. } | PeerControlResponse::PipeListening { .. }
| PeerControlResponse::SshProxyConnected { .. } | PeerControlResponse::SshProxyConnected { .. }
| PeerControlResponse::SshAdminShellOutput { .. }
| PeerControlResponse::DocumentSynced { .. } | PeerControlResponse::DocumentSynced { .. }
| PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer( | PeerControlResponse::DbSynced { .. } => Err(NodeError::IrohPeer(
"peer returned wrong response type to CAS fetch".to_owned(), "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( async fn handle_local_ssh_proxy_stream(
node: LocalNode, node: LocalNode,
peer_node: &str, peer_node: &str,
@ -3706,7 +3762,7 @@ async fn handle_iroh_control_connection(
target_node: NodeId::new(node.node_id.clone()), target_node: NodeId::new(node.node_id.clone()),
connected_at: UnixMillis(geth_store::now_ms()), connected_at: UnixMillis(geth_store::now_ms()),
local_sshd_target: Some("127.0.0.1:22".to_owned()), 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(), note: "authorized SSH proxy metadata check passed; run geth ssh proxy for the dedicated byte stream".to_owned(),
}) })
} else { } 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(), 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 { PeerControlRequest::DocumentSync {
peer_card, peer_card,
name, name,
@ -3925,7 +4029,7 @@ async fn handle_ssh_proxy_wire_connection(
target_node: node.node_id.clone().into(), target_node: node.node_id.clone().into(),
connected_at: UnixMillis(geth_store::now_ms()), connected_at: UnixMillis(geth_store::now_ms()),
local_sshd_target: Some("127.0.0.1:22".to_owned()), 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" note: "authorized SSH proxy byte stream over Iroh; SSH is not a geth transport"
.to_owned(), .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 { fn display_alpn(alpn: Vec<u8>) -> String {
String::from_utf8_lossy(&alpn).into_owned() String::from_utf8_lossy(&alpn).into_owned()
} }
@ -5819,9 +5943,9 @@ pub fn handle_request(
note: geth_pipe::local_pipe_runtime_note().to_owned(), note: geth_pipe::local_pipe_runtime_note().to_owned(),
}) })
} }
ControlRequest::SshProxyConnect { .. } | ControlRequest::SshProxyStream { .. } => { ControlRequest::SshProxyConnect { .. }
Err(NodeError::IrohEndpointUnavailable) | ControlRequest::SshProxyStream { .. }
} | ControlRequest::SshAdminShell { .. } => Err(NodeError::IrohEndpointUnavailable),
ControlRequest::ModuleStub { module, command } => { ControlRequest::ModuleStub { module, command } => {
Ok(ControlResponse::NotImplemented { module, command }) Ok(ControlResponse::NotImplemented { module, command })
} }
@ -7924,6 +8048,30 @@ mod tests {
other => panic!("unexpected denied SSH proxy response: {other:?}"), 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( let denied_document = handle_request_async(
&left, &left,
ControlRequest::DocumentSync { ControlRequest::DocumentSync {
@ -8099,6 +8247,16 @@ mod tests {
}, },
) )
.expect("grant left ssh proxy connect"); .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( handle_request(
&right, &right,
ControlRequest::AuthGrant { ControlRequest::AuthGrant {
@ -8634,6 +8792,33 @@ mod tests {
other => panic!("unexpected allowed SSH proxy response: {other:?}"), 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( let document_sync = handle_request_async(
&left, &left,
ControlRequest::DocumentSync { ControlRequest::DocumentSync {

View file

@ -18,5 +18,5 @@ pub struct SshProxyConnection {
#[must_use] #[must_use]
pub fn ssh_proxy_roadmap() -> &'static str { pub fn ssh_proxy_roadmap() -> &'static str {
"future SSH proxy carries SSH protocol bytes over authorized Iroh streams; SSH is not a geth transport" "SSH proxy carries SSH protocol bytes over authorized Iroh streams, and the restricted geth admin shell uses built-in control commands; SSH is not a geth transport"
} }

View file

@ -7,8 +7,10 @@ Accepted.
## Decision ## Decision
SSH keys are admin signing identities and ecosystem integration points. SSH is SSH keys are admin signing identities and ecosystem integration points. SSH is
not a geth transport. Future SSH proxy support will carry SSH protocol bytes over not a geth transport. SSH proxy support carries SSH protocol bytes over
authorized Iroh streams, and OpenSSH will still perform normal login auth. authorized Iroh streams, and OpenSSH still performs normal login auth. The
restricted geth admin shell is a separate built-in command set over protected
Iroh control requests; it does not execute host shell commands.
Geth also manages OpenSSH certificate request, renewal, approval, import, and Geth also manages OpenSSH certificate request, renewal, approval, import, and
revocation-list metadata. Signing is explicit: an approved request yields a revocation-list metadata. Signing is explicit: an approved request yields a

View file

@ -77,8 +77,12 @@ proxy <node-id>` command performs an authorized Iroh control-plane handshake:
the remote daemon validates the caller's signed peer card against the observed the remote daemon validates the caller's signed peer card against the observed
Iroh EndpointID and requires `ssh_proxy.connect` on Iroh EndpointID and requires `ssh_proxy.connect` on
`resource:ssh-proxy:local`. It returns connection metadata only. Carrying SSH `resource:ssh-proxy:local`. It returns connection metadata only. Carrying SSH
bytes over an Iroh stream and connecting to remote sshd or a restricted admin bytes over an Iroh stream is implemented on the dedicated `/geth/ssh-proxy/1`
shell remain future work, and will not make SSH a geth transport backend. ALPN and only connects to remote `127.0.0.1:22` after authorization.
`geth ssh admin-shell <node-id> <help|status|node-id>` is a separate restricted
geth admin workflow over the protected Iroh control path. It requires
`ssh_proxy.admin_shell` and executes only built-in geth commands, never host
shell commands. Neither path makes SSH a geth transport backend.
SSH certificate flows use the same split. Nodes can request new OpenSSH SSH certificate flows use the same split. Nodes can request new OpenSSH
certificates or renewals through geth metadata. A machine with the CA key or certificates or renewals through geth metadata. A machine with the CA key or

View file

@ -406,7 +406,7 @@ Goal: add authorized stream-oriented management workflows over Iroh.
- `[ ]` Unsupported platforms return clear errors. - `[ ]` Unsupported platforms return clear errors.
- `[x]` Tests cover a full two-node Unix socket forwarding exchange. - `[x]` Tests cover a full two-node Unix socket forwarding exchange.
- `[~]` SSH proxy over Iroh. - `[x]` SSH proxy over Iroh.
Acceptance criteria: Acceptance criteria:
- `[x]` `geth ssh proxy <node>` contacts an imported peer over the dedicated - `[x]` `geth ssh proxy <node>` contacts an imported peer over the dedicated
`/geth/ssh-proxy/1` Iroh ALPN. `/geth/ssh-proxy/1` Iroh ALPN.
@ -417,7 +417,7 @@ Goal: add authorized stream-oriented management workflows over Iroh.
- `[x]` The proxy opens a dedicated authorized Iroh byte stream. - `[x]` The proxy opens a dedicated authorized Iroh byte stream.
- `[x]` Remote daemon connects that stream to local sshd at `127.0.0.1:22` - `[x]` Remote daemon connects that stream to local sshd at `127.0.0.1:22`
only after authorization. only after authorization.
- `[ ]` Future completion adds a restricted built-in geth admin shell option. - `[x]` Future completion adds a restricted built-in geth admin shell option.
- `[~]` SSH certificate and revocation distribution. - `[~]` SSH certificate and revocation distribution.
Acceptance criteria: Acceptance criteria: