Guide

How to Restrict SSH/Telnet Access with a VTY access-class ACL (Step by Step)

You set up SSH, generated the keys, and the management session is now encrypted. Encrypted is not the same as restricted. SSH protects the contents of a session; it does nothing to control who is allowed to open one. Any host that can route a packet to the device can still reach the login prompt and start guessing usernames and passwords. The fix is a standard ACL applied to the VTY lines with the access-class command. It drops connection attempts from any source you did not explicitly permit — before authentication happens, so a blocked host never even reaches the prompt. This guide builds that ACL, applies it to every VTY line the platform has, pairs it with an SSH-only transport, and verifies the result from both an allowed and a denied source.

Step 1 — Understand what access-class protects (and what it doesn't)

VTY lines are the virtual terminals that accept inbound SSH and Telnet sessions. When someone opens an SSH connection to the device's IP, they land on a VTY line. There are a fixed number of them, and by default any source that can route a packet to the device can occupy one.

access-class is the tool that filters those inbound VTY sessions by source IP. It sits in front of authentication: a source the ACL denies never sees the login prompt and never gets to try a password. That is different from — and stronger than — filtering somewhere on an interface.

Do not confuse access-class with ip access-group. ip access-group binds an ACL to an interface and filters packets crossing that interface. access-class binds an ACL to a line (VTY, console, or aux) and controls who may open a management session, regardless of which interface the packet arrived on. For device management you want access-class, because it protects the VTY lines no matter how many interfaces, subinterfaces, or loopbacks reach the device.

Access control and encryption are separate jobs. transport input ssh decides HOW a session may connect (encrypted SSH versus plaintext Telnet). access-class decides WHO may connect at all. A hardened device uses both.

Step 2 — Inventory your VTY lines and choose the management source

Before writing the ACL, find out how many VTY lines this platform actually has. The classic answer is five (vty 0 4), but most modern IOS and IOS-XE devices expose sixteen (0 15), and some expose more. If you protect only 0 4 and the box has 0 15, lines 5 through 15 are left wide open.

Decide the exact source you trust. Usually that is your management subnet — 192.168.100.0/24 in this guide. Keep it as tight as your operations allow: a single jump host is safer than an entire user VLAN.

show line lists every TTY and VTY line so you can count them. show run | section line vty shows the current line configuration and any ACL already applied.

R1# show line
R1# show run | section line vty

Step 3 — Build the standard ACL for the management subnet

A standard ACL matches on source address only, which is exactly what VTY filtering needs — you are deciding who may connect, not what they may reach. Numbered standard ACLs use 1–99 (or the extended range 1300–1999); named standard ACLs work identically and read better in a running-config.

The wildcard mask is the inverse of a subnet mask. 0.0.0.255 means 'match the first three octets, ignore the last,' which covers the whole 192.168.100.0/24. A remark documents intent for the next engineer who reads the config.

Every ACL ends in an invisible deny any. You do not type it, but it is always there — anything you did not permit is dropped. That implicit deny is the entire mechanism here, and it is also what locks people out when they forget to permit their own subnet (see Common problems).

The implicit deny is silent. If you want visibility into rejected attempts, add an explicit deny any log as the last line: it counts hits and generates a syslog message whenever a blocked host tries to connect.

You can permit a single jump host with permit host 192.168.100.10, or list several sources by adding more permit lines above the deny.

R1# configure terminal
R1(config)# access-list 10 remark === Permit MGMT subnet to VTY ===
R1(config)# access-list 10 permit 192.168.100.0 0.0.0.255
R1(config)# access-list 10 deny   any log

! Named equivalent (reads better in the running-config):
R1(config)# ip access-list standard VTY-MGMT
R1(config-std-nacl)# permit 192.168.100.0 0.0.0.255
R1(config-std-nacl)# deny   any log
R1(config-std-nacl)# exit

Step 4 — Apply the ACL to EVERY VTY range with access-class in

access-class <acl> in filters inbound sessions — the direction that governs who can log in. Apply it to every VTY range the platform has, not just the first block.

Safety first: applying a VTY ACL over SSH can lock you out instantly if the ACL is wrong, because the implicit deny takes effect the moment you commit. Before you apply it remotely, arm an automatic rollback. reload in 10 schedules a reboot in ten minutes; answer 'no' to the save prompt so the router will reload the last saved config if you lose access. Once you confirm you can still reach the device, cancel the reload and save.

The two-range form below (0 4 then 5 15) guarantees you cover both the legacy 0–4 block and the 5–15 block on platforms that split them. If your platform reports lines 0 15, you can instead use line vty 0 15 to configure all sixteen in one pass.

Note for dual-stack devices: access-class 10 is an IPv4 ACL only. If the device has IPv6 addresses, SSH over IPv6 stays open until you apply an IPv6 equivalent (ipv6 access-class) — covered in Common problems.

R1(config)# do reload in 10
System configuration has been modified. Save? [yes/no]: no
Reload scheduled in 10 minutes by admin
Proceed with reload? [confirm]

R1(config)# line vty 0 4
R1(config-line)# access-class 10 in
R1(config-line)# exit
R1(config)# line vty 5 15
R1(config-line)# access-class 10 in
R1(config-line)# end

! Still connected? Cancel the rollback and save.
R1# reload cancel
R1# write memory

Step 5 — Restrict the transport to SSH only

access-class controls who; transport input controls how. Even with the ACL in place, if Telnet is still permitted, a trusted host can log in over cleartext and hand its credentials to anyone sniffing the path. Lock the transport to SSH so passwords are never sent in the clear.

transport input ssh permits SSH and implicitly denies Telnet on those lines. Apply it to the same VTY ranges you protected in Step 4. If you ever need to disable remote shell entirely on a line, transport input none does that.

You can set access-class and transport input in the same line-config pass; they are shown in separate steps only for clarity. transport input ssh assumes SSH is already working on this device — a hostname, a domain name, RSA keys, and a local or AAA login method must all be in place, or SSH will not start. SSH bootstrap is a separate topic; this guide focuses on the access-class lock.

R1(config)# line vty 0 4
R1(config-line)# transport input ssh
R1(config-line)# exit
R1(config)# line vty 5 15
R1(config-line)# transport input ssh
R1(config-line)# end

Step 6 — Verify the config and test from an allowed and a denied source

First confirm the ACL is applied to every VTY range and reads the way you expect. No VTY stanza should be missing the access-class line.

Then prove it from two sides. An SSH attempt from a permitted source (192.168.100.x) should reach the login prompt. An attempt from any other source should be refused or closed before login — access-class denies at connection setup, so a blocked host typically sees 'Connection refused,' 'connection closed by remote host,' or a timeout, and never a password prompt.

Watch the ACL counters — and syslog, if you added deny any log — to confirm denied attempts are actually landing on the deny line rather than slipping through.

Test this on real IOS (a Cisco Modeling Labs topology or physical gear), where the pre-authentication drop behaves exactly as described. Simulators do not always reproduce access-class behavior faithfully, so verify the lock on real images before you rely on it.

R1# show run | section line vty
R1# show access-lists 10
R1# show ip access-lists 10

! From an ALLOWED host (192.168.100.10) — should reach the login prompt:
Admin-PC:~$ ssh admin@192.168.100.1

! From a DENIED host (10.0.0.5) — should be refused/closed before login:
Attacker:~$ ssh admin@192.168.100.1
ssh: connect to host 192.168.100.1 port 22: Connection refused

! Back on R1 — confirm the deny counter and/or log incremented:
R1# show ip access-lists 10
R1# show logging | include SEC-6-IPACCESSLOG

Common problems (and the fix)

1) You locked everyone out — including yourself. The usual cause is forgetting to permit your own management subnet, permitting the wrong network, or fat-fingering the wildcard mask; the implicit deny then drops every source. Fix: reach the device out-of-band on the console and correct or remove the ACL. Prevention: always stage a reload in 10 before applying a VTY ACL remotely, and double-check the wildcard — a /24 is 0.0.0.255, not the subnet mask 255.255.255.0.

2) You protected vty 0 4 but the box has 0 15. Lines 5–15 carry no access-class, so anyone who lands on one of them bypasses the filter entirely. Fix: apply access-class in to every range and confirm with show run | section line vty that no VTY stanza is missing it. Re-check after IOS upgrades, which can change the line count.

3) You used ip access-group instead of access-class. Binding the ACL to an interface does not reliably lock the VTY lines — management traffic can arrive via other interfaces or loopbacks, and interface ACLs are built to filter transit traffic, not line access. Fix: remove it from the interface and apply the ACL to the lines with access-class in.

4) You applied it in the wrong direction (out instead of in). access-class out governs sessions the device itself originates (outbound Telnet/SSH from the router), not administrators logging in. With out, your inbound restriction simply never takes effect and everyone can still connect. Fix: use access-class 10 in.

5) The dual-stack device is still reachable over IPv6. access-class 10 is an IPv4 ACL and does nothing to IPv6 SSH, so on a device with IPv6 addresses the SSH port stays open over v6. Fix: build a matching IPv6 ACL and apply ipv6 access-class <name> in to the same VTY lines.

! #2 — Verify EVERY vty range carries the ACL
R1# show run | section line vty

! #3 — Wrong tool: remove from the interface, apply to the line
R1(config)# interface GigabitEthernet0/0
R1(config-if)# no ip access-group 10 in
R1(config-if)# line vty 0 15
R1(config-line)# access-class 10 in

! #4 — Fix the direction
R1(config-line)# no access-class 10 out
R1(config-line)# access-class 10 in

! #5 — Lock the IPv6 VTY path too
R1(config)# ipv6 access-list VTY-MGMT-V6
R1(config-ipv6-acl)# permit ipv6 2001:DB8:100::/64 any
R1(config-ipv6-acl)# exit
R1(config)# line vty 0 15
R1(config-line)# ipv6 access-class VTY-MGMT-V6 in

Frequently asked questions

What is the difference between access-class in and access-class out on VTY lines?

The in keyword filters inbound management sessions to the device's VTY lines, deciding which source addresses are allowed to connect and reach the login prompt. The out keyword filters outbound sessions the device itself originates from a VTY line (a Telnet or SSH you launch from the router), matching the destination it is permitted to reach. To control who can manage the box you almost always want access-class <acl> in.

Does a VTY access-class ACL also protect the console and AUX ports?

No. access-class on the VTY lines only governs network login sessions such as Telnet and SSH; it has no effect on the console (line con 0) or auxiliary (line aux 0) ports. Console access is controlled by line passwords and physical security, and the AUX line should be disabled or password-protected separately, so testing only over SSH can leave those paths open.

I applied the ACL but a blocked host can still log in — what did I miss?

The usual cause is an uncovered VTY range: many configs apply the ACL only to line vty 0 4 while the platform also exposes vty 5 15 (and sometimes higher ranges) that were left wide open. Run show run | section vty or show line to list every range the device has, then repeat access-class <acl> in on each one. Also confirm the connecting source really falls outside your permit entries, since a NAT or management-VRF source address may differ from what you assume.

Should I use a standard or an extended ACL on the VTY lines, and can I filter by the SSH port?

A standard ACL is normally the right choice because access-class matches on source address only, which is exactly what management filtering needs. You can attach an extended ACL to also match protocol and port, but pairing a standard source ACL with transport input ssh already restricts both who connects and how they connect, so the extra complexity rarely pays off. Keep the ACL source-only and let the transport statement enforce SSH.

Do I need an explicit deny any at the end, and how do I see denied login attempts?

You do not need a trailing deny any because every ACL ends with an implicit deny, so any source you did not permit is already dropped before authentication. Order still matters among the permit lines since matching is first-hit, and adding an explicit deny any log is worthwhile only to count and timestamp rejected attempts, which you then read with show access-lists or in the log. Remember a standard ACL uses a wildcard mask, so permit 10.1.1.0 0.0.0.255 allows the entire /24 management subnet.

Practice this on graded Cisco labs

Reading is step one — build Access Control Lists (ACLs) on real Cisco IOS and grade your own config, or try a free sample lab first.