Percio Castelo.
PT Home
How to securely change the default SSH port on Linux
← Back to blog

How to securely change the default SSH port on Linux

6/14/2026 · 7 min · Cybersecurity · 333 views

The Secure Shell (SSH) protocol is the primary gateway for remote administration of Linux servers. By default, the SSH daemon (sshd) listens on TCP port 22. While this standardization simplifies initial connectivity, it makes servers constant targets for automated port scans and brutal internet-wide brute-force attacks by malicious actors.


1. Network hardening and SSH fundamentals#

Changing the default SSH port is not a complete security solution on its own (security through obscurity), but it is a critical component of a robust defense-in-depth strategy:

  1. Mitigation of Automated Attacks: The vast majority of automated scanning bots only scan default port 22. Changing the port cuts down nearly 99% of invalid login attempt logs.
  2. Reduction of Disk I/O Overhead: Minimizing brute-force connections prevents excessive logging in auth.log (Debian/Ubuntu) or secure (RHEL/Rocky Linux).
  3. Incident Response (IR) Focus: With auth logs cleaned of automated noise on port 22, failed login alerts on the custom port can be treated immediately as high-priority, targeted security incidents.

2. Step-by-step SSH daemon port configuration#

Below is the correct sequence of execution for all SRE operations required to change the port safely.

Step 0: Planning and selecting a safe port#

When choosing a new custom port for SSH, keep the following SRE and networking rules in mind:

  • Ports Above 1024: Use ports within the non-privileged range (1024 to 65535).
  • Avoid Standard Ports: Consult services to ensure your chosen port is not reserved for other well-known services.
  • Avoid Ephemeral Port Ranges: Do not use ports above 30000, as they may conflict with ephemeral client sockets used for outgoing connections. The recommended range is between 10000 and 20000 (e.g., 22022).

Before proceeding, verify that the chosen port is not currently in use by any other application running on the server:

# Check listening sockets using the modern ss utility (recommended)
sudo ss -tulpn | grep :22022

If this command yields no output, the port is available for use.

Step 1: Mandatory preventive backup#

Before changing any configuration file on the system, create complete backup copies of all relevant configuration settings:

# Backup the main configuration file with a date-timestamp suffix
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d-%H%M%S)

# Backup the sub-configuration include directory (.d)
sudo cp -r /etc/ssh/sshd_config.d/ /etc/ssh/sshd_config.d.backup.$(date +%Y%m%d-%H%M%S) 2>/dev/null || true

Step 2: Configuring mandatory access control (MAC)#

If your system uses a security mechanism like SELinux, the kernel will block the SSH daemon from binding to a non-standard port unless you explicitly register it.

SELinux (RHEL, CentOS, Rocky Linux, AlmaLinux)#

  1. Verify the current SELinux status:
   getenforce

If the output is Enforcing, you must whitelist the new port.

  1. Add the custom port to the SSH network port object class (ssh_port_t):
   sudo semanage port -a -t ssh_port_t -p tcp 22022

For temporary troubleshooting, you can switch SELinux to permissive mode (sudo setenforce 0), verify that the daemon restarts, and then switch it back enforcing after validating and saving the permanent port rule (sudo setenforce 1).

AppArmor (Ubuntu, Debian)#

  1. Verify if there is an active AppArmor profile for the SSH daemon:
   sudo aa-status | grep sshd

If the profile exists and is in enforcing mode, check that the local rules at usr.sbin.sshd do not block alternative ports, and reload the profile if any rules are changed.

Step 3: Allowing the port in the server firewall (before modifying SSH)#

You must configure the local firewall to allow incoming traffic on the custom port before restarting the daemon.

Option a: configuration via UFW (uncomplicated firewall)#

  1. Allow incoming TCP traffic on the new port:
   sudo ufw allow 22022/tcp
  1. Reload UFW to apply changes:
   sudo ufw reload
  1. Check status to confirm the rule is active:
   sudo ufw status verbose

Option b: configuration via raw iptables#

  1. Add an INPUT chain rule to accept TCP traffic targeting your custom port:
   sudo iptables -A INPUT -p tcp --dport 22022 -j ACCEPT
  1. Save rules permanently so they are loaded during system boot:
   # Recommended option using netfilter-persistent utility
   sudo netfilter-persistent save
   
   # Or manually dump the ruleset to the system configuration files
   sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
   sudo sh -c 'ip6tables-save > /etc/iptables/rules.v6'

Step 4: Modifying sshd_config#

The SSH daemon settings are governed by sshd_config.

Openssh directive accumulation rules#

The Port directive in OpenSSH is cumulative. Unlike options such as PermitRootLogin (where the first matching line wins), declaring multiple Port lines will configure the daemon to listen on all defined ports simultaneously.

You must also audit sshd_config.d to ensure no include files override the configuration or force the default port 22:

grep -r -i "^Port" /etc/ssh/sshd_config.d/ 2>/dev/null

If you find any active Port 22 line in the include configs, comment it out or delete it.

Open the main sshd_config file:

sudo nano /etc/ssh/sshd_config

Locate the line #Port 22 (or Port 22 if it was uncommented), remove the # symbol, and set the value to your custom port:

Port 22022

Save and close the file (Ctrl+O, Enter, Ctrl+X). Alternatively, automate the replacement via sed:

sudo sed -i 's/#Port 22/Port 22022/g' /etc/ssh/sshd_config

Verifying the effective configuration#

Before restarting the service, run the SSH configuration syntax check with the -T flag. This parses the main file and its includes, letting you verify the actual config that will be applied by the daemon:

sudo sshd -T | grep -i "port"

Make sure the output displays only port 22022. If port 22 is still listed, audit the files under sshd_config.d again.

Step 5: Restarting the SSH daemon#

Once the configuration is validated and firewall rules are active, restart the SSH service:

# Restart the daemon via systemctl (systemd standard)
sudo systemctl restart sshd

Step 6: Testing the connection in a parallel session#

  1. Open a new terminal window on your local client machine.
  2. Establish a new session specifying the custom port with the -p flag:
   ssh user@SERVER_IP -p 22022

3. Additional SSH hardening and cryptographic keys#

Merely changing the port is not enough to secure a server. For enterprise-grade security, apply these additional hardening directives to the SSH daemon.

Daemon hardening configuration#

Edit /etc/ssh/sshd_config and add or adjust the following options:

# Disable root logins directly (requires sudo elevation)
PermitRootLogin no

# Disable legacy password-based authentication
PasswordAuthentication no

# Limit the maximum number of failed authentication attempts per connection
MaxAuthTries 3

# Define the grace time allowed to complete authentication (in seconds)
LoginGraceTime 60

# Disable X11 Forwarding (prevents graphical UI vulnerabilities)
X11Forwarding no

# Disable generic TCP port forwarding (unless required)
AllowTcpForwarding no

# Increase verbosity of authentication logs for auditing
LogLevel VERBOSE

Restart the SSH service (sudo systemctl restart sshd) to apply these rules.

Key-based cryptographic authentication (SSH keys)#

Using cryptographic key pairs removes the risks associated with weak user passwords.

  1. On your local client machine, generate a secure Ed25519 key pair:
   ssh-keygen -t ed25519 -C "[email protected]"
  1. Copy the public key to the remote server, specifying the custom port:
   ssh-copy-id -p 22022 user@SERVER_IP
  1. Test logging in without a password, then disable passwords by setting PasswordAuthentication no in /etc/ssh/sshd_config.

4. Mitigating scans with Fail2ban#

To shield your custom port from targeted scans, install fail2ban to automatically block hosts with consecutive authentication failures.

  1. Install the utility:
   sudo apt-get update && sudo apt-get install -y fail2ban
  1. Create a local configuration copy to prevent updates from overwriting custom rules:
   sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
  1. Edit /etc/fail2ban/jail.local and configure the SSH jail to listen on the custom port:
   [sshd]
   enabled = true
   port = 22022
   filter = sshd
   logpath = /var/log/auth.log
   maxretry = 3
   bantime = 3600

(Note: For RHEL/Rocky Linux, set logpath to secure).

  1. Restart the fail2ban service:
   sudo systemctl restart fail2ban
   sudo systemctl enable fail2ban

5. Auditing authentication logs#

To check active SSH connections and ensure login events are properly logged on the custom port, monitor log files in real-time during login tests:

# Debian / Ubuntu
sudo tail -f /var/log/auth.log

# RHEL / Rocky Linux / AlmaLinux
sudo tail -f /var/log/secure

Look for lines recording access details: Accepted publickey for user from CLIENT_IP port CLIENT_PORT ssh2: ED25519...


6. Emergency rollback procedure#

If the SSH daemon fails to start, if you are blocked from connecting, or if you need to reverse changes quickly, execute this rollback process (via console access, virtual KVM/IPMI/iLO):

  1. Log in to the server via the cloud provider's console or IPMI interface.
  2. Restore the original configuration file from the backup:
   sudo cp /etc/ssh/sshd_config.backup.* /etc/ssh/sshd_config
  1. Remove or comment out any custom configuration files you created in include directories.
  2. Restart the SSH service to revert to default port 22:
   sudo systemctl restart sshd
  1. Confirm that the service is listening on port 22:
   sudo ss -tulpn | grep ssh
  1. Reconnect from your local machine using the default port 22.

7. Automated and safe SSH port migration script#

Below is a production-grade Bash script to automate the SSH port migration. It follows the exact safe sequence of operations: creating backups, configuring firewalls, whitelisting ports in SELinux, and running configuration parsing tests before restarting the daemon. You can expand and review the entire code inline without leaving the page, or access the maintained version on GitHub Gist:

migrate_ssh_port.sh — Safely change the SSH port and prevent lockouts
GistExpand code
#!/bin/bash
# migrate_ssh_port.sh - Safely change the SSH port and prevent lockouts
# ⚠️ Run this script with root privileges (sudo)

set -e

NEW_PORT=22022
BACKUP="/etc/ssh/sshd_config.backup.$(date +%Y%m%d-%H%M%S)"

# Terminal output colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }

# 1. Verify root execution permissions
[[ $EUID -ne 0 ]] && error "This script must be run as root (or via sudo)"

# 2. Check if the target port is already in use
ss -tulpn | grep -q ":${NEW_PORT} " && error "Port ${NEW_PORT} is already in use by another service"

# 3. Create a preventive backup copy of sshd_config
log "Creating backup of current configuration at: ${BACKUP}"
cp /etc/ssh/sshd_config "$BACKUP"

# 4. Configure SELinux policies if applicable
if command -v getenforce &>/dev/null && [[ $(getenforce) == "Enforcing" ]]; then
    log "SELinux is active (Enforcing). Adding new SSH port to security policies..."
    semanage port -a -t ssh_port_t -p tcp "$NEW_PORT" 2>/dev/null || \
    warn "Port may already be registered or semanage failed. Verify manually."
fi

# 5. Configure the local firewall (BEFORE updating SSH)
log "Configuring local firewall rules..."
if command -v ufw &>/dev/null && [[ $(sudo ufw status | grep -c "Status: active") -ge 1 ]]; then
    log "UFW detected and active. Allowing TCP port ${NEW_PORT}..."
    ufw allow "${NEW_PORT}/tcp"
    ufw reload
elif command -v iptables &>/dev/null; then
    log "Iptables detected. Adding INPUT chain rule for TCP port ${NEW_PORT}..."
    iptables -A INPUT -p tcp --dport "$NEW_PORT" -j ACCEPT
    
    # Save rules permanently
    if command -v netfilter-persistent &>/dev/null; then
        netfilter-persistent save
    else
        iptables-save > /etc/iptables/rules.v4 2>/dev/null || warn "Failed to persist rules via iptables-save"
    fi
else
    warn "No standard active firewall manager (UFW/iptables) detected. Ensure port ${NEW_PORT} is allowed at the edge."
fi

# 6. Modify the main SSH configuration file
log "Updating the Port configuration in /etc/ssh/sshd_config..."
sed -i "s/#Port 22/Port ${NEW_PORT}/g" /etc/ssh/sshd_config

# 7. Check for include files conflicts
if grep -r -i "^Port 22" /etc/ssh/sshd_config.d/ &>/dev/null; then
    warn "Active 'Port 22' lines found inside /etc/ssh/sshd_config.d/. Make sure to remove them to prevent sshd from listening on port 22."
fi

# 8. Run configuration test on parsed settings
log "Validating SSH configuration syntax..."
sshd -T | grep -qi "port ${NEW_PORT}" || error "OpenSSH configuration validation failed. Rollback with: cp ${BACKUP} /etc/ssh/sshd_config"

# 9. Restart the SSH daemon
log "Restarting the SSH service..."
systemctl restart sshd

# 10. Verify final daemon status
if systemctl is-active --quiet sshd; then
    log "✅ SSH successfully configured and listening on port ${NEW_PORT}!"
    log "📋 Active socket: $(ss -tulpn | grep sshd | awk '{print $4}' | tr '\n' ' ')"
    log ""
    log "⚠️  KEEP THIS TERMINAL SESSION OPEN AND TEST CONNECTION IN A PARALLEL WINDOW:"
    log "   ssh user@SERVER_IP -p ${NEW_PORT}"
else
    error "❌ Failed to start the SSH daemon. Perform a manual rollback using: cp ${BACKUP} /etc/ssh/sshd_config"
fi

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

333 reads
Creative Commons Attribution-NonCommercial

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments