Efficient Multi-Hop SSH Tunneling for Large-Scale Data Transfer Between Firewalled Environments


3 views

We're dealing with a classic constrained environment where:

  • Application servers cannot initiate outbound connections to jump hosts
  • All access must originate from jump boxes
  • Direct server-to-server transfer is blocked by firewall rules
  • Storage limitations prevent using jump boxes as temporary staging areas

The key is establishing a reverse tunnel chain that leverages the jump boxes' interconnectivity. Here's the step-by-step approach:


# On QA Jump Box (initiating the tunnel chain):
ssh -R 2222:localhost:22 dev_jump_user@dev_jump_box

# On Development Jump Box (creating the final hop):
ssh -L 3333:dev_app_server:22 -p 2222 qa_jump_user@localhost

With tunnels established, we can now perform direct data transfer:


# From QA Application Server (after tunnels are up):
rsync -avz -e 'ssh -p 3333' dev_app_user@localhost:/source/path /destination/path

For 670GB of data, consider these enhancements:

  • Compression: Add -z flag to rsync
  • Bandwidth limiting: --bwlimit=50000 (50MB/s)
  • Checksum verification: -c for critical data
  • Resume capability: --partial --progress

Here's a robust Bash implementation:


#!/bin/bash
# Establish tunnel chain
ssh -fN -R 2222:localhost:22 dev_jump_user@dev_jump_box
ssh -fN -L 3333:dev_app_server:22 -p 2222 qa_jump_user@localhost

# Verify tunnel connectivity
nc -z localhost 3333 || { echo "Tunnel failed"; exit 1; }

# Execute transfer with progress monitoring
rsync -avz --stats --human-readable --progress \
    -e 'ssh -p 3333 -o StrictHostKeyChecking=no' \
    dev_app_user@localhost:/source/path /destination/path

# Cleanup
pkill -f "ssh -fN -R 2222"
pkill -f "ssh -fN -L 3333"

When tunnels aren't feasible:

  • Netcat over SSH: tar cvzf - /source | ssh jump_box "nc -l 1234" | ssh qa_box "nc dev_box 1234 | tar xvzf -"
  • SOCKS proxy chaining
  • SSH ControlMaster for persistent connections

Always:

  • Use SSH key authentication
  • Set appropriate timeout values
  • Restrict port forwarding ranges
  • Monitor tunnel processes

In complex enterprise environments, we often encounter strict firewall rules that create data transfer challenges. Here's the exact topology we're dealing with:

Client → DEV_Jump_Box (10.0.1.10) → DEV_App_Server (10.0.2.20)
                  ↓
Client → QA_Jump_Box (10.0.1.11)  → QA_App_Server (10.0.3.30)

The critical constraints:

  • App servers can't initiate connections to jump boxes
  • Direct app-to-app communication is blocked
  • Jump boxes can communicate with each other

Since we can't establish forward tunnels from app servers, we'll use reverse SSH tunnels through the jump boxes:

# On DEV Application Server (run as persistent background process):
ssh -fNTR 2222:localhost:22 user@DEV_Jump_Box

# On QA Application Server:
ssh -fNTR 3333:localhost:22 user@QA_Jump_Box

This creates endpoints on each jump box that we can chain together.

From your local machine (or CI/CD server):

# First hop: Local to DEV jump box
ssh -L 4000:localhost:2222 user@DEV_Jump_Box

# Second hop: DEV jump box to QA jump box
ssh -L 5000:QA_Jump_Box:3333 user@DEV_Jump_Box

# Final connection point
ssh -p 5000 user@localhost

With tunnels established, we can use rsync for efficient transfer:

rsync -avz --progress -e "ssh -p 5000" /source/path/ user@localhost:/destination/path/

For better performance with large files:

rsync -avz --progress --bwlimit=50000 --partial \
    -e "ssh -p 5000 -c aes128-gcm@openssh.com" \
    /source/path/ user@localhost:/destination/path/

Simplify recurrent connections with ~/.ssh/config:

Host dev-jump
    HostName DEV_Jump_Box
    User service-account
    IdentityFile ~/.ssh/tunnel_key

Host qa-jump
    HostName QA_Jump_Box  
    User service-account
    IdentityFile ~/.ssh/tunnel_key

Host dev-app-via-jump
    HostName localhost
    Port 2222
    ProxyJump dev-jump

Host qa-app-via-jump
    HostName localhost  
    Port 3333
    ProxyJump qa-jump

For long-running transfers, use autossh to maintain tunnels:

# On both app servers:
autossh -M 0 -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \
    -NTR 2222:localhost:22 user@DEV_Jump_Box

Combine with tmux/screen for session persistence:

tmux new-session -d -s tunnel 'autossh [...]'

If tunnel stability is problematic, use jump boxes as temporary relays:

# On DEV jump box:
ssh user@DEV_App_Server "tar cf - /source/path" | \
ssh user@QA_Jump_Box "ssh user@QA_App_Server 'tar xf - -C /destination'"

This streams data through the jump boxes without requiring local storage.