databasesequelize.jsssh-tunnel

SSH tunnel to Sequelize PostgreSQL database


I am trying to access one of our remote databases (AWS RDS) that is hidden behind a bastion EC2 instance. I can access the database easily through my SQL client, but cannot access it through the CLI tool I am building (using Sequelize and tunnel-ssh). I was following this GitHub Gist but it uses the same values everywhere and is quite confusing unfortunately.

I will admit to having a poor understanding of SSH tunnelling in general, which may be apparent in the examples below. Is there something wrong with my configuration?


Database Config

Host:     wdXXXXXXXXXXXX.XXXXXXXXX.XX-XXXXX-X.rds.amazonaws.com
Port:     5432
User:     [DB_USER]
Password: [DB_PASSWORD]
Database: [DB_NAME]

Bastion Config

Server:   35.183.XX.XXX
Port:     22
Password:
SSH Key:  ~/.ssh/id_rsa.aws
const config = {
  // I don't need to specify any local values, do I?
  // localHost: "127.0.0.1",
  // localPort: 5432,

  // This should be bastion config, correct?
  username: "ec2-user",
  host: 35.183.XX.XXX,
  port: 22,
  privateKey: require("fsf").readFileSync("/path/to/ssh/key"),

  // This should be destination (database) config, correct?
  dstHost: wdXXXXXXXXXXXX.XXXXXXXXX.XX-XXXXX-X.rds.amazonaws.com,
  dstPort: 5432
};

// NOTE: If I don't have an "await" here, nothing seems to run inside the function itself (no consoles, etc)
const server = await tunnel(config, async (error, server) => {
  if (error) return console.error(error);

  const db = new Sequelize(DB_NAME, DB_USER, DB_PASSWORD, {
    dialect: "postgres",
    // NOTE: If this is already the destination in the SSH tunnel, should I use it again vs localhost?
    host: "wdXXXXXXXXXXXX.XXXXXXXXX.XX-XXXXX-X.rds.amazonaws.com",
    port: 5432
  });

  db.authenticate().then(async () => {
    const orgs = await db.organization.findAll();

    console.log("Successful query", orgs);
  }).catch(err => {
    console.error("DB auth error": err);
  });
});

Is there something wrong with my configuration above? Is my understanding of tunnels flawed by the values I've used in the tunnel config?

Also, why doesn't the tunnel callback appear to be called unless I await the function (which doesn't seem to be a Promise at all)?

P.S. There's also this Sequelize GitHub issue that mentions connecting with Sequelize via an SSH tunnel, but gives no examples.


Solution

  • I eventually got this figured out and was able to resolve the problem (12 hours after it started)... The database and bastion configurations were technically correct, but I was passing some values incorrectly (due to a flawed understanding of SSH tunneling).

    Database Config

    Host:     wdXXXXXXXXXXXX.XXXXXXXXX.XX-XXXXX-X.rds.amazonaws.com
    Port:     5432
    User:     [DB_USER]
    Password: [DB_PASSWORD]
    Database: [DB_NAME]
    

    Bastion Config

    Server:   35.183.XX.XXX
    Port:     22
    Password:
    SSH Key:  ~/.ssh/id_rsa.aws
    
    const config = {
      // I have confirmed that the local values are unnecessary (defaults work)
    
      // Configuration for SSH bastion
      username: "ec2-user",
      host: 35.183.XX.XXX,
      port: 22,
      privateKey: require("fs").readFileSync("/path/to/ssh/key"),
    
      // Configuration for destination (database)
      dstHost: wdXXXXXXXXXXXX.XXXXXXXXX.XX-XXXXX-X.rds.amazonaws.com,
      dstPort: 5432
    };
    
    // NOTE: Moved to its own function, refactor likely fixed a few issues along the way
    const getDB = () => new Promise((resolve, reject) => {
      const tnl = await tunnel(config, async error => {
        if (error) return reject(error);
    
        const db = new Sequelize(DB_NAME, DB_USER, DB_PASSWORD, {
          dialect: "postgres",
          // NOTE: This is super important as the tunnel has essentially moved code execution to the database server already...
          host: "localhost",
          port: 5432
        });
    
        return resolve(db);
      });
    });
    

    In the end, the major change was using localhost in the Sequelize configuration as the SSH tunnel "emerges" on the database instance, hence it should reference itself. There were likely a few other tweaks that were necessary (as I know I tried that at one point), but eventually I emerged intact.