> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# All VTY Lines Busy on Cisco IOS XE: Fix Stale SSH Sessions
- URL: https://www.pinglabz.com/vty-lines-busy-ssh-exec-timeout-tcp-keepalives/
- Published: 2026-10-26T16:05:30.000Z
- Updated: 2026-10-26T16:27:34.000Z
- Description: Built in my home lab on IOS XE 17.18: stale SSH sessions filled every VTY line and locked the admin out. Keepalives, exec-timeout and a reserved line fixed it.
- Author: Jaime
- Tags: Network Security, IOS XE, Troubleshooting, SSH, Labs, #nsa-guide

*Part of my series testing the* [*NSA Network Infrastructure Security Guide*](https://www.pinglabz.com/infrastructure-security/) *in my home lab.*

You SSH to a branch router during an incident and get `Connection refused`. The router is up, routing works, and the console is a long drive away. Very often the cause is that every VTY line is held by a session whose client is long gone: a laptop that went to sleep, a VPN that dropped, a jump host that rebooted. The router never heard a goodbye, so it keeps the line.

I built this in my home lab on IOS XE 17.18.2, filled all five VTY lines with sessions whose clients I then cut off, and tested the two fixes the [NSA Network Infrastructure Security Guide (v1.2, Oct 2023)](https://media.defense.gov/2022/Jun/15/2003018261/-1/-1/0/CTR%5FNSA%5FNETWORK%5FINFRASTRUCTURE%5FSECURITY%5FGUIDE%5F20220615.PDF?ref=pinglabz.com) recommends: an idle timeout of five minutes or less (section 7.5) and TCP keepalives in both directions (section 7.6). The guide warns that with no timeout, an idle-connection DoS can last until the timeout is reached, "which could be indefinite if the idle timeout has been disabled." That is exactly what `exec-timeout 0 0` does.

## The lab

Built in my home lab with Containerlab: one Cisco IOL router running IOS XE 17.18.2 and three Alpine Linux hosts with OpenSSH and sshpass.

- **BRANCH1**: Ethernet0/1 10.74.10.1/24, Ethernet0/2 10.74.20.1/24, Ethernet0/3 10.74.30.1/24\. SSH v2 only, local login, five VTY lines (0 to 4).
- **c1** (10.74.10.11) and **c2** (10.74.20.12): users whose sessions will go stale.
- **admin** (10.74.30.10): the engineer who needs to get in.

![Lab diagram: two clients holding all five VTY lines on BRANCH1 while the admin host is refused.](https://storage.ghost.io/c/ff/28/ff28db4a-11e1-4835-b928-158974cf96c1/content/images/2026/10/vty-lines-busy-ssh-exec-timeout-tcp-keepalives-1.png)

Passwords in the outputs are placeholders like `<netops-pw>`. The starting config is the one I still find on a lot of routers: no idle timeout and no keepalives.

```text
BRANCH1#show running-config | include service tcp|^ip ssh
ip ssh bulk-mode 131072
ip ssh rsa keypair-name SSH-KEY
BRANCH1#show running-config | section line vty
line vty 0 4
 exec-timeout 0 0
 activation-character 13
 login local
 transport input ssh
BRANCH1#show ip ssh | include SSH|timeout
SSH Enabled - version 2.0
Authentication timeout: 120 secs; Authentication retries: 3
IOS Keys in SECSH format(ssh-rsa, base64 encoded): SSH-KEY

```

Note that `show running-config | include service tcp` returned nothing: neither TCP keepalive service is configured on this router.

## Fault: all VTY lines in use, SSH connection refused

I opened three SSH sessions from c1 and two from c2 and left them idle. Every VTY line was now in use:

```text
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   2 vty 0     netops     idle                 00:00:25 10.74.10.11
   3 vty 1     netops     idle                 00:00:30 10.74.10.11
   4 vty 2     netops     idle                 00:00:27 10.74.10.11
   5 vty 3     netops     idle                 00:00:19 10.74.20.12
   6 vty 4     netops     idle                 00:00:24 10.74.20.12
BRANCH1#show tcp brief
TCB       Local Address               Foreign Address             (state)
773BC7F8E6B8  10.74.10.1.22              10.74.10.11.58830           ESTAB
773BC5E72418  10.74.10.1.22              10.74.10.11.55406           ESTAB
773BC91714C8  10.74.20.1.22              10.74.20.12.35276           ESTAB
773BC9017000  10.74.10.1.22              10.74.10.11.58818           ESTAB
773BC261D2F0  10.74.20.1.22              10.74.20.12.35270           ESTAB
------------  0.0.0.0.22                 *.*                         LISTEN

```

Then I made the clients disappear the way a sleeping laptop or a dead VPN does. I added a firewall rule on c1 and c2 that drops all traffic to and from the router, then the SSH clients exited. With that rule in place nothing from the clients, not even a FIN, can reach the router, so it never learns the sessions ended. The admin host now cannot get in at all:

```text
admin$ sshpass -p "<netops-pw>" ssh netops@10.74.30.1 'show users'; echo "sshpass exit $?"
sshpass exit 255
ssh: connect to host 10.74.30.1 port 22: Connection refused

```

This is a TCP refusal, not an authentication failure. With every VTY line taken, the router stops accepting new connections on port 22\. Four minutes after the cut nothing had changed. The sessions were six and a half minutes idle, the router still showed all five SSH sessions as started, and the admin host was still refused:

```text
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   2 vty 0     netops     idle                 00:06:37 10.74.10.11
   3 vty 1     netops     idle                 00:06:37 10.74.10.11
   4 vty 2     netops     idle                 00:06:34 10.74.10.11
   5 vty 3     netops     idle                 00:06:27 10.74.20.12
   6 vty 4     netops     idle                 00:06:32 10.74.20.12
BRANCH1#show ssh | include IN
0          2.0     IN   chacha20-polhmac-sha2-256-etm@openssh.com Session started       netops
1          2.0     IN   chacha20-polhmac-sha2-256-etm@openssh.com Session started       netops
2          2.0     IN   chacha20-polhmac-sha2-256-etm@openssh.com Session started       netops
3          2.0     IN   chacha20-polhmac-sha2-256-etm@openssh.com Session started       netops
4          2.0     IN   chacha20-polhmac-sha2-256-etm@openssh.com Session started       netops

```

With `exec-timeout 0 0` and no keepalives, nothing on the router is ever going to clear these sessions. The only way in is the console.

## Immediate fix: show users and clear line from the console

From the console, `show users` tells you which line to clear. Clearing one is enough to let one admin session in:

```text
BRANCH1#clear line vty 0
[confirm]
 [OK]
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   3 vty 1     netops     idle                 00:07:01 10.74.10.11
   4 vty 2     netops     idle                 00:07:06 10.74.10.11
   5 vty 3     netops     idle                 00:06:58 10.74.20.12
   6 vty 4     netops     idle                 00:07:03 10.74.20.12

```

```text
admin$ sshpass -p "<netops-pw>" ssh netops@10.74.30.1 'show users'
    Line       User       Host(s)              Idle       Location
   0 con 0                idle                 00:00:17   
*  2 vty 0     netops     idle                 00:00:00 10.74.30.10
   3 vty 1     netops     idle                 00:07:27 10.74.10.11
   4 vty 2     netops     idle                 00:07:32 10.74.10.11
   5 vty 3     netops     idle                 00:07:24 10.74.20.12
   6 vty 4     netops     idle                 00:07:29 10.74.20.12

```

Once you are in over SSH you can clear the rest from there. But this only works if someone can reach the console, which is the whole problem at a remote site. The real fix is to make the router clean up by itself.

## Fix 1: service tcp-keepalives-in clears dead SSH sessions

I enabled keepalives in both directions as the guide recommends, kept `exec-timeout 0 0` on purpose so the keepalives were the only thing that could clear a session, and cleared the old lines:

```text
BRANCH1(config)#service tcp-keepalives-in
BRANCH1(config)#service tcp-keepalives-out
BRANCH1(config)#end
BRANCH1#show running-config | include service tcp
service tcp-keepalives-in
service tcp-keepalives-out

```

Then a new test: three fresh sessions from c1 and one from c2\. I cut c1 off the same way as before, and left c2 connected but idle, so it was a real user who simply was not typing.

```text
BRANCH1#show clock
*15:35:00.589 UTC Mon Oct 26 2026
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   2 vty 0     netops     idle                 00:00:19 10.74.10.11
   3 vty 1     netops     idle                 00:00:24 10.74.10.11
   4 vty 2     netops     idle                 00:00:21 10.74.10.11
   5 vty 3     netops     idle                 00:00:14 10.74.20.12

```

Three minutes after the cut (15:38:22 on the router clock) show users still listed all three c1 sessions. By the seven-minute check they were gone, and the c2 session, idle for the same time but with a live client, was untouched:

```text
BRANCH1#show clock
*15:42:36.160 UTC Mon Oct 26 2026
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   5 vty 3     netops     idle                 00:07:42 10.74.20.12
BRANCH1#show logging | include SSH|TCP|LINK|vty
(earlier lines trimmed)
*Oct 26 15:39:41.943: %SSH-5-SSH2_CLOSE: SSH2 Session from 10.74.10.11 (tty = 0) for user '' using crypto cipher 'chacha20-poly1305@openssh.com', hmac 'hmac-sha2-256-etm@openssh.com' closed
*Oct 26 15:39:44.936: %SSH-5-SSH2_CLOSE: SSH2 Session from 10.74.10.11 (tty = 1) for user '' using crypto cipher 'chacha20-poly1305@openssh.com', hmac 'hmac-sha2-256-etm@openssh.com' closed
*Oct 26 15:39:47.944: %SSH-5-SSH2_CLOSE: SSH2 Session from 10.74.10.11 (tty = 2) for user '' using crypto cipher 'chacha20-poly1305@openssh.com', hmac 'hmac-sha2-256-etm@openssh.com' closed

```

The c1 sessions had logged in at 15:34:41, 15:34:44 and 15:34:47, according to the router log. Each one closed five minutes later, three seconds apart, in the same order. That matches [Cisco's documented behavior](https://www.cisco.com/c/en/us/td/docs/ios-xml/ios/bsm/configuration/15-2mt/bsm-troubleshooting.html?ref=pinglabz.com): keepalives go out once a minute on an idle connection, and the connection is closed if 5 minutes pass with no response. Here that was about five minutes after the last activity. That is the important limit: keepalives only clear sessions whose client is gone. A user who left a working laptop logged in still holds the line forever with `exec-timeout 0 0`.

## Fix 2: exec-timeout 5 0, the NSA-recommended idle timeout

The guide recommends a session timeout of five minutes or less and says not to use zero, since zero turns the timeout off. I set the VTY lines to five minutes and also cut the SSH login timeout from 120 to 60 seconds, so a half-finished login cannot hold a line for two minutes:

```text
BRANCH1(config)#line vty 0 4
BRANCH1(config-line)#exec-timeout 5 0
BRANCH1(config-line)#exit
BRANCH1(config)#ip ssh time-out 60
BRANCH1(config)#end
BRANCH1#show ip ssh | include timeout
Authentication timeout: 60 secs; Authentication retries: 3
BRANCH1#show clock
*15:42:57.454 UTC Mon Oct 26 2026

```

The idle c2 session was already open when I made the change. I opened a second idle session from c2 at about 15:43 to compare. One minute later both were still there:

```text
BRANCH1#show users
    Line       User       Host(s)              Idle       Location
*  0 con 0                idle                 00:00:00   
   2 vty 0     netops     idle                 00:00:58 10.74.20.12
   5 vty 3     netops     idle                 00:09:23 10.74.20.12

```

The older session was already more than nine minutes idle and still had not been closed. The router did not count idle time from before the change. Both sessions closed five minutes later:

```text
BRANCH1#show logging | include 15:4[3-9].*SSH2_CLOSE
*Oct 26 15:47:57.347: %SSH-5-SSH2_CLOSE: SSH2 Session from 10.74.20.12 (tty = 3) for user 'netops' using crypto cipher 'chacha20-poly1305@openssh.com', hmac 'hmac-sha2-256-etm@openssh.com' closed
*Oct 26 15:48:17.351: %SSH-5-SSH2_CLOSE: SSH2 Session from 10.74.20.12 (tty = 0) for user 'netops' using crypto cipher 'chacha20-poly1305@openssh.com', hmac 'hmac-sha2-256-etm@openssh.com' closed

```

The old session (tty 3) closed exactly five minutes after I changed the config. The new one (tty 0) closed exactly five minutes after it logged in. The client saw this:

```text
BRANCH1#Connection to 10.74.20.1 closed by remote host.

```

So if you fix this during an outage, the sessions that are already stuck do not clear right away. Clear them by hand once and let the timeout take care of the future.

## Fix 3: reserve the last VTY line for the admin subnet

Timeouts limit how long a stuck line stays stuck, but someone can still fill every line on purpose. So I kept the last line for the management subnet with an access-class:

```text
BRANCH1(config)#ip access-list standard ADMIN-ONLY
BRANCH1(config-std-nacl)#permit 10.74.30.0 0.0.0.255
BRANCH1(config-std-nacl)#exit
BRANCH1(config)#line vty 4
BRANCH1(config-line)#access-class ADMIN-ONLY in
BRANCH1(config-line)#end

```

c1 then tried to open five sessions. It got four, and the fifth was refused, because the only free line did not accept its address:

```text
r1: BRANCH1#
r2: BRANCH1#
r3: BRANCH1#
r4: BRANCH1#
r5: ssh: connect to host 10.74.10.1 port 22: Connection refused

```

With c1 holding VTY 0 to 3, the admin host still got in on VTY 4:

```text
admin$ sshpass -p "<netops-pw>" ssh netops@10.74.30.1 'show users'
    Line       User       Host(s)              Idle       Location
   0 con 0                idle                 00:00:04   
   2 vty 0     netops     idle                 00:00:26 10.74.10.11
   3 vty 1     netops     idle                 00:00:23 10.74.10.11
   4 vty 2     netops     idle                 00:00:20 10.74.10.11
   5 vty 3     netops     idle                 00:00:17 10.74.10.11
*  6 vty 4     netops     idle                 00:00:00 10.74.30.10

```

In production I would put an access-class on every VTY line (the guide covers management ACLs in section 7.4) and keep the last line for the smallest admin range. The lockout risk of a wrong ACL is covered in [VTY access-class lockout on IOS XE](https://www.pinglabz.com/vty-access-class-ssh-lockout-management-vrf/).

## Final config: VTY lines, keepalives and SSH timeout

```text
BRANCH1#show running-config | include service tcp|^ip ssh|access-list
service tcp-keepalives-in
service tcp-keepalives-out
ip ssh bulk-mode 131072
ip ssh time-out 60
ip ssh rsa keypair-name SSH-KEY
ip access-list standard ADMIN-ONLY
BRANCH1#show running-config | section line vty
line vty 0 3
 exec-timeout 5 0
 activation-character 13
 login local
 transport input ssh
line vty 4
 access-class ADMIN-ONLY in
 exec-timeout 5 0
 activation-character 13
 login local
 transport input ssh

```

I tested the VTY lines only. The guide's five-minute limit also covers the console and AUX lines, so set `exec-timeout 5 0` under `line con 0` too.

🧪

**Try it yourself.** Start with [the free IOS essentials lab](https://labs.pinglabz.com/labs/ess-01?plz%5Fplacement=post:vty-busy:cta:v1&ref=pinglabz.com), no card needed. [PingLabz Pro](https://www.pinglabz.com/membership/?plz%5Fplacement=post:vty-busy:cta:v1) unlocks every lab in the browser.

## Key takeaways

- `Connection refused` on port 22 from a router that is otherwise healthy often means every VTY line is in use. Check `show users` from the console.
- `exec-timeout 0 0` plus no keepalives means stale sessions never clear. In this lab they were still there after more than seven minutes idle, and nothing in that config would ever remove them.
- `service tcp-keepalives-in` and `-out` removed sessions whose client had vanished, about five minutes after their last activity here, but left a live idle user alone.
- `exec-timeout 5 0` removes idle users too. A session already open when you change it gets a fresh five minutes, so clear stuck lines by hand during the outage.
- `ip ssh time-out 60` stops half-finished logins from holding a line for two minutes. (Cisco default is 120 seconds; I did not time this one.)
- Keep one VTY line for the admin subnet with an access-class, so a full set of lines from anywhere else cannot lock you out.

For SSH setup itself, see [How to enable SSH on Cisco routers and switches](https://www.pinglabz.com/enable-ssh-cisco/). For the rest of the management plane, see [Hardening the management plane on IOS XE](https://www.pinglabz.com/hardening-management-plane-ios-xe/) and [login block-for and quiet-mode access-class](https://www.pinglabz.com/login-block-for-quiet-mode-access-class/). For tunnels and jump hosts, see [SSH beyond login](https://www.pinglabz.com/ssh-tunneling-scp/). Other fixes from the same series: [SNMP RW community abuse and the SNMPv3 fix](https://www.pinglabz.com/snmp-rw-community-abuse-snmpv3/), [Cisco password types 7, 5, 8 and 9](https://www.pinglabz.com/cisco-password-types-7-5-8-9-audit-migrate/) and [Read-only users: privilege levels vs parser views](https://www.pinglabz.com/cisco-read-only-noc-user-privilege-levels-parser-views/). Source recommendations: [NSA Network Infrastructure Security Guide (v1.2, Oct 2023)](https://media.defense.gov/2022/Jun/15/2003018261/-1/-1/0/CTR%5FNSA%5FNETWORK%5FINFRASTRUCTURE%5FSECURITY%5FGUIDE%5F20220615.PDF?ref=pinglabz.com), sections 7.4, 7.5 and 7.6.