or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

bigquery-client.mdclickhouse-client.mdcore-types.mddatabricks-client.mdfactory-functions.mdindex.mdpostgres-client.mdredshift-client.mdsnowflake-client.mdsql-builders.mdssh-tunnel.mdtrino-client.md
tile.json

ssh-tunnel.mddocs/

SSH Tunnel Support

Secure SSH tunnel connections for PostgreSQL and Redshift warehouses, enabling connections to databases behind firewalls or in private networks through SSH bastion hosts.

Capabilities

SshTunnel Class

Generic SSH tunnel manager that modifies warehouse credentials to route through an SSH tunnel.

/**
 * SSH tunnel manager for secure warehouse connections
 * Generic over credentials type to support any warehouse with SSH configuration
 * Currently supported: PostgreSQL and Redshift
 */
class SshTunnel<T extends CreateWarehouseCredentials> {
  /** Original warehouse credentials before tunnel modification */
  readonly originalCredentials: T;

  /** Modified credentials with 127.0.0.1 and tunnel port */
  overrideCredentials: T;

  /** Local port number for the SSH tunnel (undefined until connected) */
  localPort: number | undefined;

  /**
   * Initialize SSH tunnel with warehouse credentials
   * Stores original credentials and prepares for tunnel establishment
   * @param credentials - Warehouse credentials with SSH configuration
   */
  constructor(credentials: T);

  /**
   * Establish SSH tunnel and return modified credentials
   * Only creates tunnel if useSshTunnel is true and warehouse is PostgreSQL/Redshift
   * Returns original credentials unchanged if tunnel not needed
   * When tunnel is active, returns credentials with host='127.0.0.1' and port=localPort
   * Also updates the overrideCredentials property with the returned credentials
   * @returns Promise resolving to modified credentials (or original if tunnel not needed)
   * @throws SshTunnelError if SSH connection fails or credentials invalid
   */
  connect(): Promise<T>;

  /**
   * Close the SSH tunnel connection
   * Terminates SSH connection and frees local port
   * @returns Promise that resolves when tunnel is closed
   */
  disconnect(): Promise<void>;
}

Implementation Note:

The SshTunnel class uses an internal SSH2Tunnel class for the actual SSH connection management. This internal implementation handles the low-level SSH2 protocol operations using the ssh2 library. The public SshTunnel class provides a higher-level interface for warehouse integration.

Usage Examples:

import { SshTunnel, PostgresWarehouseClient } from '@lightdash/warehouses';

// Create credentials with SSH configuration
const credentials = {
  type: 'postgres',
  host: 'internal-db.private.local',
  port: 5432,
  user: 'dbuser',
  password: 'dbpassword',
  dbname: 'analytics',
  schema: 'public',
  sslmode: 'disable',
  useSshTunnel: true,
  sshTunnelHost: 'bastion.example.com',
  sshTunnelPort: 22,
  sshTunnelUser: 'sshuser',
  sshTunnelPrivateKey: `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----`,
  startOfWeek: null,
};

// Create and connect SSH tunnel
const tunnel = new SshTunnel(credentials);
const tunnelCredentials = await tunnel.connect();

console.log('Tunnel established on port:', tunnel.localPort);
console.log('Modified credentials:', tunnelCredentials);
// Modified credentials will have: host='127.0.0.1', port=<localPort>

// Create client with tunnel credentials
const client = new PostgresWarehouseClient(tunnelCredentials);

// Use client normally
await client.test();
const results = await client.runQuery('SELECT COUNT(*) FROM users');
console.log('User count:', results.rows[0].count);

// Clean up: disconnect tunnel when done
await tunnel.disconnect();
console.log('Tunnel closed');

// Example with error handling
let tunnel: SshTunnel | undefined;
try {
  tunnel = new SshTunnel(credentials);
  const modifiedCreds = await tunnel.connect();
  const client = new PostgresWarehouseClient(modifiedCreds);

  // Use client...
  await client.test();

} catch (error) {
  console.error('SSH tunnel failed:', error.message);
} finally {
  if (tunnel) {
    await tunnel.disconnect();
  }
}

Automatic Tunnel Management:

import { PostgresWarehouseClient } from '@lightdash/warehouses';

// PostgreSQL and Redshift clients automatically handle SSH tunnels
// If credentials include ssh configuration, tunnel is created internally

const credentials = {
  type: 'postgres',
  host: 'internal-db.private.local',
  port: 5432,
  user: 'dbuser',
  password: 'dbpassword',
  dbname: 'analytics',
  schema: 'public',
  sslmode: 'require',
  useSshTunnel: true,
  sshTunnelHost: 'bastion.example.com',
  sshTunnelPort: 22,
  sshTunnelUser: 'sshuser',
  sshTunnelPrivateKey: '-----BEGIN RSA PRIVATE KEY-----\n...',
  startOfWeek: null,
};

// Client automatically creates and manages SSH tunnel
const client = new PostgresWarehouseClient(credentials);

// Tunnel is established on first connection
await client.test(); // Tunnel created here

// All subsequent queries use the tunnel
await client.runQuery('SELECT * FROM orders');

// Tunnel is cleaned up when client is destroyed

Types

/**
 * SSH tunnel configuration fields
 * These fields are included directly in PostgreSQL and Redshift credentials
 */
interface SshTunnelConfiguration {
  /** Enable SSH tunnel (must be true to use tunnel) */
  useSshTunnel?: boolean;

  /** SSH bastion host hostname or IP */
  sshTunnelHost?: string;

  /** SSH port on bastion host (default: 22) */
  sshTunnelPort?: number;

  /** SSH username for authentication */
  sshTunnelUser?: string;

  /** SSH public key (stored but not used by tunnel implementation) */
  sshTunnelPublicKey?: string;

  /** SSH private key in PEM format (RSA, DSA, ECDSA, or Ed25519) */
  sshTunnelPrivateKey?: string;
}

/**
 * Base warehouse credentials type with optional SSH
 * T must extend CreateWarehouseCredentials
 */
type CreateWarehouseCredentials =
  | CreateRedshiftCredentials
  | CreateBigqueryCredentials
  | CreatePostgresCredentials
  | CreateSnowflakeCredentials
  | CreateDatabricksCredentials
  | CreateTrinoCredentials;

SSH Tunnel Workflow

The SSH tunnel workflow involves several steps:

  1. Initialization: Create SshTunnel instance with original credentials
  2. Connection: Call connect() to establish SSH tunnel:
    • Connect to SSH bastion host using provided credentials
    • Create port forward from random local port to database host:port
    • Generate modified credentials with host='127.0.0.1' and port=localPort
  3. Database Connection: Use modified credentials to connect to database
    • Traffic flows: Client -> 127.0.0.1:localPort -> SSH tunnel -> bastion -> database
  4. Disconnection: Call disconnect() to close tunnel and free resources

Example Flow:

Original:  Client -> internal-db.private.local:5432 (blocked by firewall)

With SSH:  Client -> 127.0.0.1:12345 -> [SSH Tunnel] -> bastion.example.com:22
           -> internal-db.private.local:5432 (✓ accessible)

Supported Warehouses

SSH tunnel support is currently available for:

  • PostgreSQL: Full support with all SSL modes
  • Redshift: Full support with AWS certificates

SSH tunnels are not supported for cloud-native warehouses:

  • BigQuery (uses Google Cloud API, not direct connection)
  • Clickhouse (uses HTTP/HTTPS API)
  • Databricks (uses HTTPS API)
  • Snowflake (uses HTTPS API)
  • Trino (uses HTTP API)

Security Considerations

Private Key Security

  • Never commit private keys to version control
  • Store private keys securely (environment variables, secrets manager, etc.)
  • Use encrypted private keys when possible
  • Rotate private keys regularly
  • Use dedicated SSH keys for database access (not personal SSH keys)

SSH Configuration Best Practices

  • Use strong SSH key types (prefer Ed25519 or RSA 4096-bit)
  • Restrict bastion host access with IP whitelisting
  • Use SSH key passphrases for additional security
  • Configure SSH timeouts to prevent stale connections
  • Monitor SSH access logs
  • Use dedicated service accounts for SSH tunnels

Network Security

  • Ensure bastion host is properly hardened
  • Use SSH key-based auth only (disable password auth)
  • Configure firewall rules on bastion to allow only database access
  • Use VPN + SSH tunnel for additional security layers
  • Monitor tunnel connections for anomalies

SSH Key Format

The private key must be in PEM format (string with newlines):

const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz...
[multiple lines of base64-encoded key data]
...
-----END RSA PRIVATE KEY-----`;

Supported key types:

  • RSA: -----BEGIN RSA PRIVATE KEY-----
  • DSA: -----BEGIN DSA PRIVATE KEY-----
  • ECDSA: -----BEGIN EC PRIVATE KEY-----
  • Ed25519: -----BEGIN OPENSSH PRIVATE KEY-----

Note: Passphrase-protected private keys are not currently supported. The private key must be unencrypted. To remove a passphrase from a key, use: ssh-keygen -p -f /path/to/key -N ""

Troubleshooting

Connection Timeout

Error: SSH tunnel connection timeout

Solutions:

  • Verify bastion host is reachable (check hostname, port, firewall)
  • Verify SSH credentials (username, private key)
  • Check network connectivity to bastion host
  • Verify SSH port (default 22) is correct

Authentication Failed

Error: SSH authentication failed

Solutions:

  • Verify SSH username is correct
  • Verify private key format (must be PEM)
  • Verify private key matches public key on bastion
  • Check if private key requires passphrase (not currently supported)
  • Verify SSH key is authorized on bastion host

Port Forward Failed

Error: Failed to establish port forward

Solutions:

  • Verify database host:port is reachable from bastion
  • Check bastion firewall allows outbound connections to database
  • Verify database credentials (host, port)
  • Check if local port is already in use

Tunnel Disconnected

Error: SSH tunnel disconnected unexpectedly

Solutions:

  • Check network stability
  • Verify bastion host SSH timeout configuration
  • Enable ClientAliveInterval on bastion SSH server
  • Implement reconnection logic in application
  • Monitor bastion host logs for issues

Advanced Usage

Custom Port Selection

The SSH tunnel automatically selects a random available local port. To check which port was assigned:

const tunnel = new SshTunnel(credentials);
await tunnel.connect();
console.log('Tunnel port:', tunnel.localPort); // e.g., 12345

Multiple Tunnels

Create multiple tunnels for different databases:

const tunnel1 = new SshTunnel(postgresCredentials);
const creds1 = await tunnel1.connect();

const tunnel2 = new SshTunnel(redshiftCredentials);
const creds2 = await tunnel2.connect();

const pgClient = new PostgresWarehouseClient(creds1);
const rsClient = new RedshiftWarehouseClient(creds2);

// Use both clients...

await tunnel1.disconnect();
await tunnel2.disconnect();

Long-Running Connections

For long-running applications, implement keepalive and reconnection:

class ManagedTunnel {
  private tunnel: SshTunnel<CreatePostgresCredentials>;
  private client: PostgresWarehouseClient;

  async connect() {
    this.tunnel = new SshTunnel(credentials);
    const modifiedCreds = await this.tunnel.connect();
    this.client = new PostgresWarehouseClient(modifiedCreds);
  }

  async query(sql: string) {
    try {
      return await this.client.runQuery(sql);
    } catch (error) {
      // Reconnect on tunnel failure
      if (error.message.includes('tunnel') || error.message.includes('ECONNRESET')) {
        await this.reconnect();
        return await this.client.runQuery(sql);
      }
      throw error;
    }
  }

  async reconnect() {
    await this.disconnect();
    await this.connect();
  }

  async disconnect() {
    await this.tunnel?.disconnect();
  }
}