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

  1. Check and fix permissions on the known_hosts file and its parent directory so the running user can write.
  2. Ensure the directory for known_hosts_path exists (create it before connecting).
  3. If the filesystem is read-only (containers/CI), point known_hosts_path at a writable volume.
  4. 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

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


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/e9ac8adb4a0755b7. Report an issue: GitHub.