Detect, Restrict, and Respond to Unauthorized SSH Tunnels

Unauthorized forwarding is an access incident even when no shell login appears in the first alert. A compromised key can use -R, -L, -D, Unix-domain forwarding, or a long-lived carrier to move data and reach services. Detect the process, the SSH session, the listening socket, and the resulting egress together. Then contain the identity and the path without destroying evidence.

Do not kill first and investigate never. If there is active harm, contain the route promptly, preserve volatile and log evidence where policy permits, revoke the key, and record every action. Avoid broad firewall changes that break the known-good recovery path.

Prevent broad forwarding by default

Ordinary users should not receive forwarding merely because they can authenticate. Apply a global deny and create narrowly reviewed Match exceptions:

AllowTcpForwarding no
GatewayPorts no
DisableForwarding yes
X11Forwarding no
AllowAgentForwarding no

Match User tunnel
    AllowTcpForwarding remote
    PermitListen 127.0.0.1:2222

Check the installed OpenSSH manual because available directives and precedence depend on version. DisableForwarding yes is a convenient deny-all forwarding control and must not be combined accidentally with the exception. For keys that need a remote forward, use individual authorized_keys restrictions:

no-pty,no-agent-forwarding,no-X11-forwarding,no-user-rc,command="/bin/false",permitlisten="127.0.0.1:2222" ssh-ed25519 [approved-public-key-recorded-in-key-management] approved-tunnel

The bracketed public-key marker is not installable data. Do not put private keys in source control, units, environment variables, tickets, or shell history. Do not use restrict for this forwarding exception unless the required port-forwarding behavior is separately supported and tested; its no-port-forwarding component commonly conflicts with -R.

Find listeners and carrier processes

Run these commands with the privileges needed to see process ownership, and adapt service names to the distribution:

sudo ss -lntup
sudo ss -ntp
sudo lsof -nP -iTCP -sTCP:LISTEN
pgrep -a -f '(^|/)(ssh|sshd|autossh)( |$)'
ps -eo user,pid,ppid,etime,args --forest | grep -E '[s]sh|[a]utossh'

Pay special attention to an SSH client with -R, -L, -D, -W, or -w, an unexpected parent process, a listener owned by a service account, and an SSH socket bound outside loopback. A process list can expose usernames and paths; restrict and redact incident copies.

On systemd hosts, inventory persistence as well as the current process:

systemctl list-units --type=service --all | grep -Ei 'ssh|tunnel|auto'
systemctl list-timers --all
sudo find /etc/systemd /usr/lib/systemd /var/lib/systemd \
  -type f \( -name '*.service' -o -name '*.timer' \) -print 2>/dev/null
sudo crontab -l 2>/dev/null
sudo find /etc/cron* -maxdepth 2 -type f -print 2>/dev/null

Also inspect user services, authorized keys, shell startup files, configuration-management state, and container or orchestration manifests. Treat a newly installed supervisor or a key with an unexpected command= and permitlisten option as evidence requiring review.

Correlate authentication, forwarding, and egress

sudo journalctl _COMM=sshd --since "2 hours ago" --no-pager
sudo journalctl -u ssh --since "2 hours ago" --no-pager 2>/dev/null \
  || sudo journalctl -u sshd --since "2 hours ago" --no-pager
sudo journalctl -u ssh-reverse-tunnel.service --since "2 hours ago" --no-pager
sudo ss -ntp
ip route get 198.51.100.44

On systems using file-based auth logs, use the distribution’s documented location and access controls. Correlate successful public-key authentication with the source address, account, key fingerprint or certificate identity, forwarding request, process owner, destination socket, and firewall connection. The absence of a shell command does not make a tunnel benign.

  • Alert on new -R, -L, -D, -W, or tunnel interfaces outside the approved inventory.
  • Alert on bastion listeners that are not 127.0.0.1 or explicitly approved private addresses.
  • Watch for persistent outbound SSH to unapproved hosts, repeated reconnects, unusual long-lived byte counts, DNS lookups to unapproved resolvers, and DMZ attempts toward trusted, management, storage, or backup networks.
  • Use host and firewall rate limits. SSH MaxStartups and any version-specific per-source setting protect authentication capacity; they do not constrain traffic inside an established forwarding channel.

Contain without erasing the trail

  1. Record the time, host, account, source, destination, PID, parent, command-line metadata, listener address, key fingerprint, current connections, and relevant firewall counters. Preserve logs and volatile evidence under the incident policy.
  2. Contain the smallest boundary first: stop the unauthorized service or SSH client, block the specific egress destination, remove the unauthorized listener, and terminate the associated SSH session. Keep a trusted administrative session and tested console path open.
  3. Revoke the identity: remove or quarantine the affected authorized-key line, revoke the certificate serial through the configured KRL or CA process, expire the access grant, and rotate any credential that may have been exposed. Removing a key does not terminate an already-established session, so verify and terminate that session.
  4. Inspect the private host and bastion for persistence, new users, modified SSH configuration, scheduled jobs, service units, containers, changed binaries, and lateral connections. Check whether the tunnel reached secrets or management services.
  5. Patch the vulnerable component, restore the least-privilege forwarding policy, and test sshd -t before a reload. Do not re-enable forwarding globally to recover convenience.
  6. Document root cause, scope, evidence hashes where applicable, actions, owner, and follow-up tests. Notify the independent bastion operator and affected service owners.

Use a safe emergency policy

If the bastion is under active abuse, set the forwarding identity to a deny state through the approved change or incident process, revoke its key, terminate its carrier, and block its specific source or destination. A temporary global DisableForwarding yes may be appropriate when the host is actively compromised, but validate the impact on recovery and record the rollback owner. Never assume changing authorized_keys alone disconnects existing channels.

# Validate before applying a reviewed emergency drop-in.
sudo sshd -t
sudo sshd -T -C user=tunnel,host=bastion.example.net,addr=198.51.100.20 \
  | grep -E '^(allowtcpforwarding|gatewayports|permitlisten|disableforwarding)'
sudo systemctl reload ssh 2>/dev/null || sudo systemctl reload sshd

Keep an out-of-band console or trusted recovery host available. After containment, inspect ss -lntup, process ancestry, logs, firewall counters, and key inventories again; then repeat after reboot to catch persistence.

Test the detection program

ExerciseExpected result
Ordinary account requests -R, -L, or -DAuthentication may succeed, but forwarding is denied and the event is logged.
Approved tunnel requests another listener or wildcard bindPermitListen/GatewayPorts rejects it; no public socket appears.
Forwarding key is revoked while connectedNew login fails; the active session is explicitly terminated and its listener disappears.
Unauthorized outbound SSH from a DMZ hostEgress rule blocks or alerts, with source, destination, owner, and counter evidence.
Reboot after cleanupNo unit, timer, cron job, user service, or container recreates the tunnel.

Run exercises in an approved window using non-production test identities. Keep Tailscale, Cloudflare Tunnel, and the documented console recovery path distinct from reverse SSH evidence so one system’s health does not hide another system’s compromise.

Return to Operate and Recover SSH on the Raspberry Pi 5 for the broader Pi recovery runbook.

Sequence navigation

Previous: Use a Bastion Host for Reverse SSH Access Behind CGNAT · Next: Operate and Recover SSH on the Raspberry Pi 5

Official references