Compare commits

..

1 Commits

Author SHA1 Message Date
200mill
3697600c96 fix: asdf 2026-07-04 16:50:09 +09:00
2 changed files with 62 additions and 9 deletions

View File

@@ -45,8 +45,9 @@ pub async fn run(
} }
}; };
// Load the operator's SSH key once. It serves as both the SSH host key and, // Load the operator's SSH key once. Its public half is folded into the
// via its public half, an always-allowed client key. // 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)?; let operator_key = load_operator_key(&cfg)?;
// Build the set of client public keys allowed to connect: the backend's // Build the set of client public keys allowed to connect: the backend's
@@ -70,13 +71,8 @@ pub async fn run(
..Default::default() ..Default::default()
}; };
// Derive the SSH host key from the operator key, round-tripped through // Dedicated, persistent SSH host key (never the operator's auth key).
// OpenSSH encoding (russh uses a different ssh-key version than we do). let host_key = load_or_create_host_key()?;
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}"))?;
config.keys.push(host_key); config.keys.push(host_key);
let config = Arc::new(config); let config = Arc::new(config);
@@ -129,6 +125,53 @@ fn load_operator_key(cfg: &Config) -> Result<ssh_key::PrivateKey> {
Ok(key) 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 /// SHA-256 fingerprints of every public key allowed to connect: the backend's
/// authorized_keys plus the operator's own key. /// authorized_keys plus the operator's own key.
async fn build_allowed_fingerprints( async fn build_allowed_fingerprints(

View File

@@ -43,4 +43,14 @@ impl Config {
pub fn ssh_key_path(&self) -> PathBuf { pub fn ssh_key_path(&self) -> PathBuf {
PathBuf::from(shellexpand::tilde(&self.ssh_key_file).to_string()) 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")
}
} }