Change the SSH port with SELinux and firewalld
Moving sshd to a new port on Rocky Linux or AlmaLinux needs firewalld, an SELinux port label and sshd_config. Do it in the order that keeps you logged in.
Why changing the SSH port needs three steps here
To change the SSH port on Rocky Linux, AlmaLinux, CentOS Stream or Fedora, one edit is not enough. Three separate systems each get a say in whether a connection on the new port works. firewalld decides whether the packet reaches the machine. SELinux decides whether sshd is allowed to bind that port number at all. sshd_config decides which port the daemon asks for. Miss the SELinux step and the daemon refuses to start. Miss the firewalld step and it starts, listens, and nobody can reach it.
On Ubuntu the same job is one edit and a restart, because Ubuntu uses AppArmor rather than SELinux and ships no profile that restricts which ports sshd may bind. If ufw is running there, you add one rule. That is the whole difference. The RHEL family ships firewalld running and SELinux enforcing on a fresh install, and both of them care about port numbers.
Do the work in this order and your current session stays alive through every step:
- Open the new port in firewalld, leaving port 22 open for now.
- Add the SELinux label for the new port with
semanage. - Set the port in the sshd configuration.
- Restart
sshd, then log in on the new port from a second terminal before closing the first.
Find your provider's web console (VNC or serial) before you start, and check that you can log in through it. That console is your way back in if the change goes wrong. A port change is one of the most common reasons a tenant locks themselves out of a server they have just paid for.
First, get semanage installed
semanage is the tool that edits SELinux policy settings, and a minimal Rocky Linux or AlmaLinux install does not include it. It lives in policycoreutils-python-utils.
sudo dnf install -y policycoreutils-python-utilsRunning the command before installing that package gives sudo: semanage: command not found, and that is the point where many readers decide SELinux is not installed and skip the step. SELinux is installed. Only the management tool is missing. If the dnf syntax is new to you, the dnf and apt command equivalents map it back to what you already know.
Pick a port and check that nothing owns it
Any free TCP port from 1024 to 65535 will do. Two checks before you commit to one:
sudo ss -tlnp | grep -w 2222
sudo semanage port -l | grep -w 2222The first shows whether a process is already listening on that number. The second shows whether SELinux policy already assigns it to some other service type. A free port returns nothing from either. If policy already claims it, the semanage port -a in step 2 fails with ValueError: Port tcp/2222 already defined, and the fix is to choose a different number.
2222 is used as the example throughout this guide. It is also the first port a scanner tries after 22, so pick something less obvious on a real server.
Step 1: open the port in firewalld
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports--permanent writes the rule into the zone file on disk and does not touch the running firewall. --reload loads the on-disk configuration into the running firewall. Skip the reload and the rule exists but does nothing until firewalld next restarts, which is one of the most common ways this whole procedure appears to fail for no reason.
Leave the ssh service entry alone for now. That entry is what keeps port 22 open, and it is your fallback while you test.
Check your provider's control panel as well. Many hosts run a network firewall in front of the VPS, outside the operating system, so a port you opened in firewalld can still be dropped upstream. The firewalld basics guide for a VPS covers zones and the runtime versus permanent split if that model is new to you.
Step 2: label the port for SELinux
sudo semanage port -a -t ssh_port_t -p tcp 2222
sudo semanage port -l | grep ssh_port_t-a adds a new port assignment. -t ssh_port_t is the type that SSH ports carry. The second command lists everything ssh_port_t now covers, so you can confirm your number went in before you touch the daemon.
Why SELinux blocks the port at all
SELinux (security-enhanced Linux) gives every object on the system a label, and TCP port numbers are objects like any other. The SSH daemon runs confined in a domain called sshd_t. Policy lets sshd_t bind TCP ports labelled ssh_port_t, and out of the box the only port carrying that label is 22. Ask the daemon to bind 2222 and the kernel checks the label, finds whatever generic type policy assigned to that number, and refuses the name_bind permission on the socket.
That is why this failure does not look like a firewall problem. The kernel refuses before a listening socket ever exists, so sshd reports the error and exits. A firewall problem is the mirror image: the daemon is running and healthy, and packets are discarded on the way in.
getenforce tells you which mode the box is in. On Permissive a denial is recorded but not enforced, so the port change appears to work and then breaks on the day somebody runs setenforce 1 or the box reboots into enforcing mode. Label the port either way. The SELinux basics guide for a server goes into modes, contexts and booleans properly.
Step 3: set the port in the sshd configuration
On Rocky Linux 9 and 10, AlmaLinux 9 and 10, and current Fedora, /etc/ssh/sshd_config begins with an include line, so the tidy place for your change is a drop-in file. Package updates then never fight your edit.
grep -n '^Include' /etc/ssh/sshd_config
echo 'Port 2222' | sudo tee /etc/ssh/sshd_config.d/10-port.conf
sudo sshd -tIf grep finds no Include line, which is the case on Rocky Linux 8 and other older images, put Port 2222 into /etc/ssh/sshd_config directly instead. sshd -t parses the whole configuration, drop-ins included, and reports syntax errors. Fix anything it reports before you restart, because a configuration that fails to parse means a daemon that does not come back up.
Port may appear more than once, and sshd listens on every port listed. Keeping Port 22 alongside Port 2222 for the first day is a cheap safety net, as long as you remember to remove it.
Is your sshd started by a socket unit?
Some images start SSH through systemd socket activation instead of as a long-running service. Where that is set up, systemd owns the listening socket and hands connections to sshd, so the Port line in sshd_config is ignored entirely. Check before you restart anything:
systemctl is-enabled sshd.socketAn enabled answer means the port is set on the socket unit, not in sshd_config:
sudo systemctl edit sshd.socket[Socket]
ListenStream=
ListenStream=2222The bare ListenStream= is required. Values accumulate across drop-ins, so without an empty assignment to clear the list first, the socket keeps listening on 22 as well as 2222. Apply it with sudo systemctl daemon-reload and then sudo systemctl restart sshd.socket. If the unit is disabled or not present on your server, this section does not apply to you.
Step 4: restart, then test from a second terminal
sudo systemctl restart sshd
systemctl status sshd
sudo ss -tlnp | grep sshdKeep this terminal open. Do not log out of it. Open a second terminal on your own machine and connect on the new port:
ssh -p 2222 youruser@203.0.113.10Close the first session only after that second login has worked. If it does not work, you still hold a shell you can undo everything from. This one habit is the difference between a five minute change and an afternoon on the provider console.
Firewall drop or SELinux denial? How to tell them apart
From your laptop the two failures look almost identical. On the server they look nothing alike.
- If
systemctl status sshdshows the unit failed, the daemon never got its socket. That is a configuration error or an SELinux denial. - If the unit is active and
ss -tlnpshows sshd bound to the new port, the daemon is fine and the problem is on the network path: firewalld, the provider's separate firewall, or the address and port you dialled.
For the SELinux case, read the audit record instead of guessing:
sudo ausearch -m AVC -ts recent
sudo journalctl -u sshd -n 50 --no-pagerA name_bind denial on class tcp_socket names the process in comm="sshd", the port number in src=, and the label the port actually carries in tcontext=. That last field is the answer. Anything other than ssh_port_t means step 2 did not apply to the port you are using, usually a typo in the number or the wrong protocol. Install setroubleshoot-server if you would rather have sealert turn the record into a sentence.
The message sshd itself writes when the kernel refuses the bind reads like this:
error: Bind to port 2222 on 0.0.0.0 failed: Permission denied.Permission denied on a port above 1024, where no root privilege is needed to bind, is the SELinux signature. Address already in use in that same line is a different fault: another process is holding the port. From the client side, the difference between connection refused and connection timed out separates the two network cases, because a refusal means your packet reached the host and nothing was listening, while a timeout means nothing answered at all.
Close port 22 and update your clients
Once several logins on the new port have worked, take 22 away:
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-allLeave the SELinux label on port 22 alone. It comes from the base policy, and it grants nothing once the firewall stops letting packets in.
Then fix the clients, because every tool that assumed the default port now needs telling. Put it in ~/.ssh/config on your own machine once instead of typing -p forever:
Host myvps
HostName 203.0.113.10
Port 2222
User youruserscp, sftp, rsync and Ansible all read that file. Backup jobs, monitoring checks and cron scripts that hardcode port 22 do not, so hunt those down while the change is fresh in your head.
What moving the port does and does not buy you
It cuts log noise. Automated scanners hammer port 22 constantly, and moving off it removes most of those lines from the journal, which makes real events easier to see. It is not a security control. Any scanner sweeping the full port range finds your daemon and reads its version banner anyway. Treat the port change as housekeeping, and put the real protection into key-only authentication with password logins turned off, which the SSH hardening guide for a VPS walks through step by step.
Everything above works identically on both major RHEL rebuilds, since they are built from the same sources. See Rocky Linux and AlmaLinux compared if you are still choosing between them. Check which release you were actually given before following any older guide, with cat /etc/os-release. Guides written for Rocky Linux 8 still rank well and their semanage and firewall-cmd steps remain correct, but Rocky 8 has no sshd_config.d include line and no socket unit to think about, so the sshd half of those guides does not match a current box.
fail2ban has to be told about the new port
fail2ban is not in the base repositories. It comes from EPEL (extra packages for enterprise Linux):
sudo dnf install -y epel-release
sudo dnf install -y fail2ban fail2ban-firewalldThe fail2ban-firewalld subpackage makes fail2ban write its bans through firewalld, which is what you want on a box where firewalld owns the ruleset.
The stock sshd jail sets port = ssh, and that name resolves through /etc/services to 22. After your change the jail watches a port nobody is attacking, so it bans nobody while failed logins pile up on 2222. Set the port by number in /etc/fail2ban/jail.local:
[sshd]
enabled = true
port = 2222
backend = systemd
maxretry = 5
bantime = 3600backend = systemd reads failures from the journal rather than from /var/log/secure, which is the safer choice on a minimal install where rsyslog may not be present. Start it with sudo systemctl enable --now fail2ban and inspect the jail with sudo fail2ban-client status sshd. The jail syntax is the same one used in the fail2ban setup for SSH on Ubuntu 24.04. Only the package source and the ban action differ.
Patching matters more than the port
A server with a moved SSH port and four months of unapplied security updates is in worse shape than one sitting on port 22 that patches itself every night. Turn on unattended updates in the same session, while you are already root: automatic dnf updates on Rocky Linux and AlmaLinux covers the timer and the choice between downloading updates and applying them.
FAQ
Why does sshd fail to start after I change the port on Rocky Linux?
Almost always the missing SELinux port label. sshd runs confined in the sshd_t domain, and policy only lets it bind ports labelled ssh_port_t, which by default is port 22 alone. The kernel refuses the bind, so the daemon exits instead of listening, and journalctl -u sshd carries a line of the form error: Bind to port 2222 on 0.0.0.0 failed: Permission denied. Run sudo semanage port -a -t ssh_port_t -p tcp 2222 with your own port number, then restart the service. If semanage is not found, install policycoreutils-python-utils first.
Do I still need semanage if SELinux is in permissive mode?
Yes. In permissive mode the denial is recorded and the bind is allowed anyway, so the change looks like it worked. The label is still missing. The moment anyone runs setenforce 1, or the box boots with SELINUX=enforcing in /etc/selinux/config, sshd stops starting on that port. Adding the label costs one command and removes a failure that would otherwise turn up weeks later with no obvious cause.
The port is labelled and sshd is running, so why does my connection time out?
A running daemon means SELinux is satisfied, so the packet is being dropped on the way in. Check sudo firewall-cmd --list-ports for your port, and confirm you ran firewall-cmd --reload after the --permanent rule, because a permanent rule on its own never reaches the running firewall. Then check your host's control panel for a separate network firewall in front of the VPS. That is the second place people get blocked, and nothing inside the operating system will show it.
Which port should I use instead of 22?
Any free TCP port from 1024 to 65535. Avoid 2222 and 22222 on a real server, since scanners try them immediately after 22. Confirm the number is free with sudo ss -tlnp, confirm SELinux policy has not already claimed it with sudo semanage port -l, and skip anything assigned to a service you might install later. A high, unmemorable number is fine, because you will write it into ~/.ssh/config once and never type it again.