t8y2/dbx · warning · io::Error (PermissionDenied)
Failed to persist host key for {host}:{port} to {known_hosts
Error message
Failed to persist host key for {host}:{port} to {known_hosts_path} ({e}). The host is trusted for this session only. What it means
KnownHostsStore::learn persists an accepted SSH host key to the known_hosts file. If the write fails, the io::Error is replaced with an error of kind PermissionDenied carrying 'Failed to persist host key for {host}:{port} to {path} ({e}). The host is trusted for this session only.' Persistence failure does not abort the connection — the key is trusted in-memory for the session, but TOFU trust will not survive restarts.
Source
Thrown at crates/dbx-core/src/db/ssh_host_key.rs:98
Ok(true) => return Ok(HostKeyState::Trusted),
Err(russh::keys::Error::KeyChanged { line }) => {
return Err(host_key_changed_error(host, port, line, &self.known_hosts_path.display().to_string()));
}
// Unknown (or an unreadable dbx store): report as a candidate for TOFU.
_ => {}
}
Ok(HostKeyState::Unknown)
}
/// Records a host key into the dbx store (TOFU persistence). Called by the
/// caller only after the user explicitly accepts the key. A write failure
/// is reported (so the caller knows persistence did not happen) but does
/// not by itself abort the session — the host may simply be trusted for
/// this session only.
pub fn learn(&self, host: &str, port: u16, key: &PublicKey) -> Result<(), io::Error> {
learn_known_hosts_path(host, port, key, &self.known_hosts_path).map_err(|e| {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"Failed to persist host key for {host}:{port} to {} ({e}). \
The host is trusted for this session only.",
self.known_hosts_path.display()
),
)
})
}
}
fn host_key_changed_error(host: &str, port: u16, line: usize, store: &str) -> io::Error {
io::Error::other(format!(
"Host key for {host}:{port} changed (recorded at {store}, line {line}). \
This may indicate a man-in-the-middle attack. Remove the old entry and reconnect only if you expect this change."
))
}
View on GitHub (pinned to c0390bff16)
Solutions
- Check and fix permissions on the known_hosts file and its parent directory so the running user can write.
- Ensure the directory for known_hosts_path exists (create it before connecting).
- If the filesystem is read-only (containers/CI), point known_hosts_path at a writable volume.
- Accept that the host is trusted for the session only, and expect the TOFU prompt again next session.
Example fix
// before
let store = KnownHostsStore::new("/readonly/.ssh/known_hosts");
// after
let store = KnownHostsStore::new("/tmp/writable/.ssh/known_hosts"); // or ensure dir exists & writable Defensive patterns
Strategy: try-catch
Validate before calling
// Rust
let path = store.known_hosts_path();
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let probe = std::fs::OpenOptions::new().append(true).create(true).open(&path);
if let Err(e) = probe {
eprintln!("known_hosts not writable: {e}");
} Try / catch
// Rust
if let Err(e) = store.learn(host, port, &key) {
if e.kind() == std::io::ErrorKind::PermissionDenied {
log::warn!("{e}; continuing with session-only trust");
}
} Prevention
- Provision a writable known_hosts path (and parent directory) at startup, not at first connect.
- Avoid running as root/sudo in a way that leaves ~/.ssh files root-owned.
- In containers/CI, mount a writable volume for SSH state.
When it happens
Trigger: Calling learn after a user accepts an unknown host key when the known_hosts file cannot be written: read-only filesystem, missing parent directory, insufficient file permissions, or disk full.
Common situations: Running the app in a container/home directory mounted read-only, ~/.ssh/known_hosts owned by root after a sudo run, read-only CI workspace, or a known_hosts_path pointing to a nonexistent directory.
Related errors
- Custom H2 JDBC JAR does not exist: " + path
- Both H2 PageStore and MVStore files exist for " + base + ";
- H2 MVStore file is too small to contain valid headers: " + f
- Reusable Java agent package is not a file: {output}
- Reusable native agent package is not a file: {output}
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/e9ac8adb4a0755b7.
Report an issue: GitHub.