main #1
@@ -45,8 +45,9 @@ pub async fn run(
|
||||
}
|
||||
};
|
||||
|
||||
// Load the operator's SSH key once. It serves as both the SSH host key and,
|
||||
// via its public half, an always-allowed client key.
|
||||
// Load the operator's SSH key once. Its public half is folded into the
|
||||
// client allowlist (an always-allowed client key). It is NOT used as the
|
||||
// SSH host key — see load_or_create_host_key.
|
||||
let operator_key = load_operator_key(&cfg)?;
|
||||
|
||||
// Build the set of client public keys allowed to connect: the backend's
|
||||
@@ -70,13 +71,8 @@ pub async fn run(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Derive the SSH host key from the operator key, round-tripped through
|
||||
// OpenSSH encoding (russh uses a different ssh-key version than we do).
|
||||
let host_pem = operator_key
|
||||
.to_openssh(ssh_key::LineEnding::LF)
|
||||
.context("encode host key")?;
|
||||
let host_key = russh::keys::PrivateKey::from_openssh(host_pem.as_bytes())
|
||||
.map_err(|e| anyhow!("parse host key: {e}"))?;
|
||||
// Dedicated, persistent SSH host key (never the operator's auth key).
|
||||
let host_key = load_or_create_host_key()?;
|
||||
config.keys.push(host_key);
|
||||
let config = Arc::new(config);
|
||||
|
||||
@@ -129,6 +125,53 @@ fn load_operator_key(cfg: &Config) -> Result<ssh_key::PrivateKey> {
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Load the persistent serve-ssh host key, generating an Ed25519 one on first
|
||||
/// use. This is a dedicated key, distinct from the operator's auth key, so the
|
||||
/// listener has a stable identity without cross-context key reuse.
|
||||
fn load_or_create_host_key() -> Result<russh::keys::PrivateKey> {
|
||||
let path = Config::serve_ssh_host_key_path();
|
||||
let key: ssh_key::PrivateKey = if path.exists() {
|
||||
let raw = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
ssh_key::PrivateKey::from_openssh(&raw).context("parse host key")?
|
||||
} else {
|
||||
let k = ssh_key::PrivateKey::random(&mut rand::rngs::OsRng, ssh_key::Algorithm::Ed25519)
|
||||
.map_err(|e| anyhow!("generate host key: {e}"))?;
|
||||
let pem = k.to_openssh(ssh_key::LineEnding::LF).context("encode host key")?;
|
||||
write_private(&path, pem.as_bytes())?;
|
||||
ui::print_info(&format!("generated serve-ssh host key at {}", path.display()));
|
||||
k
|
||||
};
|
||||
ui::print_info(&format!(
|
||||
"host key fingerprint {}",
|
||||
key.fingerprint(ssh_key::HashAlg::Sha256)
|
||||
));
|
||||
// Round-trip into russh's ssh-key version (russh re-exports an incompatible
|
||||
// ssh-key release, so we cannot hand it our value directly).
|
||||
let pem = key.to_openssh(ssh_key::LineEnding::LF).context("encode host key")?;
|
||||
russh::keys::PrivateKey::from_openssh(pem.as_bytes()).map_err(|e| anyhow!("parse host key: {e}"))
|
||||
}
|
||||
|
||||
/// Write private-key bytes to `path` with `0600` permissions, creating the
|
||||
/// parent directory if needed.
|
||||
fn write_private(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)
|
||||
.with_context(|| format!("create {}", path.display()))?;
|
||||
f.write_all(bytes)
|
||||
.with_context(|| format!("write {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SHA-256 fingerprints of every public key allowed to connect: the backend's
|
||||
/// authorized_keys plus the operator's own key.
|
||||
async fn build_allowed_fingerprints(
|
||||
|
||||
@@ -43,4 +43,14 @@ impl Config {
|
||||
pub fn ssh_key_path(&self) -> PathBuf {
|
||||
PathBuf::from(shellexpand::tilde(&self.ssh_key_file).to_string())
|
||||
}
|
||||
|
||||
/// Path to the dedicated, persistent serve-ssh host key, kept next to the
|
||||
/// config file (respecting `RSHC_CONFIG_PATH`).
|
||||
pub fn serve_ssh_host_key_path() -> PathBuf {
|
||||
Self::path()
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_default()
|
||||
.join("serve_ssh_host_ed25519")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user