Detailed documentation of SSH tunnel function
Function Overview
What is an SSH tunnel?
SSH Tunneling, also known as SSH port forwarding, is a technology that establishes an encrypted channel between local and remote servers through the SSH protocol.
Application scenarios
-
Connect to the database behind the firewall
- The database is located on the intranet and cannot be accessed directly
- Access intranet MongoDB through Bastion Host
-
Encrypt unsecured network connections
- Securely access MongoDB in a public network environment
- Prevent data transmission from being eavesdropped
-
Unified secure access portal
- All database access is through SSH tunnel
- Centrally manage access rights
Working principle
Your application → SSH tunnel (local port) → SSH server → Intranet MongoDB
(Local) (Encrypted transmission) (Springboard) (Target database)
Quick start
1. Enable SSH tunneling
Install monsqlize, then add an ssh block under the MongoDB config. monSQLize opens the tunnel during connect() and closes it during close().
2. Basic configuration (password authentication)
import MonSQLize from 'monsqlize';
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'mydb',
config: {
//SSH tunnel configuration
ssh: {
host: 'bastion.example.com',
port: 22,
username: 'deploy',
password: 'your-password', //SSH password
},
//MongoDB connection configuration (automatically resolves remoteHost and remotePort from URI)
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/mydb'
}
});
await msq.connect(); //Automatically establish SSH tunnel
//... using MongoDB
await msq.close(); //Automatically close SSH tunnel
3. Basic configuration (private key authentication, recommended)
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'mydb',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa', //private key path
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/mydb'
}
});
Configuration instructions
SSH configuration items
MongoDB configuration items
Automatic parsing rules:
- ✅ Recommendation: Only configure
uri, the system will automatically parse remoteHost and remotePort from the URI
- ⚠️ Special case: If the address in the URI is different from the actual MongoDB server address, you only need to specify it explicitly
Example comparison:
//✅ Recommended: Automatic analysis (99% of scenarios)
config: {
ssh: { host: 'bastion', username: 'user', password: 'pass' },
uri: 'mongodb://user:pass@internal-mongo:27017/mydb'
//remoteHost and remotePort automatically resolve from URI to: internal-mongo:27017
}
//⚠️ Explicitly specified (special scenario: URI is different from the actual address)
config: {
ssh: { host: 'bastion', username: 'user', password: 'pass' },
uri: 'mongodb://user:pass@loadbalancer:27017/mydb', // Use the load balancing address
remoteHost: 'actual-mongo-server', //Actual MongoDB server
remotePort: 27017
}
Usage example
Example 1: Password authentication (simple test)
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
password: 'your-password',
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production'
}
});
await msq.connect();
const users = msq.collection('users');
const count = await users.count({});
console.log('Total number of users:', count);
await msq.close();
Example 2: Private key authentication (recommended for production environment)
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
port: 22,
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa', //Supports ~ symbol
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@10.0.1.100:27017/production'
}
});
Example 3: Using private key content
const fs = require('fs');
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKey: fs.readFileSync('/path/to/id_rsa', 'utf8'), //Directly transfer the private key content
},
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production'
}
});
Example 4: Using an encrypted private key
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
passphrase: 'your-key-password', //Private key password
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production'
}
});
Example 5: Specify local port
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
localPort: 27018, //Fixed local port
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production'
}
});
//MongoDB will connect via localhost:27018
Example 6: Custom SSH port
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
port: 2222, //Custom SSH port
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production'
}
});
Example 7: Adjust timeout settings
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
readyTimeout: 30000, //SSH connection timeout 30 seconds
keepaliveInterval: 60000, //Heartbeat interval 60 seconds
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://mongouser:mongopass@internal-mongo:27017/production',
options: {
serverSelectionTimeoutMS: 10000, //MongoDB select server timeout
connectTimeoutMS: 10000, //MongoDB connection timeout
}
}
});
Troubleshooting
Problem 1: SSH connection timeout
Error message:
Error: Timed out while waiting for handshake
Possible reasons:
- The SSH server address is wrong or inaccessible
- SSH port blocked by firewall
- The network is unstable
Solution:
//1. Check whether the SSH server is accessible
//Run in terminal: ssh user@host -p port
//2. Increase the timeout period
config: {
ssh: {
//... other configurations
readyTimeout: 60000, //increase to 60 seconds
}
}
Problem 2: SSH authentication failed
Error message:
Error: All configured authentication methods failed
Possible reasons:
- Incorrect username or password
- The private key path is wrong or the permissions are incorrect.
- The SSH server does not allow this authentication method
Solution:
# 1. Test SSH login
ssh user@host -p port
# 2. Check private key permissions
chmod 600 ~/.ssh/id_rsa
# 3. Use password authentication testing
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
password: 'your-password', //Test password authentication
}
}
Problem 3: MongoDB connection failed
Error message:
MongoServerSelectionError: Server selection timed out
Possible reasons:
- MongoDB address or port error
- MongoDB authentication failed
- MongoDB service is not running
Solution:
//1. Confirm MongoDB address and port
//Run on SSH server: nc -zv internal-mongo 27017
//2. Check the MongoDB URI
config: {
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://user:pass@host:port/db?authSource=admin'
}
//3. Add directConnection option (if it is a replica set)
config: {
uri: 'mongodb://user:pass@host:port/db?directConnection=true',
options: {
directConnection: true,
}
}
The built-in SSH tunnel supports one MongoDB host in the URI. Multi-host replica set URIs and mongodb+srv:// SRV URIs are rejected with INVALID_CONFIG; use an external SSH tunnel, bastion proxy, or MongoDB-side network access for those topologies.
Problem 4: Port conflict
Error message:
Error: listen EADDRINUSE: address already in use
Solution:
//Specify a different local port
config: {
ssh: {
//... other configurations
localPort: 27019, //Use another port
}
}
//Or do not specify (automatically assign a random port)
config: {
ssh: {
//... other configurations
//localPort is not set
}
}
Best Practices
1. Use private key authentication
✅ Recommended:
ssh: {
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
}
❌ Not recommended:
ssh: {
username: 'deploy',
password: 'plain-text-password', //Clear text passwords are not safe
}
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: process.env.SSH_HOST,
username: process.env.SSH_USER,
privateKeyPath: process.env.SSH_KEY_PATH,
},
//Automatically resolve remoteHost and remotePort from URI
uri: process.env.MONGO_URI
}
});
3. Properly manage SSH keys
# Generate SSH key pair
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_mongo
# Set private key permissions
chmod 600 ~/.ssh/id_rsa_mongo
# Copy the public key to the SSH server
ssh-copy-id -i ~/.ssh/id_rsa_mongo.pub user@bastion.example.com
4. Connection pool and timeout configuration
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
readyTimeout: 30000,
keepaliveInterval: 30000,
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://user:pass@internal-mongo:27017/production',
options: {
maxPoolSize: 10, //Maximum number of connections
minPoolSize: 2, //Minimum number of connections
serverSelectionTimeoutMS: 10000,
connectTimeoutMS: 10000,
socketTimeoutMS: 45000,
}
}
});
5. Error handling
try {
await msq.connect();
//Using MongoDB
} catch (err) {
if (err.message.includes('Authentication')) {
console.error('SSH authentication failed, please check username and password/private key');
} else if (err.message.includes('Timed out')) {
console.error('SSH connection timed out, please check network and server status');
} else if (err.message.includes('MongoServerSelectionError')) {
console.error('MongoDB connection failed, please check the MongoDB address and authentication information');
} else {
console.error('Unknown error:', err);
}
} finally {
await msq.close();
}
6. Logging
const msq = new MonSQLize({
type: 'mongodb',
databaseName: 'production',
logger: {
info: (msg, meta) => console.log('[INFO]', msg, meta),
warn: (msg, meta) => console.warn('[WARN]', msg, meta),
error: (msg, meta) => console.error('[ERROR]', msg, meta),
debug: (msg, meta) => console.debug('[DEBUG]', msg, meta),
},
config: {
ssh: {
host: 'bastion.example.com',
username: 'deploy',
privateKeyPath: '~/.ssh/id_rsa',
},
//Automatically resolve remoteHost and remotePort from URI
uri: 'mongodb://user:pass@internal-mongo:27017/production'
}
});
//Output example:
// [INFO] 🔐 Establishing SSH tunnel for MongoDB...
// [INFO] ✅ SSH connection established [MongoDB]
// [INFO] ✅ SSH tunnel ready [MongoDB] { localPort: 56789, remote: 'internal-mongo:27017' }
FAQ
A: There is a slight effect, but it is usually negligible.
- SSH tunnel establishment and transfer overhead depend on key exchange, network path, cipher, proxy hops, and workload
- Measure connect and steady-state throughput in the deployment environment before setting timeouts or capacity budgets
- Suitable for most application scenarios
Q2: Are multiple SSH tunnels supported?
A: Supported. Each MonSQLize instance can establish an independent SSH tunnel.
const msq1 = new MonSQLize({ config: { ssh: { host: 'bastion1' } } });
const msq2 = new MonSQLize({ config: { ssh: { host: 'bastion2' } } });
await msq1.connect(); //SSH tunnel 1
await msq2.connect(); //SSH Tunnel 2 (standalone)
Q3: Will the SSH tunnel automatically reconnect after it is disconnected?
A: No. Requires manual reconnection. If the SSH connection closes after it was ready, monSQLize marks the tunnel as disconnected and closes the local forwarding server so subsequent use fails fast instead of silently using a dead tunnel.
It is recommended to implement reconnection logic:
async function connectWithRetry(msq, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await msq.connect();
return;
} catch (err) {
if (i === maxRetries - 1) throw err;
console.log(`Connection failed, try again in ${3-i} seconds...`);
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
Q4: Does it support springboard chain (multi-level SSH)?
A: Not directly supported. ProxyJump needs to be configured on the SSH server.
SSH configuration example (~/.ssh/config):
Host final-server
HostName internal-mongo.local
User mongouser
ProxyJump bastion1,bastion2
Q5: Is SSH tunnel supported on Windows?
A: Fully supported. monSQLize uses the ssh2 library (pure JavaScript implementation) and does not require a system SSH client.
Q6: Which is better, password authentication or private key authentication?
A: Private key authentication is more secure and recommended.