NetStacksNetStacks

Host Key Verification & Known Hosts

How NetStacks verifies SSH host keys with explicit trust-on-first-use prompts, manages trusted keys in the Trusted Hosts tab, and lets you revoke or re-key hosts.

Overview

Every SSH connection NetStacks makes verifies the remote device's host key before authentication proceeds. The host key is the server's own public key — it proves you are talking to the device you think you are, not a man-in-the-middle. NetStacks uses Trust On First Use (TOFU): the first time you connect to a host:port pair, you are shown the key's fingerprint and must explicitly accept it. On every later connection the presented key is compared against the one you trusted, and the connection only proceeds silently when they match.

This page covers four things:

  • The host-key prompt shown on a first connection and what to verify before clicking Trust This Key.
  • The Trusted Hosts settings tab, where every key you have trusted is listed for audit.
  • Accept-new / changed-key behavior — the prominent warning shown when a host presents a different key than the one on file.
  • Rotating and removing trusted keys (revoke, then be re-prompted), and the per-session auto-accept escape hatch used by automation.
Strict checking is always on

There is no global "disable host-key checking" setting in NetStacks. The legacy ssh.hostKeyChecking setting was removed; the agent now rejects any attempt to set it. The only way to bypass a prompt is the explicit, non-persistent per-session auto-accept opt-in used by automation.

How Verification Works

Host-key verification runs inside the Local Agent (the SSH sidecar bundled with the Terminal). When the SSH handshake reaches the point where the server presents its public key, the agent classifies it against the trusted store into one of three outcomes:

Matches
The host:port is already trusted and the presented key is identical. The connection proceeds silently — no prompt.
Unknown
There is no stored key for this host:port. This is a first connection. The agent creates a pending prompt and blocks the handshake until you accept or reject it.
Changed
The host:port is known, but the presented key is different from the trusted one. This is the strong "MITM possible" signal. The agent blocks and surfaces a high-visibility prompt showing both fingerprints side by side.

Fingerprints are computed as SHA-256 over the key bytes and displayed in the familiar SHA256:<base64> form (no padding), the same format OpenSSH shows. The trusted store is keyed by host:port, so the same hostname on a different port (for example a console-server line on 2201) is tracked as a separate entry.

Why fingerprints, not full keys

A fingerprint is a short, comparable hash of the full public key. You verify the fingerprint against an out-of-band source (the device itself, your inventory) so a tampered key never matches. Two keys produce the same fingerprint only if they are identical.

The Host-Key Prompt (TOFU)

When the agent classifies a key as Unknown or Changed, it raises a pending prompt and pauses the connection. The Terminal polls for pending prompts roughly every 750 ms while a connection is in flight, so the modal appears within a second of the handshake reaching the host-key stage. The prompt shows the host, port, the SHA-256 fingerprint the host presented, and a countdown.

Prompts auto-reject after 120 seconds

A pending prompt has a hard 2-minute (120 s) timeout. If you do not click Accept or Reject before the countdown reaches zero, the handshake aborts with a host-key error and you must reconnect. The countdown in the modal reflects this.

First connection to a new host

For a brand-new host the prompt is titled "Confirm SSH host key" and shows a single fingerprint — the key the host just presented. Before clicking Trust This Key, verify that fingerprint against an out-of-band source. On most network operating systems you can read the device's own host key directly:

! Cisco IOS / IOS-XE — show the device's SSH host key
show crypto key mypubkey rsa
show ssh server host-keys        ! IOS-XE 17.x+ (ed25519/ecdsa/rsa)

! Arista EOS
show management ssh hostkey

! Juniper Junos
show system host-keys

! Linux / Unix host — print SHA256 fingerprints of the host keys
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done

If the fingerprint shown by the device matches the one in the NetStacks prompt, click Trust This Key. The key is written to your trusted store and the SSH handshake continues. From then on this host connects silently — until the key changes or you revoke it.

If the fingerprint does not match, or you cannot verify it, click Reject. The handshake aborts and nothing is stored.

What 'Trust' actually writes

Accepting persists the presented key to your known_hosts file under the exact host:port you connected to. The action is recorded in the agent's audit log as "host key persisted to known_hosts after explicit user approval". Nothing is written when you reject or let the prompt time out.

When a host key has CHANGED

If a host you have already trusted presents a different key, the prompt changes dramatically. It is titled "⚠ SSH host key has CHANGED", framed with a red border, and shows two fingerprints stacked:

  • Previously-trusted key — the SHA-256 you accepted before.
  • Key the host just presented — the new, unexpected SHA-256.

The accept button reads "Trust New Key Anyway" instead of "Trust This Key", to make clear you are overriding a prior trust decision.

A changed key can mean an attack

A changed host key means the device is presenting a key you never trusted. That can be benign — a firmware upgrade, RMA replacement, or a re-imaged box generates new host keys — but it is also exactly what a man-in-the-middle attack looks like. Verify with the device owner out-of-band before accepting. Do not click "Trust New Key Anyway" just to clear the dialog.

If you confirm the change is legitimate, click Trust New Key Anyway; the new key replaces the old one in your store. If you cannot confirm it, click Reject — the connection aborts and the previously-trusted key is left untouched.

No approval service available

In the rare case that a connection path has no prompt channel wired up (a legacy internal call site), a changed key is refused outright with a host-key mismatch error rather than silently accepted. Strict checking never degrades to silent trust on a changed key.

The Trusted Hosts Tab

Every key you trust is listed in Settings → Trusted Hosts. This tab is your audit trail: TOFU is one-way at connect time (you accept or you abort), so the Trusted Hosts tab is the place to review what you have trusted and undo mistakes.

Each row shows:

Host & port
The host:port pair the key was trusted for (for example 192.168.1.1:22 or core-sw1.lab:2201).
Key type
The algorithm of the trusted key — ssh-ed25519, ssh-rsa, ecdsa-sha2-nistp256, and so on.
Fingerprint
The SHA256:… fingerprint of the trusted key.

A filter box matches against host, port, key type, and fingerprint, so you can quickly locate one device in a large store. A Refresh button reloads the list from disk. Entries are sorted by host, then port. If you have never trusted a key, the tab shows an empty state explaining that accepted keys will appear there after your first TOFU prompt.

Find the tab fast

The Trusted Hosts tab is searchable in Settings under the keywords host key, known hosts, ssh, tofu, trust, fingerprint, revoke, and mitm.

Revoking & Re-Keying Hosts

Once a key is trusted it sticks forever — even if you trusted it by mistake. Revoking gives you the way back. Click Revoke on a row in the Trusted Hosts tab and confirm. The entry is removed from your trusted store immediately, and the next connection to that host:port triggers a fresh TOFU prompt, exactly like a first connection.

Revoke a key when:

  • You accepted a fingerprint by mistake and want to re-verify it.
  • A device was legitimately re-keyed — firmware upgrade, RMA replacement, or re-imaging — and you want to re-establish trust cleanly rather than override a changed-key warning at connect time.
  • You are decommissioning a device and want to clear its stored key.

There are two clean ways to handle a legitimate re-key:

  1. Proactively — revoke the entry in Trusted Hosts before the next connection. The reconnect then shows the ordinary "Confirm SSH host key" prompt (a clean first-trust), not the red "changed" warning.
  2. At connect time — if you connect first and hit the red "changed" prompt, verify the new fingerprint out-of-band and click Trust New Key Anyway to replace it.
Revocation is audited

Removing a key is recorded in the agent audit log as "host key removed from known_hosts via user revoke". Revoking a host you never trusted is a no-op (the API returns 404), so it is safe to attempt.

The known_hosts File

Trusted keys are persisted to a standard OpenSSH known_hosts file at the default path ~/.ssh/known_hosts. NetStacks writes a managed header and one line per trusted key. The format is host:port key-type base64-key — note the :port suffix on the host field, which is how NetStacks keys distinct ports separately:

~/.ssh/known_hoststext
# SSH known hosts file managed by NetStacks
192.168.1.1:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqJ...
core-sw1.lab:2201 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY...
edge-rtr2:22 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQ...

On startup the agent loads this file into memory; comment lines (starting with #) and blank lines are ignored, and malformed or unparseable lines are skipped with a warning rather than failing the whole load. If the file does not exist yet, the store simply starts empty — your first trusted key creates it (and the .ssh directory if needed).

NetStacks rewrites the whole file

When NetStacks persists a trust or a revoke it rewrites known_hosts in its own normalized form (managed header, one canonical line per key). It does not preserve hashed hostnames, OpenSSH markers like @cert-authority, or comments you added by hand. If you share this file with system ssh and rely on those, keep a separate known_hosts for NetStacks.

Inspect the file directly

You can audit the same data from a shell. To see what NetStacks trusts, read the file; to compare a line's fingerprint, reconstruct the key and hash it with ssh-keygen:

# Show the trusted keys NetStacks manages
cat ~/.ssh/known_hosts

# Get the SHA256 fingerprint of one stored key
# (strip the "host:port" field, feed "type base64" to ssh-keygen)
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGqJ..." | ssh-keygen -lf -

Per-Session Auto-Accept (MOP escape hatch)

Interactive sessions always prompt. Automated runs cannot pop a modal and wait for a human, so NetStacks exposes a single, explicit, non-persistent opt-in on the connect call — auto_accept_changed_keys. When set, an Unknown or Changed key is trusted automatically and persisted, without a prompt. This is the deliberate "I expect a key change because the device was just RMA'd / re-imaged" path used by Method-of-Procedure (MOP) execution.

Auto-accept defeats MITM protection for that connection

auto_accept_changed_keys trusts whatever key the host presents, including a changed one, with no verification. Use it only for trusted, controlled automation against devices you expect to have re-keyed. It defaults to false everywhere, is decided per session, and is never stored as a global setting.

It is genuinely per-session: there is no setting that turns it on for everything. Interactive Terminal connections, SFTP, and ordinary command execution all run with it off, so they always go through the prompt flow. Attempting to set the old global ssh.hostKeyChecking key is rejected by the agent:

{
  "error": "ssh.hostKeyChecking is no longer configurable — strict host-key checking is always on. Per-session opt-in is the only escape hatch.",
  "code": "VALIDATION"
}

API Reference

The Terminal and the Local Agent expose the same host-key surface (the agent over its local sidecar, the Controller over the same paths). Endpoints are rooted under /api.

Pending prompts

GET /api/host-keys/prompts lists currently-pending fingerprint prompts. The sidecar returns a raw array; the Controller wraps it as { "prompts": […] }. Each prompt carries the fields below:

// GET /api/host-keys/prompts
[
  {
    "id": "8c1f3e2a-...",        // UUID — pass back to approve/reject
    "host": "192.168.1.1",
    "port": 22,
    "is_changed_key": false,      // true => host had a different key on file
    "fingerprint": "SHA256:abc...",          // key the host just presented
    "previous_fingerprint": null,            // SHA256 of prior trusted key, if changed
    "created_at": "2026-06-16T12:00:00Z"     // RFC3339 — drives the 120s countdown
  }
]

Resolve a prompt

Approving lets the handshake continue and persists the key; rejecting aborts the handshake with a host-key error. Both return 204 No Content on success, or 404 with code NOT_FOUND if the prompt already timed out or was resolved.

# Approve — trust the presented key and continue the SSH handshake
curl -X POST http://127.0.0.1:PORT/api/host-keys/prompts/8c1f3e2a-.../approve

# Reject — abort the handshake; nothing is written to known_hosts
curl -X POST http://127.0.0.1:PORT/api/host-keys/prompts/8c1f3e2a-.../reject

List & revoke trusted keys

GET /api/host-keys returns every trusted entry (the data behind the Trusted Hosts tab). DELETE /api/host-keys/:host/:port revokes one; it returns 204 when an entry was removed and 404 when no key was stored for that pair.

# List every trusted host key
curl http://127.0.0.1:PORT/api/host-keys
# => [{ "host": "192.168.1.1", "port": 22,
#       "key_type": "ssh-ed25519", "fingerprint": "SHA256:abc..." }]

# Revoke the trusted key for 192.168.1.1:22
# (next connection re-prompts via TOFU)
curl -X DELETE http://127.0.0.1:PORT/api/host-keys/192.168.1.1/22
Endpoint summary
GET /api/host-keys/prompts
List pending fingerprint prompts.
POST /api/host-keys/prompts/:id/approve
Accept a fingerprint; the key is persisted and the handshake continues.
POST /api/host-keys/prompts/:id/reject
Refuse a fingerprint; the handshake aborts.
GET /api/host-keys
List every trusted host key.
DELETE /api/host-keys/:host/:port
Revoke a trusted key so the next connection re-prompts.

Q&A

What does "Trust This Key" actually do?
It writes the host's presented public key to your known_hosts file (default ~/.ssh/known_hosts) under the exact host:port you connected to, and lets the SSH handshake continue. Every later connection compares the presented key against this stored one.
Can I turn off host-key checking entirely?
No. Strict checking is always on and there is no global toggle — the old ssh.hostKeyChecking setting was removed and is now rejected. The only way to skip a prompt is the per-session auto-accept opt-in used by automation, which is never persisted.
The host key changed. Is it an attack?
Possibly. A changed key is what a man-in-the-middle looks like, but it also happens after a firmware upgrade, RMA, or re-image. Verify the new fingerprint out-of-band (read it from the device with show ssh server host-keys or ssh-keygen -lf) before clicking Trust New Key Anyway. If you cannot verify it, reject.
How long do I have to respond to a prompt?
120 seconds. If you do not accept or reject within two minutes, the prompt auto-rejects and the handshake aborts; reconnect to try again.
I accepted a key by mistake. How do I undo it?
Open Settings → Trusted Hosts, find the entry, and click Revoke. The key is removed and your next connection to that host re-prompts you to verify a fresh fingerprint.
Where is the trusted-keys file and what format is it?
~/.ssh/known_hosts, in standard OpenSSH format with a NetStacks-managed header. Each line is host:port key-type base64-key. See The known_hosts File.
Why is the same device listed twice in Trusted Hosts?
Trust is keyed by host:port. The same hostname on a different port (for example a console line on 2201) is a separate entry with its own fingerprint.
Do automated MOP runs prompt me?
No — automation cannot wait on a modal. MOP execution can pass the explicit auto_accept_changed_keys opt-in to trust the presented key without a prompt. It is off by default and decided per session. Interactive sessions always prompt.

Troubleshooting

The prompt never appears

The Terminal polls for pending prompts about every 750 ms while a connection is in flight. In Controller (enterprise) mode it only polls once you are authenticated, because the host-key prompt endpoint requires a valid session. If a connection seems to hang at the host-key stage, make sure you are signed in, then reconnect.

Connection aborts with a host-key error before I can respond

A pending prompt times out after 120 seconds and auto-rejects, aborting the handshake. Reconnect and accept (or reject) within the countdown. If you were away from the machine, just retry the connection.

"host key for host:port changed ... refusing connection"

The host presented a different key than the one you trusted and the connection path had no way to prompt you, so it refused outright. Verify the change is legitimate, then either revoke the entry in Trusted Hosts (so the reconnect shows a clean first-trust prompt) or reconnect through an interactive path that can surface the changed-key prompt.

I can't set ssh.hostKeyChecking

That is intentional. The agent returns a VALIDATION error for that key because strict checking is always on. Use the per-session auto-accept opt-in for automation instead.

A line in known_hosts is being ignored

The loader skips malformed or unparseable lines (and comments / blank lines) with a warning rather than failing. Confirm the line is host:port key-type base64-key with a valid OpenSSH key. The simplest fix is to delete the entry and re-trust the host on the next connection.

Revoke says nothing happened

Revoking a host:port that was never trusted is a no-op — the API returns 404 and the list is unchanged. Check that the host and port match an existing row exactly (remember the :port distinction).