Changing the SSH port without locking yourself out: sshd, socket activation and SELinux

Published on 16 min read

Changing the SSH port almost always fails on the order of the steps. This guide keeps the server listening on both ports while you work, so the old one only goes away once the new one is proven to work.

Changing the SSH port is one of the most frequently given and most poorly justified recommendations in server administration. It does have a real benefit, just not the one usually claimed for it, and it comes with a side effect: between the moment the service lets go of the old port and the moment the new one is reachable through every packet filter, there is a gap. If you do not plan for it, you will find it.

This guide carries out the change in a way that never opens that gap: while you work, the server listens on the old and the new port at the same time, and the old one only goes away once the new one is proven to work.

Everything here applies to Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS and Ubuntu 22.04 LTS. A separate section covers SELinux on RHEL systems such as AlmaLinux and Rocky Linux. The commands are written for running as root. If you work as a regular user, put sudo in front of every command. The example port is 2222, the example address 203.0.113.10.

What a different port gets you, and what it does not

No security gain. A full port scan across all 65,535 ports finds the service anyway, and the version banner that OpenSSH sends when a connection is opened gives it away immediately, on port 51022 just as much as on port 22. Changing the port replaces none of the measures that actually work: key-based login instead of a password, password login switched off, a tight packet filter.

Less log noise, and that is worth more than it sounds. The vast majority of what hits port 22 is untargeted mass scanning. Those tools try port 22 and nothing else, because scanning the whole internet on every port does not pay off. Move the service and that class of traffic disappears from the journal, which is what makes a single targeted login attempt stand out in the first place.

Costs you have to factor in. From now on every tool needs the port: backup scripts, deployment runs, monitoring. And one point almost every guide leaves out: corporate networks, hotel Wi-Fi and some mobile plans only allow a handful of outbound ports, usually 22, 80 and 443. From a network like that you may not reach your server at all any more.

In short: do it if you want quiet logs. Do not do it in the belief that you have solved a security problem.

The way back, before you change anything

1. Open the console in the customer panel once

Every KVM root server and every dedicated server at KernelHost has a VNC console in the customer panel. It looks straight at the screen output of the system and works independently of the network stack of the server, so a wrong firewall rule cannot block it. Log in there once beforehand and make sure you know the root password. A rescue route you try for the first time in an emergency is not a rescue route.

2. Two sessions, and the first one stays open

Log in a second time before you start. Existing SSH connections survive both a restart of the service and the closing of the old port in the firewall, because the connection state is already established. So you keep a root shell even when you have long since locked yourself out for new connections. That is exactly the trap: everything looks fine until you close the window.

3. A timer that undoes the change on its own

If both sessions break off, for example because your own internet connection fails, this job rolls the port change back after fifteen minutes:

systemd-run --on-active=15min --unit=ssh-portrollback /bin/sh -c 'rm -f /etc/ssh/sshd_config.d/20-port.conf /etc/systemd/system/ssh.socket.d/override.conf; systemctl daemon-reload; systemctl try-restart ssh.socket ssh.service'

Check: systemctl list-timers ssh-portrollback.timer shows when it will fire. Once everything has worked, cancel the job, otherwise your change reverts later while the server is in production:

systemctl stop ssh-portrollback.timer

The order that does not lock you out

This sequence is built so that at no point is the old way already closed while the new one is not open yet.

  1. Pick a port and check that it is free.
  2. Firewall first: open the new port, leave the old one open.
  3. Make sshd listen on both ports.
  4. With socket activation, adjust the socket unit as well.
  5. Restart and verify on the listening socket.
  6. Log in over the new port with a third, fresh session.
  7. Only now remove port 22 and update the tools.

Step 1: Picking the port

Not 2222. This port is the most common fallback choice and mass scanners have long been probing it as well. We only use it here because it reads well as an example.

Stay below 32768. The range from which the kernel hands out source ports for outgoing connections is written here:

cat /proc/sys/net/ipv4/ip_local_port_range

The usual output is 32768 60999. A port from that range can be taken temporarily by an outgoing connection. Most of the time this goes well, but after a reboot, when other services start before sshd, the bind fails with Address already in use and the server comes up without SSH. It happens sporadically and is unpleasant to track down.

Below 1024 has a real advantage. Only root may bind ports below 1024. So if sshd goes down, no unprivileged user can hook in there and run a fake SSH service that records credentials. With several user accounts that is an argument, with a single administrator it stays rather theoretical.

Check: the port must be neither in use nor reserved for a service you want to run later. Both commands should print nothing:

ss -tlnp | grep -E ':2222 '
grep -w 2222 /etc/services

Step 2: The firewall first

This step comes before the sshd configuration, not after it. An open port with no service behind it is harmless, a service without an open port locks you out.

ufw allow 2222/tcp comment 'SSH new'
ufw status verbose

Check: the output must now list both ports, twice each, once for IPv4 and once with the addition (v6). If the IPv6 line is missing, the new port is not reachable over IPv6, and modern clients try IPv6 first. Details in our guide to the UFW firewall.

If you maintain nftables by hand, add the port to your rule file, reload it and check the loaded ruleset with nft list ruleset | grep 2222. On RHEL systems with firewalld:

firewall-cmd --permanent --add-port=2222/tcp
firewall-cmd --reload
firewall-cmd --list-ports

Two places get overlooked. First fail2ban: if a ban is in effect there, after the switch it still blocks port 22 only, while the attempts on the new port go through. Add the port in /etc/fail2ban/jail.local:

[sshd]
enabled = true
port    = 2222

Detection keeps working, because fail2ban reads the login attempts from the journal instead of keying on the port. Only the blocking depends on this line, see our guide to fail2ban. Second, an upstream packet filter outside the server, otherwise you look for the fault in the wrong place.

Step 3: sshd_config and the sshd_config.d trap

Do not touch /etc/ssh/sshd_config. All four systems read additional configuration from /etc/ssh/sshd_config.d/, and your own file there survives package updates without any prompts. First look at what is already there:

ls -l /etc/ssh/sshd_config.d/
grep -n '^Include' /etc/ssh/sshd_config

On Debian and Ubuntu the Include line sits at the very top by default, usually on line 12. That matters more than it looks: for most directives the first value found wins, not the last. Because the include is at the top, the files in that directory beat everything that follows further down in sshd_config. If it has been moved to the end of the file, the order flips and your file has no effect.

Port is the exception, and that exception is exactly what makes the risk-free change possible: several Port lines do not replace each other, they add up. sshd then listens on every port listed. The flip side matters just as much: the default of 22 only applies as long as no Port line exists at all. The moment you write one, 22 is gone unless you list it explicitly.

tee /etc/ssh/sshd_config.d/20-port.conf >/dev/null <<'EOF'
Port 22
Port 2222
EOF
chmod 644 /etc/ssh/sshd_config.d/20-port.conf

Check: first the syntax, then the effective overall configuration with the include files resolved:

sshd -t
sshd -T | grep -E '^(port|listenaddress) '

Two lines are expected, port 22 and port 2222. If a listenaddress line shows up as well, that is the second place ports can live: ListenAddress may name an address together with a port and then narrows down what is listened on. An overlooked line such as ListenAddress 127.0.0.1 explains most of the cases where the port is configured correctly but nobody gets through from outside.

If sshd -t reports Missing privilege separation directory: /run/sshd instead, the service has not run since boot, and mkdir -p /run/sshd clears that up.

Step 4: Socket activation, where the port is not in sshd_config

This is where the distributions part ways, and where most accidents happen. Ubuntu has relied on socket activation since 22.10: it is not sshd that holds the port, it is systemd. It listens on behalf of sshd and only starts an sshd process once a connection comes in. The port then lives in the ssh.socket unit under ListenStream, and a Port line in the sshd configuration can end up doing nothing.

Do not guess, ask the system:

systemctl is-enabled ssh.socket ssh.service
SystemActive by defaultWhere the port comes from
Debian 13 (trixie)ssh.service/etc/ssh/sshd_config.d/
Debian 12 (bookworm)ssh.service/etc/ssh/sshd_config.d/
Ubuntu 24.04 LTSssh.socketListenStream in the socket unit
Ubuntu 22.04 LTSssh.service/etc/ssh/sshd_config.d/

The table describes the shipping state, not necessarily your server: images from different sources differ, and an upgraded system keeps the setting it had before. If ssh.socket is active, systemctl cat shows the unit together with every drop-in file and its path, including the ones generated automatically:

systemctl cat ssh.socket

You add the new port as a drop-in file of your own. The first, empty ListenStream= line clears the existing values, after which only yours count. Without that reset line the defaults would simply be added on top:

mkdir -p /etc/systemd/system/ssh.socket.d
tee /etc/systemd/system/ssh.socket.d/override.conf >/dev/null <<'EOF'
[Socket]
ListenStream=
ListenStream=0.0.0.0:22
ListenStream=[::]:22
ListenStream=0.0.0.0:2222
ListenStream=[::]:2222
EOF
systemctl daemon-reload

Here too both ports sit side by side, for the same reason as in step 3. A file under /etc/systemd/system/ takes precedence over anything the system generates itself.

Check: systemctl cat ssh.socket now lists your file as a block of its own.

If ssh.service and ssh.socket are active at the same time, they fight over the same port. That shows up as fatal: Cannot bind any address. or ssh.socket: Socket service ssh.service already active, refusing. Then decide on one of the two modes, our guide to hardening SSH covers the background.

Step 5: Restarting and verifying on the socket

One command that is correct on all four systems, because try-restart only restarts what is actually running and leaves the other unit untouched:

sshd -t && systemctl try-restart ssh.socket ssh.service

Do not use reload: on systems with socket activation it answers with fatal: Cannot bind any address., and the service has let go of its port afterwards. And no lone systemctl restart ssh.socket either: on Debian it fails, on Ubuntu 22.04 it quietly switches the host over to socket mode. Existing sessions survive the restart in every case.

The check, and it is the only one that really counts:

ss -tlnp | grep -E ':(22|2222) '

Four lines are expected: 0.0.0.0:22, [::]:22, 0.0.0.0:2222 and [::]:2222. If the IPv6 lines are missing, you will not reach the server over IPv6.

One quirk that leads to needless panic: with socket activation the last column may read systemd instead of sshd, because systemd holds the listening socket as long as no sshd process is running. So filter by port number, not by process name, otherwise you will take a working service for a dead one.

Step 6: The third session, the actual test

Now, and not one step earlier, open a new terminal. The two older sessions stay open.

ssh -p 2222 root@203.0.113.10

If that does not work, ssh -vv -p 2222 root@203.0.113.10 shows at which point it fails.

One prompt along the way is normal: SSH asks again whether the host key is genuine, even though nothing has changed on the server. Entries in known_hosts are bound to the port and are stored for non-standard ports in the form [203.0.113.10]:2222. Look it up and remove it if in doubt:

ssh-keygen -F '[203.0.113.10]:2222'
ssh-keygen -R '[203.0.113.10]:2222'

Now is the best moment to check whether the change survives a reboot. With the rule for port 22 still in place, that is risk-free.

Step 7: Closing port 22 and updating the tools

Only once step 6 has worked do you remove the old port. Just one line is left in 20-port.conf:

tee /etc/ssh/sshd_config.d/20-port.conf >/dev/null <<'EOF'
Port 2222
EOF
sshd -t && systemctl try-restart ssh.socket ssh.service

With socket activation, also delete the two lines with port 22 from /etc/systemd/system/ssh.socket.d/override.conf and follow up with systemctl daemon-reload. Then the firewall:

ufw delete allow 22/tcp
ufw status verbose

Check: ss -tlnp | grep -E ':(22|2222) ' now shows the new port only, again in both address families.

That leaves the part that lingers the longest: every tool that talks to the server. The most convenient way is to settle it once and for all in ~/.ssh/config:

Host my-server
    HostName 203.0.113.10
    Port 2222
    User kernel

Wherever you give the address directly, you need the port, and the notation differs from tool to tool. This is the classic stumbling block:

ToolPort optionExample
ssh-p (lowercase)ssh -p 2222 kernel@203.0.113.10
scp-P (uppercase)scp -P 2222 file.tar.gz kernel@203.0.113.10:/tmp/
sftp-P (uppercase)sftp -P 2222 kernel@203.0.113.10
ssh-copy-id-p (lowercase)ssh-copy-id -p 2222 kernel@203.0.113.10
rsyncthrough -ersync -av -e 'ssh -p 2222' ./data/ kernel@203.0.113.10:/srv/
gitin the URLssh://git@203.0.113.10:2222/srv/repo.git

rsync --port, by the way, applies to the rsync daemon and not to SSH, so it has no effect here. In Ansible the port goes into the inventory as ansible_port.

SELinux on RHEL systems

On AlmaLinux, Rocky Linux and RHEL, SELinux is in enforcing mode by default, and the policy allows sshd only those ports that are labeled as ssh_port_t. By default that is port 22 alone. Without this extra step the service does not start, and the message misleadingly looks like a permissions problem:

error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.

First a look at the state and the existing labels:

getenforce
semanage port -l | grep ssh_port_t

If semanage is missing, it lives in the policycoreutils-python-utils package:

dnf install -y policycoreutils-python-utils

Add the new port, and do so before you restart sshd:

semanage port -a -t ssh_port_t -p tcp 2222

If the port is already assigned to another type, -a fails with a note that it is already defined. In that case change the assignment with -m instead of adding a new one.

Check: semanage port -l | grep ssh_port_t now lists both ports. If something still jams after that, the denied accesses show up here:

ausearch -m avc -ts recent

Debian and Ubuntu have no comparable restriction in a default installation, so this section does not apply there.

Common errors and how to fix them

ssh: connect to host 203.0.113.10 port 2222: Connection refused: the packets arrive, but nobody is listening. The service was not restarted, the configuration did not take effect, or a ListenAddress line binds it to a different address.

ssh: connect to host 203.0.113.10 port 2222: Connection timed out: the packets do not arrive at all, almost always a packet filter on the server or in your own network. The difference from the previous message is the single most valuable piece of diagnostic information: refused means the service is missing, timed out means a filter.

error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.: on RHEL systems this is SELinux, see above. On Debian and Ubuntu the message practically only appears when sshd does not run as root and is supposed to bind a port below 1024.

error: Bind to port 2222 on 0.0.0.0 failed: Address already in use.: another process holds the port, ss -tlnp | grep ':2222 ' names it. If the error only appears sporadically after a reboot, the port sits in the range of the dynamic source ports, see step 1.

fatal: Cannot bind any address.: none of the requested addresses could be bound. On Debian and Ubuntu this almost always means that ssh.socket already holds the port and you are trying to start ssh.service on it as well.

Job for ssh.service failed because the control process exited with error code.: the cause is in the journal, not in this line. journalctl -u ssh -n 50 --no-pager shows it, regardless of the OpenSSH version.

/etc/ssh/sshd_config.d/20-port.conf line 1: Bad configuration option: prot: a typo. sshd -t names the file and the line number, which is exactly why the command belongs before every restart.

sshd: no hostkeys available -- exiting.: the host keys are missing or not readable, ssh-keygen -A creates the missing ones. sshd -t and sshd -T need read access to them and therefore have to run as root.

ssh: connect to host 203.0.113.10 port 22: Connection refused after a successful switch: your client keeps trying the default port. Add an entry to ~/.ssh/config or pass -p.

If you are locked out after all

  1. Do not reboot. A reboot restores the firewall rules and the service configuration, so the server comes back just as closed as it was.
  2. Open the VNC console in the customer panel and log in as root. The console login does not run over SSH and is unaffected by your change.
  3. Identify the class of fault. ss -tlnp answers that right away: if the port is there, it is a filter problem. If it is not there, it is a service problem, and journalctl -u ssh -n 50 --no-pager says why.
  4. Roll back instead of repairing. rm -f /etc/ssh/sshd_config.d/20-port.conf and, if you created it, rm -f /etc/systemd/system/ssh.socket.d/override.conf, then systemctl daemon-reload and systemctl try-restart ssh.socket ssh.service.
  5. If the firewall was the cause, ufw disable helps, followed by a clean rebuild with the correct port rule.

Summary

  • Firewall first, sshd afterwards. The old port stays open until the new one is proven to work.
  • Several Port lines add up, which is what makes the transition risk-free. The default of 22 falls away as soon as any Port line exists.
  • With socket activation, the default on Ubuntu 24.04, the port lives in ListenStream.
  • What counts is never the configuration file, it is ss -tlnp, filtered by port number.
  • The rescue route is the VNC console in the customer panel, so keep the root password at hand.

If you are only just putting the server into service, our checklist for new root servers fits this step into the rest of the basic setup. And once more for perspective: a different port makes your logs readable. What secures access is key-based login and password login switched off. If you only put one of the two into practice, take the second.

Frequently asked questions

Does a different SSH port make the server more secure?
No. A full port scan across all 65,535 ports finds the service anyway, and the version banner that OpenSSH sends when a connection is opened gives it away immediately, on port 51022 just as much as on port 22. The real benefit lies elsewhere: the vast majority of what hits port 22 is untargeted mass scanning, and those tools try port 22 and nothing else. After the switch that class of traffic disappears from the journal, which is what makes a single targeted login attempt stand out in the first place. What secures access is key-based login, password login switched off and a tight packet filter.
Which port should I pick?
Not 2222, because it is the most common fallback choice and mass scanners have long been probing it as well. Stay below 32768: that is usually where the range begins from which the kernel hands out source ports for outgoing connections, as documented in /proc/sys/net/ipv4/ip_local_port_range. A port from that range can be taken temporarily, and after a reboot the bind then fails sporadically with Address already in use while the server comes up without SSH. Only root may bind ports below 1024, so no unprivileged user can hook in there with a fake SSH service and record credentials. Check in advance with ss -tlnp and with grep -w 2222 /etc/services that the port is free and not reserved for another service.
How do I change the port without locking myself out?
By having the server listen on both ports while you work. The order is: open the new port in the firewall and leave the old one open, then make sshd listen on both ports, restart, log in over the new port with a third, freshly opened session, and only then remove port 22. What makes this possible is a peculiarity of OpenSSH: several Port lines do not replace each other, they add up. The flip side matters just as much: the default of 22 only applies as long as no Port line exists at all. The moment you write one, you have to list 22 explicitly, otherwise it is gone.
My Port line has no effect, sshd still listens on 22 only. Why is that?
Two causes come into question. First socket activation: Ubuntu has relied on it since 22.10, and it is active by default on Ubuntu 24.04. In that case it is not sshd that holds the port but systemd, and what counts is ListenStream in the ssh.socket unit, not the Port line in the sshd configuration. Which mode is running on your server is answered by systemctl is-enabled ssh.socket ssh.service. Second the include order: for most directives the first value found wins, and on Debian and Ubuntu the include line for /etc/ssh/sshd_config.d/ sits at the very top by default. If it has been moved to the end of the file, your file has no effect. What actually applies is shown by sshd -T, filtered on the port and listenaddress lines. If a listenaddress line turns up there, it narrows down what is listened on as well.
What is the difference between Connection refused and Connection timed out?
That is the single most valuable piece of diagnostic information. ssh: connect to host 203.0.113.10 port 2222: Connection refused means the packets arrive but nobody is listening: the service was not restarted, the configuration did not take effect, or a ListenAddress line binds it to a different address. ssh: connect to host 203.0.113.10 port 2222: Connection timed out means the packets do not arrive at all, and that is almost always a packet filter on the server or in your own network. In short: refused means the service is missing, timed out means a filter.
On AlmaLinux and Rocky Linux sshd does not start: error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.
That is SELinux and not a permissions problem, even though the message looks like one. On RHEL systems SELinux is in enforcing mode by default, and the policy allows sshd only those ports that are labeled as ssh_port_t. By default that is port 22 alone. Label the new port before you restart the service: semanage port -a -t ssh_port_t -p tcp 2222. If semanage is missing, it lives in the policycoreutils-python-utils package. If the port is already assigned to another type, -a fails with a note that it is already defined, in which case you change the assignment with -m. Denied accesses are shown by ausearch -m avc -ts recent. Debian and Ubuntu have no comparable restriction in a default installation.
What else do I have to adjust after the switch?
First fail2ban: if a ban is in effect there, it otherwise still blocks port 22 only, while the attempts on the new port go through. Add the port in the sshd section of /etc/fail2ban/jail.local. Detection keeps working anyway, because fail2ban reads the login attempts from the journal, only the blocking depends on this line. Then every tool that talks to the server: the most convenient way is to settle it once and for all with a host entry including the port in ~/.ssh/config. Wherever you give the address directly, the notation differs from tool to tool: ssh and ssh-copy-id take a lowercase -p, scp and sftp an uppercase -P, rsync gets the port through the -e option, git in the URL, Ansible as ansible_port in the inventory. rsync --port, by contrast, applies to the rsync daemon and has no effect here. That SSH asks again whether the host key is genuine on the first connection is normal: entries in known_hosts are bound to the port and are stored in the form [203.0.113.10]:2222.
I have locked myself out. What now?
Do not reboot the server, it restores the firewall rules and the service configuration and comes back just as closed as it was. Open the VNC console in your customer panel instead and log in as root, because the console login does not run over SSH and is unaffected by your change. The class of fault is answered by ss -tlnp right away: if the port is there, it is a filter problem, if it is not there, a service problem, and journalctl -u ssh -n 50 --no-pager says why. Then roll back instead of repairing: delete /etc/ssh/sshd_config.d/20-port.conf and, if you created it, /etc/systemd/system/ssh.socket.d/override.conf, then systemctl daemon-reload and systemctl try-restart ssh.socket ssh.service. If the firewall was the cause, ufw disable helps, followed by a clean rebuild with the correct port rule.

SSH OpenSSH sshd Server security Debian Ubuntu systemd SELinux