Backstory: Why test if SSH allows passwords?

I'm developing telebit which, among other things, provides an option for you to access your devices (such as your laptop) via ssh.

Since there's an implicit trust in the physical security of the device, people don't typically worry about having good passwords on a local computer (which is a potential problem if you turn on SSH without thinking it through).

Also, because new ssh users generally aren't introduced to the more secure key-based authentication from the get-go, most people aren't using it.

I believe that security and convenience are inextricably linked - and that unless your security measures are also an increase in convenience for end-users, you're actually decreasing security. That said, I can't solve every problem all at once, and I don't want to make it inconvenient to use Telebit, so my meet-in-the-middle solution for the time being is to make it very easy for users to see their SSH config and make sure to let them know what's not-as-good as it could be, and how to fix it.

Looking at sshd_config

If you happen to know where sshd_config is and have access to it, you'll know what the ssh server should do.

There are three options worth looking at:

PermitRootLogin without-password
PasswordAuthentication no

Typically the server config will live in one of the following locations:

  • /etc/ssh/sshd_config on macOS, Linux, and other Unixes
  • C:\ProgramData\ssh\sshd_config (%PROGRAMDATA%\ssh\sshd_config) on Windows

There's also the matter of setting ChallengeResponseAuthentication no, but I'm not convinced this is necessary since it might be implemented for OTP or 2FA/MF2.

Testing if SSH requests for a password with Bash

Since there could be a big difference between what the config says the server should do and what it actually does - such as if a different config file or server is in use, and since you may not even have access to the config file anyway, it's also worth checking the server directly.

This will work for most use cases:

ssh -v -n \
  -o Batchmode=yes \
  -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null \
  DOES_NOT_EXIST@localhost

You should see something like this, which will show the broadest scope of authentication methods allowed.

debug1: Authentications that can continue: publickey,password,keyboard-interactive

Here's a breakdown of the ssh options:

  • -v causes the authentication methods to be displayed
  • -o Batchmode=yes causes ssh to enter a non-interactive mode where any prompts result in immediate failure
  • -n causes ssh to not open a shell (often used with tunneling), which in this case will cause it to immediately exit, even on success (which is important if connecting to a service that allows clients without authentication - such as honeypot or serveo.net)
  • -o StrictHostKeyChecking=no and -o UserKnownHostsFile=/dev/null are used to automatically accept the host without reading it from or writing it to the known-hosts file.
    • On Windows you'll want to use NUL (in cmd.exe) or $null (in PowerShell) instead of /dev/null
  • I suppose which user@host you use doesn't matter that much since only the global PasswordAuthentication affects the output. For example PermitRootLogin: prohibit-password takes precedence, but is not applied until after.

The debug output will likely be output to stderr rather than stdout and you'll probably want to limit the output. Adding 2>&1 | grep password will do the trick:

ssh -v -n \
  -o Batchmode=yes \
  -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null \
  DOES_NOT_EXIST@localhost 2>&1 | grep password

Though, I'm not sure what the equivalent would be on Windows...

Testing if SSH requests for a password with node.js

Here's my initial draft of putting this all together programmatically in node.js:

'use strict';

/*global Promise*/
var PromiseA = Promise;
var crypto = require('crypto');
var util = require('util');
var readFile = util.promisify(require('fs').readFile);
var exec = require('child_process').exec;

function sshAllowsPassword(user) {
  // SSH on Windows is a thing now (beta 2015, standard 2018)
  // https://stackoverflow.com/questions/313111/is-there-a-dev-null-on-windows
  var nullfile = '/dev/null';
  if (/^win/i.test(process.platform)) {
    nullfile = 'NUL';
  }
  var args = [
    'ssh', '-v', '-n'
  , '-o', 'Batchmode=yes'
  , '-o', 'StrictHostKeyChecking=no'
  , '-o', 'UserKnownHostsFile=' + nullfile
  , user + '@localhost'
  , '| true'
  ];
  return new PromiseA(function (resolve) {
    // not using promisify because all 3 arguments convey information
    exec(args.join(' '), function (err, stdout, stderr) {
      stdout = (stdout||'').toString('utf8');
      stderr = (stderr||'').toString('utf8');
      if (/\bpassword\b/.test(stdout) || /\bpassword\b/.test(stderr)) {
        resolve('yes');
        return;
      }
      if (/\bAuthentications\b/.test(stdout) || /\bAuthentications\b/.test(stderr)) {
        resolve('no');
        return;
      }
      resolve('maybe');
    });
  });
}

module.exports.checkSecurity = function () {
  var conf = {};
  var noRootPasswordRe = /(?:^|[\r\n]+)\s*PermitRootLogin\s+(prohibit-password|without-password|no)\s*/i;
  var noPasswordRe = /(?:^|[\r\n]+)\s*PasswordAuthentication\s+(no)\s*/i;
  var sshdConf = '/etc/ssh/sshd_config';
  if (/^win/i.test(process.platform)) {
    // TODO use %PROGRAMDATA%\ssh\sshd_config
    sshdConf = 'C:\\ProgramData\\ssh\\sshd_config';
  }
  return readFile(sshdConf, null).then(function (sshd) {
    sshd = sshd.toString('utf8');
    var match;
    match = sshd.match(noRootPasswordRe);
    conf.permit_root_login = match ? match[1] : 'yes';
    match = sshd.match(noPasswordRe);
    conf.password_authentication = match ? match[1] : 'yes';
  }).catch(function () {
    // ignore error as that might not be the correct sshd_config location
  }).then(function () {
    var doesntExist = crypto.randomBytes(16).toString('hex');
    return sshAllowsPassword(doesntExist).then(function (maybe) {
      conf.requests_password = maybe;
    });
  }).then(function () {
    return conf;
  });
};

if (require.main === module) {
  module.exports.checkSecurity().then(function (conf) {
    console.log(conf);
    return conf;
  });
}

I'm not going to go through the trouble of explaining that line-by-line, but it'll probably end up on npm as its own module sooner or later.

FYI, I tried testing different configurations and specifically root@localhost instead of a random user, but that didn't have an affect on the output.

See also


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )