SSH Port Forwarding on RHEL, Rocky Linux, AlmaLinux, CentOS Stream and Oracle Linux

Configure local, remote and dynamic SSH port forwarding on RHEL 8–10, Rocky Linux, AlmaLinux, CentOS Stream and Oracle Linux.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Local, remote and dynamic SSH port forwarding on RHEL-based Linux

OpenSSH can carry TCP connections through an authenticated, encrypted SSH session. The SSH channel protects traffic between the SSH client and SSH server.

Each forward also has a separate connection to its final destination. For local and dynamic forwarding, that connection is opened from the SSH server side. For remote forwarding, it is opened from the SSH client side. That final hop may still use an unencrypted protocol such as HTTP unless the destination service provides TLS.

This guide shows how to create local (-L), remote (-R), and dynamic (-D) forwards on RHEL 8 through 10, Rocky Linux, AlmaLinux, CentOS Stream, and Oracle Linux using one consistent lab topology.

Tested on: Rocky Linux 10.2 with OpenSSH 9.9p1. The forwarding behavior was validated locally with separate loopback services; the examples use client1, bastion.example.com, and web.internal to make the client, SSH gateway, and destination roles easier to distinguish.

NOTE
This guide uses a normal non-root account named tunneluser. Root access is not required unless you bind a listener to a privileged port below 1024 or change the OpenSSH server configuration.

SSH port forwarding compatibility and lab topology

Distribution Versions SSH client package Procedure
RHEL 8, 9, 10 openssh-clients Same ssh -L, -R and -D syntax
Rocky Linux 8, 9, 10 openssh-clients Same procedure
AlmaLinux 8, 9, 10 openssh-clients Same procedure
CentOS Stream 9, 10 openssh-clients Same procedure
Oracle Linux 8, 9, 10 openssh-clients Same procedure

Confirm the client tools on the machine where you run ssh:

bash
ssh -V

Sample output:

output
OpenSSH_9.9p1, OpenSSL 3.5.5 27 Jan 2026
bash
rpm -q openssh-clients

Sample output:

output
openssh-clients-9.9p1-23.el10_2.rocky.0.1.x86_64

On a host that accepts SSH connections, confirm the server package and service:

bash
rpm -q openssh-server
bash
systemctl status sshd

Sample output:

output
● sshd.service - OpenSSH server daemon
     Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-07-11 16:21:02 IST; 1h 16min ago
   Main PID: 888 (sshd)

Use one topology and the same names throughout the examples:

Role Example
SSH client client1
SSH server or gateway bastion.example.com
Internal target web.internal:8080
SSH account tunneluser
Local forwarded port 5555
Remote forwarded port 9000
SOCKS port 1080

For local forwarding (-L), the target hostname is resolved and contacted from the SSH server side. For remote forwarding (-R), the target is contacted from the SSH client side.

Dynamic forwarding (-D) also creates destination connections from the SSH server side, but hostname resolution depends on the SOCKS client. Use a proxy URL such as socks5h:// when the destination hostname must be resolved through the remote network.


Local, remote and dynamic SSH forwarding compared

Forwarding type Listener is created on Destination is reached from Typical use
Local -L SSH client SSH server side Access an internal web or database service
Remote -R SSH server SSH client side Make a client-side service reachable from the SSH server
Dynamic -D SSH client SSH server side Create a local SOCKS proxy

OpenSSH defines the three forms with these templates:

bash
ssh -L [bind_address:]local_port:target_host:target_port user@ssh_server
ssh -R [bind_address:]remote_port:target_host:target_port user@ssh_server
ssh -D [bind_address:]socks_port user@ssh_server

Common options used in this guide:

Option Purpose
-N Do not run a remote command
-T Do not allocate a pseudo-terminal
-f Move SSH to the background after authentication
-v Show diagnostic output
-o ExitOnForwardFailure=yes Exit if SSH cannot create the requested listener
-g Allow other hosts to connect to a locally forwarded port

Test a new tunnel in the foreground first:

bash
ssh -N -T -v -o ExitOnForwardFailure=yes ...

Add -f only after the listener and destination path work.


Check SSH server forwarding permissions

Forwarding can be restricted on the SSH server. Read the effective runtime configuration:

bash
sshd -T | grep -Ei \
  'allowtcpforwarding|disableforwarding|gatewayports|permitopen|permitlisten'

Sample output:

output
gatewayports no
allowtcpforwarding yes
disableforwarding no
permitopen any
permitlisten any

When forwarding permissions are configured inside a Match block, include representative connection details so sshd applies the matching rules:

bash
sshd -T \
  -C user=tunneluser,addr=192.0.2.50,host=client1 \
  | grep -Ei \
  'allowtcpforwarding|disableforwarding|gatewayports|permitopen|permitlisten'

Replace the user, client address, and hostname with values from the connection you are testing. Without -C, the output might not reflect restrictions applied by Match User, Match Address, or similar conditional blocks. AllowTcpForwarding, GatewayPorts, PermitOpen, and PermitListen are all valid inside Match blocks.

Relevant sshd_config directives:

  • AllowTcpForwarding yes|no|local|remote — controls whether local or remote TCP forwarding is accepted
  • DisableForwarding yes|no — disables forwarding when set to yes
  • PermitOpen host:port — restricts destination addresses for forwarded connections
  • PermitListen host:port — restricts remote-forward listener addresses and ports
  • GatewayPorts no|yes|clientspecified — controls whether remote -R listeners can bind beyond loopback

There are two different GatewayPorts settings:

  • GatewayPorts in the SSH client configuration controls the default bind behavior of local and dynamic forwards created with -L and -D.
  • GatewayPorts in the SSH server configuration controls whether remote forwards created with -R may bind beyond loopback.

The -g client option allows other hosts to connect to locally created forwarded ports. OpenSSH documents client GatewayPorts for local forwarding and server GatewayPorts for remote listeners. The server setting accepts no, yes, or clientspecified; its default is no.

For a dedicated tunnel account, place restrictions in a Match User tunneluser block when you need tighter policy than the global defaults.

After editing the SSH server configuration, validate and reload:

bash
sshd -t

A valid configuration exits silently. On success:

bash
systemctl reload sshd

Keep an existing administrative SSH session open while reloading a remote sshd instance so a syntax error does not lock you out.


Configure local SSH port forwarding

Local forwarding sends traffic from a listener on the SSH client through the encrypted SSH session; the SSH server opens the connection to the target service.

text
client1:5555
    |
    | encrypted SSH connection
    v
bastion.example.com
    |
    | connection from bastion
    v
web.internal:8080

Local SSH port forwarding from client through bastion to internal service

Create the tunnel from client1:

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:5555:web.internal:8080 \
  [email protected]
  • 127.0.0.1:5555 is the listener on the SSH client.
  • web.internal:8080 is resolved and contacted from the bastion.
  • [email protected] establishes the encrypted SSH session.

In another terminal, confirm the listener:

bash
ss -lntp | grep ':5555'

Sample output:

output
LISTEN 0      128        127.0.0.1:5555       0.0.0.0:*    users:(("ssh",pid=14018,fd=4))

Send HTTP traffic through the tunnel:

bash
curl http://127.0.0.1:5555

The curl command should return the same response you would see when contacting web.internal:8080 from the bastion.

When the target service runs on the SSH server itself, point the remote side at loopback on the bastion:

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:5555:127.0.0.1:8080 \
  [email protected]

Only the target hostname changes; the forwarding mechanics stay the same.

Allow another host to use the local forward

Binding to a specific client-side address is usually clearer than opening the forward to every interface. Confirm that the bind address exists on the SSH client:

bash
ip address show

Then bind the forward to that address:

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -L 192.0.2.10:5555:web.internal:8080 \
  [email protected]

Replace 192.0.2.10 with an address assigned to the client host. OpenSSH cannot create the listener when the requested address is not locally available.

ssh -g can also allow other hosts to connect to a local forward, but that increases exposure. The client firewall must allow the chosen port when another host connects. Avoid an empty bind address unless listening on all interfaces is intentional.

Allowing other hosts to connect to a locally forwarded port


Configure remote SSH port forwarding

Remote forwarding is useful when a service runs on the SSH client and you want a listener on the SSH server that reaches back to the client.

Example scenario:

  • A web application runs on client1:3000.
  • public.example.com accepts SSH connections.
  • A loopback listener on public.example.com:9000 forwards to client1:3000.

Create the tunnel from client1:

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:9000:127.0.0.1:3000 \
  [email protected]
  • 127.0.0.1:9000 is created on the SSH server.
  • 127.0.0.1:3000 is contacted from the SSH client.
  • Keep the remote listener on loopback unless you deliberately need a wider bind.

Remote SSH port forwarding with listener on the SSH server

From public.example.com, verify the listener:

bash
ss -lntp | grep ':9000'

Sample output:

output
LISTEN 0      128        127.0.0.1:9000       0.0.0.0:*    users:(("sshd-session",pid=14298,fd=8))

Test the forwarded path:

bash
curl http://127.0.0.1:9000

For persistent public listeners, firewall rules, and GatewayPorts hardening, see the reverse SSH port forwarding guide.

Expose a remote forward to other hosts

This is a higher-risk configuration. The SSH server must permit a non-loopback bind, normally with:

text
GatewayPorts clientspecified

The client can then request:

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -R 0.0.0.0:9000:127.0.0.1:3000 \
  [email protected]

Also remember:

  • PermitListen can restrict the allowed listener.
  • firewalld must allow the remote port when other hosts should connect.
  • The exposed application should enforce its own authentication.
  • Binding a remote forward to all interfaces can publish an internal service publicly.

Configure dynamic SSH port forwarding

-D creates a SOCKS4/SOCKS5 proxy on the SSH client. Applications choose the destination for each connection, and the SSH server opens those destination connections from its side. This is appropriate for reaching services you are authorized to access through an approved SSH gateway—not for evading firewall policy.

Create a loopback-only SOCKS listener:

Dynamic SSH port forwarding with a local SOCKS proxy

bash
ssh -N -T \
  -o ExitOnForwardFailure=yes \
  -D 127.0.0.1:1080 \
  [email protected]

Confirm the listener:

bash
ss -lntp | grep ':1080'

Sample output:

output
LISTEN 0      128        127.0.0.1:1080       0.0.0.0:*    users:(("ssh",pid=14286,fd=4))

Test the proxy with curl:

bash
curl --proxy socks5h://127.0.0.1:1080 \
  http://web.internal:8080

socks5h asks the proxy side to resolve web.internal, which matters when that name exists only on the remote network. For browser and application SOCKS setup, see the SSH SOCKS proxy guide.


Save, background and close SSH tunnels safely

A reusable SSH client config entry keeps the forward settings in one place:

sshconfig
Host app-tunnel
    HostName bastion.example.com
    User tunneluser
    LocalForward 127.0.0.1:5555 web.internal:8080
    ExitOnForwardFailure yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

Start the tunnel in the foreground:

bash
ssh -N -T app-tunnel

For a controlled background connection, create a private control-socket directory:

bash
mkdir -p ~/.ssh/control
bash
chmod 700 ~/.ssh/control

Start a multiplexed master connection:

bash
ssh -M \
  -S ~/.ssh/control/app-tunnel \
  -f -N -T \
  -o ExitOnForwardFailure=yes \
  app-tunnel

Check whether the master is running:

bash
ssh -S ~/.ssh/control/app-tunnel -O check app-tunnel

Sample output:

output
Master running (pid=14480)

Close the tunnel cleanly:

bash
ssh -S ~/.ssh/control/app-tunnel -O exit app-tunnel

Sample output:

output
Exit request sent.

OpenSSH connection multiplexing can add or cancel forwarding channels on an existing master connection, so a brand-new SSH process is not always required for every forward change. For a reusable path across hosts, ControlPath ~/.ssh/control/%C avoids accidental socket-name collisions; OpenSSH recommends including %h, %p, and %r, or %C, so different destinations do not share one socket name.

Do not use kill -9 as the normal way to stop a tunnel. A normal kill, a control-socket exit, or a supervising systemd unit lets SSH shut down forwarding cleanly. For idle disconnects and keepalive tuning, see keep alive SSH sessions in Linux. A boot-persistent tunnel belongs in a supervised unit file—a topic for a separate article.

Optional verification with tcpdump: tcpdump -ni any port 22 shows the encrypted SSH leg on port 22, while the gateway-to-target connection appears with the destination protocol and port.


Troubleshoot SSH port forwarding

Bind address or port already in use

Check whether another process owns the listener port:

bash
ss -lntp | grep ':5555'

Choose a free port or stop the conflicting process. ExitOnForwardFailure=yes prevents SSH from continuing silently when it cannot create the listener.

open failed: administratively prohibited

Run the client with verbose output:

bash
ssh -vvv ...

On the SSH server, inspect forwarding policy:

bash
sshd -T | grep -Ei \
  'allowtcpforwarding|disableforwarding|permitopen|permitlisten'

When restrictions may live in a Match block, repeat the check with connection criteria:

bash
sshd -T \
  -C user=tunneluser,addr=192.0.2.50,host=client1 \
  | grep -Ei \
  'allowtcpforwarding|disableforwarding|permitopen|permitlisten'

Also check restrictions on the user's public key, such as no-port-forwarding, restrict, or permitopen=.

Tunnel opens but the destination refuses the connection

For local and dynamic forwarding, test the target from the SSH server:

bash
curl http://web.internal:8080

For remote forwarding, test the destination from the SSH client. ExitOnForwardFailure=yes confirms that the forwarding listener was created; it does not prove the final service is reachable.

Remote forwarding listens only on localhost

Check the -R bind address and the server's GatewayPorts value. The default server behavior keeps remote forwards on loopback.

Another host cannot reach a forwarded listener

Inspect the bind address:

bash
ss -lntp

Identify the active firewalld zone:

bash
firewall-cmd --get-active-zones

Inspect the applicable zone:

bash
firewall-cmd --zone=public --list-all

Check the forwarded listener port directly:

bash
firewall-cmd --zone=public --query-port=5555/tcp

Replace public and 5555 with the active zone and listener port on your host. For a public remote forward, use 9000/tcp instead.

Confirm the listener uses the expected address and that the zone allows the port when non-loopback access is required.

Tunnel disconnects after becoming idle

Add keepalive options to ~/.ssh/config:

sshconfig
ServerAliveInterval 30
ServerAliveCountMax 3

These detect an unresponsive SSH connection. For automatic restart after failure, supervise the tunnel with systemd rather than an unmanaged background process.

Symptom Likely cause Main check
Address already in use Listener port occupied ss -lntp
Administratively prohibited SSH server restriction AllowTcpForwarding, PermitOpen, PermitListen
Connection refused Final service unreachable Test from the side that creates the final connection
Remote listener is loopback-only GatewayPorts no sshd -T
Other systems cannot connect Bind address or firewalld ss, firewall-cmd
SOCKS hostname does not resolve DNS resolved on wrong side Use socks5h://

Conclusion

  • -L creates a listener on the SSH client.
  • -R creates a listener on the SSH server.
  • -D creates a local SOCKS proxy.
  • Use loopback bind addresses by default.
  • Add ExitOnForwardFailure=yes.
  • Verify listeners with ss and services with curl.
  • Restrict forwarding with AllowTcpForwarding, PermitOpen, and PermitListen.
  • Use control sockets or systemd instead of kill -9.

References


Frequently Asked Questions

1. Does SSH port forwarding encrypt the final connection to the target service?

SSH encrypts traffic between the SSH client and SSH server. The final destination connection is a separate network hop: the SSH server opens it for local and dynamic forwarding, while the SSH client opens it for remote forwarding. That hop may still use an unencrypted protocol such as HTTP unless the destination service provides TLS.

2. What is the difference between socks5 and socks5h?

With socks5h, the proxy side resolves the destination hostname. That matters when names such as web.internal resolve only on the remote network reached through the SSH gateway.

3. Why should I avoid kill -9 on an SSH tunnel process?

SIGKILL does not let SSH or the forwarded application shut down cleanly. Prefer ssh -O exit on a control socket, a normal kill signal, or a supervising systemd unit for long-running tunnels.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …