network-device-cli · v1.1.0 · 2026-11-05 · sha256 6c4b962af1f73f3a

network-device-cli v1.1.0B

Immutable. This exact content never changes and is served at /api/v1/blob/6c4b962af1f73f3a.

---
name: network-device-cli
description: "Use when SSHing into a router, switch, or firewall CLI."
version: 1.1.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [ssh, cisco, ios, network, legacy-crypto, cli]
---

# Network Device CLI (Cisco IOS and other appliances)

## When to Use

- The user gives a host + credentials and asks for a command (ping, show) to be
  run **on** the device — router, switch, firewall, appliance.
- An SSH attempt to network gear fails with `Permission denied` or
  `no acceptable kex algorithm`.
- You need device-side reachability (can the router reach 8.8.8.8?) rather than
  host-side.
- A request wants device config **changed** (interface, VLAN, route, ACL), not
  just read — see "Before a config change" below.
- You are rebuilding a device-inventory / automation workflow that must survive
  session resets — the inventory (hostname, ip, port, credentials, config_list,
  checkpoint) belongs on disk, never in the conversation.

Not for a Linux server over SSH — plain `ssh user@host` works there.

Network gear is not a Linux box. It runs an exec-mode CLI with paging, restricted
exec commands, and — on anything a few years old — SSH algorithms that modern
OpenSSH disables by default. Follow this order; each step fails in a way that
misdiagnoses as "wrong password" if you skip it.

## 1. Confirm reachability, then read the banner

```bash
ping -c1 -W2 <host>
ssh -v -o ConnectTimeout=10 -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null <user>@<host> exit 2>&1 \
  | grep -iE "remote software|kex: algorithm|Authentications that can continue"
```

`remote software version Cisco-1.25` plus `kex: algorithm: (no match)` means
legacy crypto, NOT bad credentials. Solve the crypto before you touch the
password — otherwise every auth attempt reports `Permission denied` and you burn
time guessing passwords that were right all along.

## 2. Force legacy algorithms per connection

Modern OpenSSH still *ships* these algorithms; it only stops offering them. So
the fix is flags on the invocation, not an install and not a config edit:

```
-o KexAlgorithms=+diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1 \
-o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa \
-o Ciphers=+aes128-cbc,3des-cbc -o MACs=+hmac-sha1 \
-o PreferredAuthentications=password -o NumberOfPasswordPrompts=1
```

Do **not** edit `/etc/ssh/ssh_config.d/*.conf` for this: it needs root, and it
weakens every future SSH connection instead of one. Keep the legacy flags inside
the invocation (or a `Host` block in `~/.ssh/config` scoped to that IP).

## 3. Password auth without sshpass/expect

`sshpass` is usually absent and installing it needs root. OpenSSH's own askpass
hook works fine and keeps the password out of `argv` and out of logs:

```bash
cat > /tmp/askpass.sh <<'EOF'
#!/bin/bash
printf '%s\n' "$SSH_PROBE_PW"
EOF
chmod +x /tmp/askpass.sh

SSH_PROBE_PW="$PW" SSH_ASKPASS=/tmp/askpass.sh SSH_ASKPASS_REQUIRE=force DISPLAY=:0 \
  ssh -tt <legacy flags> <user>@<host> '<command>'
```

`SSH_ASKPASS_REQUIRE=force` is what makes it work without `setsid`. Do not put
the password inline in the script or on the ssh command line.

## 4. Issue commands the way the device accepts them

- **One command per connection** on IOS. A single string with IOS arguments
  (`ping 8.8.8.8 repeat 5 timeout 2`) is rejected as
  `Line has invalid autocommand "…"` — exec mode takes the bare form only
  (`ping 8.8.8.8`, default 5 packets). For arguments, drive an interactive
  session instead of exec mode.
- **Hold the channel open.** With `< /dev/null` the session closes the moment
  auth completes and you get a truncated reply (a lone `!` from a 5-packet
  ping). Pipe a sleep into it instead:
  ```bash
  { sleep 25; } | ssh -tt <flags> <user>@<host> 'ping 8.8.8.8' 2>&1 | tr -d '\r'
  ```
- **`-tt`** allocates the PTY the CLI expects; without it IOS output arrives
  mangled or not at all. `tr -d '\r'` afterwards makes the output readable.
- **Paging:** `terminal length 0` as an exec command can drop the session.
  Prefer reading short `show` output, or set paging inside an interactive
  session only.

## 5. Report the device's own verdict

Quote the summary line verbatim — `Success rate is 100 percent (5/5),
round-trip min/avg/max = 8/36/91 ms`, `%SYS-5-CONFIG_I`, `state: up`. Do not
paraphrase into your own numbers, and do not claim success without the device's
line in the output.

## 6. Before a config change: plan, approval, checkpoint

Reading is cheap; writing is not. For anything that changes state:

1. Read first. Pull the running config and the relevant `show` output before
   proposing a diff — never configure from memory of what the device "should"
   look like.
2. Present the plan before touching the device, and stop there. Five blocks,
   in this order: (a) what changes and why, in one short reason; (b) the exact
   command list, verbatim; (c) the exact rollback command list; (d) the read-only
   command that will verify it afterwards plus the output/state you expect;
   (e) risk — which lines can cut you off, and why they do not here. A
   confirmation that has not seen all five is not approval. This applies even
   when the user asked for the change in the same message, and even for one line.
   If any line changes after approval, the old approval is void: re-present,
   re-ask.
3. Save the pre-change running config to a file, and write the rollback script
   before applying. `references/config-change-safety.md` has the checkpoint →
   apply → save → verify → rollback loop and the per-vendor command profiles.
   Where the `network-change-management` skill is installed, run its rollback
   generator: `rollback_gen <script> --existing-config <saved-config>` drafts
   the rollback — read it line by line and fill its
   `# TODO restore from snapshot:` entries before use.
4. Never send a command that can cut your own path back in: VTY/management
   interface, `access-class`, default route, the interface you are connected
   through.
5. Verify with the command promised in (d) once the change lands, and quote the
   device's own output line. The device echoing the config line is not
   verification; reading the resulting state back is.

A change is not "done" because the device echoed the line. Re-read the running
config and check the lines are actually present; a capture that shows no error is
weaker evidence than a captured config containing the line.

## 7. Persistent interactive session (OpenSSH)

One-shot OpenSSH suits a single `show`. For a session that enters `enable` with a
secret and then runs a dozen commands, hold one PTY open instead of spawning an
ssh per command — the engine's own technique, no paramiko, no Python:

```bash
SSH_ASKPASS=/tmp/askpass SSH_ASKPASS_REQUIRE=force setsid \
  ssh -tt -o KexAlgorithms=+diffie-hellman-group14-sha1 \
      -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa \
      -o Ciphers=+aes128-cbc -o MACs=+hmac-sha1 admin@10.0.0.5 < plan
```

- `SSH_ASKPASS` + `setsid` (no controlling tty) supplies the password without
  sshpass; `SSH_ASKPASS_REQUIRE=force` needs OpenSSH 8.4+.
- `-tt` forces the PTY so the device prints prompts; feed a paced plan of
  `delay<TAB>command` pairs on stdin — pause after connect, then one line at a
  time. Pacing replaces read-until-idle: gear that pauses mid-output otherwise
  gets the next command typed into the middle of the previous one.
- On a piped PTY the prompt is never a standalone line: the device prints it and
  the next command lands on the same line (`R2>show clock`). Parse prompts as a
  line *prefix* (`^[A-Za-z0-9_.-]{1,63}[>#]`), not a line suffix.
- Disable paging first (`terminal length 0`); a stray `--More--` cannot be
  answered from a pipe.
- `enable`: send `enable`, pause, send the secret, then confirm the prompt ends
  in `#`. Report the prompt you actually saw rather than assuming the enable
  succeeded.
- Keep the password out of `argv` — the askpass helper reads it from the
  environment.

`scripts/ios_session.sh` is this session shape as a runnable CLI (same flags as
the old paramiko version: `--host --user --password --secret --cmd --show-config
--paging`).

## Credential parsing pitfall

Credentials arrive as run-together prose: `user admin pass S3cr3t! secret en4ble!` is
user `admin`, **password `S3cr3t!`**, and `en4ble!` is the *enable secret* (privileged
exec password) — not a passphrase with spaces. Try the whole string, then the
single token, and report which one authenticated rather than silently using it.
A Cisco banner plus a rejected multi-word password is the signal that you split
the string wrong.

## Supporting files

- `scripts/legacy-ssh.sh` — wraps the flags, askpass, and hold-open stdin so a
  one-shot command works first try.
- `scripts/ios_session.sh` — persistent OpenSSH PTY session (enable, paging off,
  read running config) as a runnable CLI.
- `references/config-change-safety.md` — checkpoint → apply → save → verify →
  rollback loop, rollback-script rules, and per-vendor command profiles.