Overview
SSH (Secure Shell) provides encrypted communication channels between networked devices. The protocol operates at the application layer and establishes secure connections over TCP port 22 by default. SSH replaced insecure protocols like Telnet and rlogin by encrypting all transmitted data including authentication credentials.
The protocol architecture consists of three main components: the transport layer handles server authentication and encryption setup, the authentication layer verifies client identity, and the connection layer multiplexes multiple logical channels over a single SSH connection. Each layer operates independently with defined responsibilities.
SSH supports multiple authentication methods including password-based, public key cryptography, and keyboard-interactive authentication. Public key authentication remains the most secure method as it eliminates password transmission over the network. The protocol negotiates encryption algorithms during connection establishment, selecting the strongest mutually supported cipher.
require 'net/ssh'
# Basic SSH connection
Net::SSH.start('example.com', 'username', password: 'secret') do |ssh|
result = ssh.exec!('hostname')
puts result
end
SSH sessions persist after initial authentication, allowing multiple operations without repeated credential exchange. The protocol maintains connection state and handles channel management transparently. Session multiplexing reduces overhead by reusing established connections for subsequent operations.
Key Principles
The SSH protocol operates through a layered architecture where each layer provides specific security and communication functions. The transport layer establishes the encrypted tunnel, the user authentication layer verifies identity, and the connection layer provides multiplexed channels for various services.
Transport Layer Protocol
The transport layer initiates with version exchange where both client and server advertise supported SSH protocol versions. Following version negotiation, algorithm negotiation occurs where both parties agree on key exchange methods, encryption algorithms, MAC algorithms, and compression methods. The protocol selects algorithms based on client preference order matched against server capabilities.
Key exchange establishes shared secrets without transmitting them across the network. Diffie-Hellman key exchange variants generate session keys that encrypt subsequent communication. The server provides its host key during this phase, which clients verify against known hosts to prevent man-in-the-middle attacks. After successful key exchange, the protocol derives encryption keys, integrity keys, and initialization vectors from the shared secret.
Authentication Layer
Authentication begins after transport layer encryption activates. The protocol supports multiple authentication methods that can be attempted sequentially until one succeeds. Public key authentication requires the client to prove possession of the private key corresponding to an authorized public key. The server sends a challenge that only the private key holder can correctly sign.
Password authentication transmits credentials encrypted by the transport layer. While simpler than key-based authentication, password methods remain vulnerable to brute force attacks if passwords lack sufficient entropy. Keyboard-interactive authentication provides flexible authentication flows supporting one-time passwords and multi-factor authentication schemes.
Connection Layer
The connection layer multiplexes multiple channels over the authenticated connection. Channel types include session channels for shell access and command execution, direct-tcpip channels for port forwarding, and forwarded-tcpip channels for reverse port forwarding. Each channel operates independently with separate data streams and flow control.
Session channels support interactive shells, command execution, and subsystem requests. The protocol specifies subsystems like SFTP (SSH File Transfer Protocol) and SCP (Secure Copy Protocol) that operate over session channels. Channels maintain separate stdin, stdout, and stderr streams, preserving the semantics of local command execution.
Cryptographic Negotiation
During connection establishment, both parties exchange lists of supported algorithms in preference order. The protocol selects the first client algorithm that appears in the server's list for each algorithm category. Categories include key exchange algorithms, server host key algorithms, encryption algorithms for each direction, MAC algorithms for each direction, and compression algorithms for each direction.
Modern implementations prefer elliptic curve cryptography for key exchange (curve25519, ECDH) and authenticated encryption modes (AES-GCM, ChaCha20-Poly1305). The protocol renegotiates keys periodically or after transmitting specified data volumes to limit exposure if keys become compromised.
Security Implications
SSH protocol security depends on proper implementation of cryptographic primitives, secure key management, and defense against various attack vectors. The protocol's security model assumes network connections are untrusted and potentially compromised by active attackers.
Host Key Verification
Host key verification prevents man-in-the-middle attacks by confirming server identity before authentication. Clients maintain a known_hosts file mapping hostnames to verified public keys. First connection to a new host requires manual verification of the host key fingerprint through an out-of-band channel.
require 'net/ssh'
# Strict host key verification
Net::SSH.start('example.com', 'username',
verify_host_key: :always,
password: 'secret'
) do |ssh|
ssh.exec!('whoami')
end
# Accept unknown hosts (development only)
Net::SSH.start('new-host.com', 'username',
verify_host_key: :never,
password: 'secret'
) do |ssh|
ssh.exec!('pwd')
end
Changing host keys indicate potential security issues including server reinstallation, key rotation, or active attacks. The protocol provides no mechanism for automatic host key validation, requiring manual intervention when keys change. Production systems should implement automated host key distribution through configuration management rather than disabling verification.
Key-Based Authentication Security
Private keys require protection equivalent to passwords since possession of an unencrypted private key grants access to associated accounts. Keys should use passphrase encryption to protect against theft. Modern key formats like OpenSSH's native format provide stronger encryption than legacy PEM formats.
require 'net/ssh'
# Key-based authentication with encrypted key
Net::SSH.start('example.com', 'username',
keys: ['~/.ssh/id_ed25519'],
passphrase: 'key_passphrase',
auth_methods: ['publickey']
) do |ssh|
result = ssh.exec!('ls -la')
puts result
end
Key rotation practices mitigate risk from key compromise. Organizations should establish key lifecycle policies defining maximum key age and rotation procedures. Compromised keys must be immediately removed from authorized_keys files on all systems.
Algorithm Selection
Cryptographic algorithm selection directly impacts connection security. Weak algorithms remain supported for backward compatibility but should be disabled in security-sensitive environments. The protocol allows either party to terminate connections if acceptable algorithms cannot be negotiated.
Disable deprecated algorithms including 3DES, RC4, MD5-based MACs, and CBC mode ciphers vulnerable to plaintext recovery attacks. Prefer authenticated encryption modes eliminating the need for separate MAC algorithms. Configuration should specify minimum key lengths and restrict key exchange to methods providing forward secrecy.
Forward Secrecy
Forward secrecy ensures session decryption remains impossible even if long-term keys become compromised. Diffie-Hellman key exchange variants provide forward secrecy by generating ephemeral session keys. Static RSA key exchange lacks forward secrecy as compromised host keys enable decryption of recorded sessions.
Access Control
SSH access control operates at multiple layers. Network filtering restricts SSH connections to authorized sources. Host-based authentication validates client hostnames against permitted systems. User-level controls define which public keys can authenticate to specific accounts and what commands they can execute.
Command restrictions in authorized_keys files limit key usage to specific operations. The command= option forces execution of specified commands regardless of client requests. This pattern implements automated systems requiring SSH access without granting interactive shell access.
# Using Net::SSH with restricted commands
require 'net/ssh'
Net::SSH.start('backup-server.com', 'backup-user',
keys: ['~/.ssh/backup_key'],
auth_methods: ['publickey']
) do |ssh|
# This key might be restricted to rsync commands
output = ssh.exec!('rsync --version')
puts output
end
Ruby Implementation
Ruby's Net::SSH library provides SSH protocol implementation supporting all major authentication methods, port forwarding, and file transfer operations. The library handles protocol negotiation, encryption, and channel management transparently.
Basic Connection Management
Net::SSH uses block syntax for automatic connection cleanup. Connections close automatically when blocks exit, preventing resource leaks. The library maintains connection state and reuses existing connections when multiple operations execute within a single block.
require 'net/ssh'
# Connection with automatic cleanup
Net::SSH.start('server.example.com', 'deploy',
keys: ['~/.ssh/id_rsa'],
auth_methods: ['publickey']
) do |ssh|
# Execute multiple commands
ssh.exec!('cd /var/www && git pull')
ssh.exec!('systemctl restart webapp')
end
# Connection automatically closed
Connections without blocks require explicit closing. The start method returns a session object that must be closed to release resources. Manual connection management suits scenarios requiring connection persistence across method boundaries.
require 'net/ssh'
# Manual connection management
session = Net::SSH.start('database.example.com', 'dba',
password: ENV['DB_SSH_PASSWORD'],
port: 2222
)
begin
session.exec!('pg_dump production > backup.sql')
session.exec!('gzip backup.sql')
ensure
session.close
end
Authentication Methods
The library supports multiple authentication methods attempted in specified order. Password authentication provides simplicity but less security than key-based methods. Key authentication requires specifying key file paths or using ssh-agent.
require 'net/ssh'
# Multiple authentication methods
Net::SSH.start('server.example.com', 'admin',
keys: ['~/.ssh/id_ed25519', '~/.ssh/id_rsa'],
password: 'fallback_password',
auth_methods: ['publickey', 'password']
) do |ssh|
ssh.exec!('uptime')
end
# SSH agent authentication
Net::SSH.start('server.example.com', 'user',
auth_methods: ['publickey'],
use_agent: true
) do |ssh|
ssh.exec!('hostname')
end
Command Execution Patterns
The exec! method executes commands synchronously and returns output as strings. This method suits simple commands where output fits in memory. Complex commands requiring real-time output processing need asynchronous execution.
require 'net/ssh'
Net::SSH.start('app-server.com', 'deploy') do |ssh|
# Simple synchronous execution
output = ssh.exec!('df -h')
puts output
# Asynchronous execution with callbacks
ssh.open_channel do |channel|
channel.exec('tail -f /var/log/app.log') do |ch, success|
abort "Command failed" unless success
channel.on_data do |ch, data|
puts "STDOUT: #{data}"
end
channel.on_extended_data do |ch, type, data|
puts "STDERR: #{data}" if type == 1
end
end
end
ssh.loop
end
Port Forwarding
Local port forwarding redirects local ports through SSH to remote destinations. This technique accesses services behind firewalls or provides encrypted tunnels for unencrypted protocols. Remote port forwarding exposes local services to remote networks.
require 'net/ssh'
Net::SSH.start('gateway.example.com', 'user') do |ssh|
# Local port forwarding: localhost:3307 -> remote:3306
ssh.forward.local(3307, 'database.internal', 3306)
# Access forwarded port from another connection
require 'mysql2'
client = Mysql2::Client.new(
host: '127.0.0.1',
port: 3307,
username: 'dbuser',
password: 'dbpass'
)
results = client.query('SELECT COUNT(*) FROM users')
puts results.first
ssh.loop { true }
end
File Transfer Operations
Net::SCP provides SCP protocol implementation for file transfers. The library handles file permissions and timestamps automatically. Recursive directory transfers require explicit options.
require 'net/ssh'
require 'net/scp'
Net::SSH.start('file-server.com', 'user') do |ssh|
# Upload single file
ssh.scp.upload!('/local/path/file.txt', '/remote/path/file.txt')
# Download file
ssh.scp.download!('/remote/logs/app.log', '/local/logs/app.log')
# Recursive directory upload
ssh.scp.upload!('/local/directory', '/remote/directory',
recursive: true
)
# Upload with progress callback
ssh.scp.upload!('/local/large-file.zip', '/remote/large-file.zip') do |ch, name, sent, total|
progress = (sent.to_f / total * 100).round(2)
print "\rUploading: #{progress}%"
end
puts "\nUpload complete"
end
Implementation Approaches
SSH implementation strategies vary based on automation requirements, scale, and security constraints. Approaches differ in connection management, error handling, and integration with existing infrastructure.
Direct Connection Pattern
Direct connections work for simple automation tasks requiring limited SSH operations. Each operation establishes a new connection, authenticates, executes commands, and closes. This pattern suits scripts running infrequently where connection overhead remains acceptable.
require 'net/ssh'
def deploy_application(host, user)
Net::SSH.start(host, user, keys: ['~/.ssh/deploy_key']) do |ssh|
commands = [
'cd /var/www/app',
'git pull origin main',
'bundle install --deployment',
'systemctl restart app'
]
commands.each do |cmd|
output = ssh.exec!(cmd)
puts "#{cmd}: #{output}"
end
end
rescue Net::SSH::AuthenticationFailed
abort "Authentication failed for #{user}@#{host}"
rescue Net::SSH::ConnectionTimeout
abort "Connection timeout for #{host}"
end
deploy_application('web1.example.com', 'deploy')
Connection Pool Pattern
Connection pooling maintains persistent connections for reuse across multiple operations. This approach reduces latency by eliminating repeated authentication overhead. Pools manage connection lifecycle including validation, reuse, and cleanup.
require 'net/ssh'
require 'connection_pool'
class SSHConnectionPool
def initialize(host, user, size: 5)
@pool = ConnectionPool.new(size: size, timeout: 5) do
Net::SSH.start(host, user,
keys: ['~/.ssh/id_rsa'],
keepalive: true,
keepalive_interval: 60
)
end
end
def exec(command)
@pool.with do |ssh|
ssh.exec!(command)
end
end
def shutdown
@pool.shutdown { |ssh| ssh.close }
end
end
# Usage
pool = SSHConnectionPool.new('app-server.com', 'deploy', size: 10)
50.times do
Thread.new do
result = pool.exec('uptime')
puts result
end
end
pool.shutdown
Batch Operations Pattern
Batch operations execute commands across multiple hosts concurrently. This pattern parallelizes deployment tasks, reducing total execution time. Thread pools or process pools manage concurrency while limiting resource consumption.
require 'net/ssh'
require 'concurrent-ruby'
class BatchSSHExecutor
def initialize(hosts, user)
@hosts = hosts
@user = user
end
def execute(command, max_threads: 10)
pool = Concurrent::FixedThreadPool.new(max_threads)
futures = @hosts.map do |host|
Concurrent::Future.execute(executor: pool) do
execute_on_host(host, command)
end
end
results = futures.map(&:value)
pool.shutdown
pool.wait_for_termination
results
end
private
def execute_on_host(host, command)
Net::SSH.start(host, @user, keys: ['~/.ssh/id_rsa']) do |ssh|
output = ssh.exec!(command)
{ host: host, output: output, success: true }
end
rescue => e
{ host: host, error: e.message, success: false }
end
end
# Execute across multiple servers
hosts = ['web1.example.com', 'web2.example.com', 'web3.example.com']
executor = BatchSSHExecutor.new(hosts, 'deploy')
results = executor.execute('systemctl status nginx')
results.each do |result|
if result[:success]
puts "#{result[:host]}: #{result[:output]}"
else
puts "#{result[:host]} ERROR: #{result[:error]}"
end
end
Gateway/Jump Host Pattern
Gateway patterns route connections through intermediate hosts when target systems lack direct network access. Jump hosts authenticate initial connections then establish secondary connections to final destinations. This pattern implements common security architectures separating public and private networks.
require 'net/ssh'
require 'net/ssh/gateway'
# Connect through bastion host
gateway = Net::SSH::Gateway.new('bastion.example.com', 'user',
keys: ['~/.ssh/bastion_key']
)
# Access internal server through gateway
gateway.ssh('internal-db.private', 'dbadmin',
keys: ['~/.ssh/internal_key']
) do |ssh|
result = ssh.exec!('pg_dump production')
File.write('backup.sql', result)
end
gateway.shutdown!
Tools & Ecosystem
The SSH ecosystem includes various tools and libraries for different use cases ranging from simple command execution to complex orchestration frameworks.
Net::SSH
Net::SSH provides the core SSH protocol implementation in Ruby. The library handles transport layer security, authentication negotiation, and channel multiplexing. Most Ruby SSH functionality builds on Net::SSH as a foundation.
require 'net/ssh'
# Connection options
options = {
keys: ['~/.ssh/id_ed25519'],
port: 2222,
timeout: 30,
forward_agent: true,
compression: true,
encryption: ['aes256-gcm@openssh.com', 'chacha20-poly1305@openssh.com'],
verify_host_key: :always
}
Net::SSH.start('server.example.com', 'user', options) do |ssh|
ssh.exec!('env')
end
Net::SCP
Net::SCP extends Net::SSH with SCP protocol support for file transfers. The library maintains compatibility with OpenSSH's SCP implementation while providing Ruby-friendly interfaces.
Capistrano
Capistrano builds on Net::SSH to provide deployment automation. The framework defines deployment workflows as reusable tasks executed across multiple servers. Capistrano handles connection management, error recovery, and rollback capabilities.
# Capfile
require 'capistrano/setup'
require 'capistrano/deploy'
# config/deploy.rb
set :application, 'myapp'
set :repo_url, 'git@github.com:user/myapp.git'
server 'web1.example.com', user: 'deploy', roles: %w{app web}
server 'web2.example.com', user: 'deploy', roles: %w{app web}
namespace :deploy do
task :restart do
on roles(:app) do
execute :systemctl, 'restart', 'myapp'
end
end
end
SSHKit
SSHKit provides lower-level SSH execution for building deployment tools. The library offers command execution abstractions, connection pooling, and output formatting. Capistrano uses SSHKit internally.
require 'sshkit'
require 'sshkit/dsl'
SSHKit::Backend::Netssh.configure do |ssh|
ssh.connection_timeout = 30
ssh.ssh_options = {
keys: ['~/.ssh/deploy_key'],
forward_agent: false,
auth_methods: ['publickey']
}
end
on ['web1.example.com', 'web2.example.com'], in: :parallel do
within '/var/www/app' do
as 'deploy' do
execute :git, :pull
execute :bundle, :install
end
end
end
Vagrant
Vagrant uses SSH for managing development virtual machines. The tool automates SSH key generation, configuration, and connection management. Vagrant SSH provides access to development environments without manual configuration.
Common Pitfalls
SSH implementation commonly encounters issues related to authentication, connection management, and security configuration. Understanding typical problems prevents repeated debugging cycles.
Host Key Verification Failures
Changed host keys cause connection failures when strict verification activates. This occurs during server reinstallation, key rotation, or when DNS returns different hosts for the same hostname. Blindly disabling verification eliminates man-in-the-middle attack protection.
require 'net/ssh'
# Wrong: Disabling verification globally
Net::SSH.start('server.com', 'user',
verify_host_key: :never # Dangerous!
) do |ssh|
ssh.exec!('hostname')
end
# Correct: Update known_hosts or verify new key
begin
Net::SSH.start('server.com', 'user',
verify_host_key: :always
) do |ssh|
ssh.exec!('hostname')
end
rescue Net::SSH::HostKeyMismatch => e
puts "Host key changed!"
puts "New fingerprint: #{e.fingerprint}"
# Verify through out-of-band channel before proceeding
end
Authentication Method Ordering
Incorrect authentication method ordering causes failures when servers limit authentication attempts. Servers disconnect clients exceeding attempt limits before trying all configured methods. Specify authentication methods explicitly in order of likelihood.
require 'net/ssh'
# Wrong: Password tried before keys
Net::SSH.start('server.com', 'user',
password: 'secret',
keys: ['~/.ssh/id_rsa']
) do |ssh|
ssh.exec!('whoami')
end
# Correct: Explicit method ordering
Net::SSH.start('server.com', 'user',
keys: ['~/.ssh/id_rsa'],
password: 'secret',
auth_methods: ['publickey', 'password']
) do |ssh|
ssh.exec!('whoami')
end
Connection Leaks
Failing to close connections exhausts file descriptors and connection limits. This occurs when exception handling prevents reaching close statements or when blocks exit prematurely. Connection pools mitigate leaks but proper cleanup remains essential.
require 'net/ssh'
# Wrong: Connection not closed on exception
def run_command(host, command)
ssh = Net::SSH.start(host, 'user')
ssh.exec!(command)
ssh.close
end
# Correct: Ensure cleanup
def run_command(host, command)
ssh = Net::SSH.start(host, 'user')
begin
ssh.exec!(command)
ensure
ssh.close
end
end
# Better: Use block form
def run_command(host, command)
Net::SSH.start(host, 'user') do |ssh|
ssh.exec!(command)
end
end
Timeout Configuration
Default timeouts may be inappropriate for long-running operations or slow networks. Commands exceeding timeouts terminate without completing. Set timeouts based on expected operation duration plus buffer for network variability.
require 'net/ssh'
# Wrong: Default timeout for long operations
Net::SSH.start('server.com', 'user') do |ssh|
ssh.exec!('pg_dump large_database') # May timeout
end
# Correct: Appropriate timeout
Net::SSH.start('server.com', 'user', timeout: 300) do |ssh|
channel = ssh.open_channel do |ch|
ch.exec('pg_dump large_database') do |ch, success|
ch.on_data do |ch, data|
File.write('backup.sql', data, mode: 'a')
end
end
end
channel.wait
end
PTY Allocation
Commands requiring pseudo-terminal allocation fail without explicit PTY requests. Interactive programs and some system commands expect terminal environments. PTY allocation affects how commands process signals and handle input/output.
require 'net/ssh'
Net::SSH.start('server.com', 'user') do |ssh|
# Wrong: Interactive command without PTY
ssh.exec!('sudo systemctl restart nginx') # May hang
# Correct: Request PTY for interactive commands
channel = ssh.open_channel do |ch|
ch.request_pty do |ch, success|
abort "PTY request failed" unless success
ch.exec('sudo systemctl restart nginx') do |ch, success|
abort "Command failed" unless success
end
end
end
channel.wait
end
Command Injection
Unsanitized user input in SSH commands enables arbitrary code execution on remote systems. Shell metacharacters allow command chaining and parameter injection. Validate and escape all user-provided data before inclusion in commands.
require 'net/ssh'
require 'shellwords'
# Wrong: Direct interpolation
def backup_file(ssh, filename)
ssh.exec!("tar czf backup.tar.gz #{filename}")
end
# Correct: Escape shell metacharacters
def backup_file(ssh, filename)
escaped = Shellwords.escape(filename)
ssh.exec!("tar czf backup.tar.gz #{escaped}")
end
# Better: Use array form when available
def backup_file(ssh, filename)
# Note: Net::SSH exec! doesn't support array form
# Escape remains necessary
escaped = Shellwords.escape(filename)
ssh.exec!("tar czf backup.tar.gz #{escaped}")
end
Reference
Connection Options
| Option | Type | Description |
|---|---|---|
| port | Integer | TCP port number, default 22 |
| timeout | Integer | Connection timeout in seconds |
| keepalive | Boolean | Enable TCP keepalive |
| keepalive_interval | Integer | Keepalive probe interval in seconds |
| compression | Boolean | Enable compression |
| forward_agent | Boolean | Enable SSH agent forwarding |
| verify_host_key | Symbol | Host key verification mode: :always, :never, :accept_new_or_local_tunnel |
| auth_methods | Array | Authentication methods to try in order |
| keys | Array | Private key file paths |
| keys_only | Boolean | Disable password authentication |
| password | String | Password for password authentication |
| passphrase | String | Private key passphrase |
| use_agent | Boolean | Use SSH agent for authentication |
| user_known_hosts_file | String | Path to known_hosts file |
Authentication Methods
| Method | Description | Security Level |
|---|---|---|
| publickey | Public key cryptography | High |
| password | Password authentication | Medium |
| keyboard-interactive | Interactive challenge-response | Medium |
| hostbased | Host-based authentication | High |
| gssapi-with-mic | GSSAPI authentication | High |
Channel Types
| Type | Purpose | Use Case |
|---|---|---|
| session | Shell access and command execution | Interactive sessions, remote commands |
| direct-tcpip | Local port forwarding | Database tunnels, service access |
| forwarded-tcpip | Remote port forwarding | Exposing local services remotely |
| x11 | X11 forwarding | GUI application display |
Common Algorithms
| Category | Recommended | Notes |
|---|---|---|
| Key Exchange | curve25519-sha256, ecdh-sha2-nistp256 | Provides forward secrecy |
| Server Host Key | ssh-ed25519, ecdsa-sha2-nistp256 | ED25519 preferred |
| Encryption | chacha20-poly1305@openssh.com, aes256-gcm@openssh.com | Authenticated encryption |
| MAC | hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com | Encrypt-then-MAC |
| Compression | none, zlib@openssh.com | Compression after authentication |
Error Classes
| Class | Cause | Recovery Strategy |
|---|---|---|
| Net::SSH::AuthenticationFailed | Invalid credentials | Verify credentials, check permissions |
| Net::SSH::ConnectionTimeout | Network unreachable | Check network, verify host up |
| Net::SSH::HostKeyMismatch | Changed host key | Verify key change legitimate, update known_hosts |
| Net::SSH::Disconnect | Server closed connection | Check server logs, retry connection |
| Net::SSH::Exception | General SSH error | Review error message, check configuration |
File Transfer Methods
| Method | Protocol | Best For |
|---|---|---|
| Net::SCP.upload! | SCP | Single files, simple transfers |
| Net::SCP.download! | SCP | Retrieving files |
| Net::SFTP | SFTP | Directory operations, file listing |
| rsync over SSH | rsync | Synchronization, large datasets |
Port Forwarding Patterns
| Pattern | Command | Description |
|---|---|---|
| Local Forward | ssh.forward.local(local_port, remote_host, remote_port) | Access remote service locally |
| Remote Forward | ssh.forward.remote(remote_port, local_host, local_port) | Expose local service remotely |
| Dynamic Forward | ssh.forward.local_socket(local_port) | SOCKS proxy through SSH |