NAME

sshdo - controls which commands may be executed via incoming ssh(1)

SYNOPSIS

  usage:
   sshdo [label]               # For use as a forced command
   sshdo [options]             # For admin use on the command line

  options:
   -h, --help                  - Output the usage message
   -V, --version               - Output the version message
   -C, --config configfile     - Override default config: /etc/sshdoers
   -c, --check [configfile...] - Check syntax in configuration files
   -l, --learn [logfile...]    - Output config to allow training logs
   -u, --unlearn [logfile...]  - Output config removing unused commands
   -a, --accepting             - For learn/unlearn, accept disallowed

  usage as a forced command in ~/.ssh/authorized_keys:
   command="/usr/bin/sshdo [label]" ssh-rsa AAAA...== user@example.net

  usage as a forced command in /etc/ssh/sshd_config:
   Match User user
   ForceCommand /usr/bin/sshdo [label]

DESCRIPTION

sshdo provides an easily configurable way of controlling which commands may be executed via incoming ssh(1) connections.

ssh can be forced to execute a particular command by specifying it in a command="" option in the ~/.ssh/authorized_keys file (or in a ForceCommand directive in /etc/ssh/sshd_config). This is a great security control but only when only a single command needs to be executed using the key.

Where there is a need to execute multiple commands using an authorized key, sshdo can be used as the forced command in the ~/.ssh/authorized_keys file (or /etc/ssh/sshd_config), and then the actual commands to be allowed can be specified in the configuration files, /etc/sshdoers and /etc/sshdoers.d/*.

See the WHY? section below for more background information.

See the manual page sshdoers(5) for details on how to specify allowed commands and who is allowed to execute them.

If a command is allowed, it is logged to the auth.info syslog(3) facility and priority with type="allowed" and it is then executed. If the command is not allowed, it is logged to auth.err with type="disallowed" and it is not executed. If the command is not allowed, but the user's authorized key is in training mode (see sshdoers(5)), then it is logged to auth.err with type="training" and it is then executed. This is useful for learning which commands need to be allowed.

When sshdo is used as a forced command, a command line argument can be supplied to act as a label for identifying which of the user's authorized keys was used (e.g. command="/usr/bin/sshdo user@example"). If supplied, it will be included in log messages. Labels are useful for allowing commands for some but not all of a user's authorized keys (see sshdoers(5)).

Note that the label should not contain whitespace characters (e.g. " ") or colon characters (":"). Any that appear will be replaced with underscore characters ("_"). If multiple command line arguments are supplied, the whitespace between them will also be replaced with underscore characters. Also, the label must not start with a dash character ("-") or it would be interpreted as (probably invalid) command line options.

There are several command line options for performing administrative tasks (see the OPTIONS section below). When invoked with no command line options (with the possible exception of the --config option), sshdo will assume that it is being invoked by sshd(8) as a forced command.

OPTIONS

The following options are mutually exclusive: --help, --version, --check, --learn and --unlearn.

-h, --help

The --help option outputs the usage message.

-V, --version

The --version option outputs the version message.

-C, --config configfile

The --config option overrides the default configuration file, /etc/sshdoers, with the configuration file specified by the given configfile argument. sshdo also consults the files in the directory whose path matches the configfile path with ".d" appended to it (e.g. /etc/sshdoers.d/). Any file in that directory whose name starts with a dot character (".") is ignored.

Note: If the --config option is used, it must be used both in the ~/.ssh/authorized_keys files (or /etc/ssh/sshd_config) and on the command line so that the same configuration is used in both cases. The $SSHDO_CONFIG environment variable can also be used to specify the configuration file (see the ENVIRONMENT section below). If both are used, the --config option takes precedence over the $SSHDO_CONFIG environment variable.

Note: When overriding the default configuration file, it is strongly recommended to use an absolute path in order to ensure that sshdo log messages that relate to different configuration files will be considered as distinct sets of log messages by the --learn and --unlearn options.

-c, --check [configfile...]

The --check option performs a syntax check on the configuration file(s) specified by the given configfile argument(s), if supplied. Otherwise, it checks the configuration file specified with the --config option, if supplied. Otherwise, it checks the configuration file specified by the $SSHDO_CONFIG environment variable, if defined. Otherwise, it checks the default configuration file, /etc/sshdoers. It also checks the files in the directory whose path matches the configuration file path with ".d" appended to it (e.g. /etc/sshdoers.d/). Any file in that directory whose name starts with a dot character (".") is ignored.

Note that errors in the configuration files wouldn't necessarily prevent sshdo from functioning. They would just be ignored. But that could mean that some commands that should be allowed would be disallowed (if any user names or group names were mistyped, for example). That's why it's important to check the syntax of configuration files before installing them. Also note that warnings for things like invalid user names and group names are not logged during normal use as a forced command. They are only reported by the --check option.

-l, --learn [logfile...]

The --learn option reads the log files specified by the given logfile arguments, if supplied. Otherwise, it reads the log files specified in the logfiles directive(s) in the configuration file, if supplied. Otherwise, it reads the default log files, /var/log/auth.log*. Log files can be uncompressed or gzip(1)-compressed. If the dash character ("-") is supplied as a logfile argument, log messages are read from standard input (stdin).

It scans the log files looking for sshdo log messages for commands that were allowed for training mode or that were disallowed (see sshdoers(5)). It then outputs the additional configuration that would be necessary to allow those commands in future. The output can be appended to /etc/sshdoers or placed in a file in /etc/sshdoers.d/.

Commands that were allowed for training mode will appear not commented out in the resulting authorization directives that are output so that they will become allowed. Commands that were only ever disallowed will appear commented out so that they will become visible but they will continue to be disallowed. The --accepting option can also be supplied which causes the disallowed commands to appear not commented out instead so that they too will become allowed.

Please check that the output wouldn't allow any command that was disallowed and that should continue to be disallowed. That's unlikely unless a private ssh key has been compromised (or just used incorrectly) during the period covered by the log files, but please check anyway.

If similar commands are encountered that vary only in the digits that appear on the command line (e.g. sequence numbers or date/time stamps), then a pattern that matches all of them will be determined (see sshdoers(5)). If the --accepting option isn't also supplied, and some of a user's uses of these similar commands were allowed and others were disallowed, then the corresponding authorization directive that is output will be commented out and will not allow any of the similar commands. If the --accepting option is also supplied in this situation, the corresponding authorization directive that is output will not be commented out and will allow all of the similar commands.

Training mode and the --learn option make it possible to safely introduce sshdo into your ssh infrastructure before you even know which commands need to be allowed. First, edit /etc/sshdoers to uncomment the "training" directive to turn on training mode globally for all users and keys (see sshdoers(5)). That will cause sshdo to allow the execution of commands that aren't already in /etc/sshdoers or /etc/sshdoers.d/*. Then, add /usr/bin/sshdo as the forced command in your ~/.ssh/authorized_keys files (or /etc/ssh/sshd_config). Then, some time later, use sshdo --learn to see what configuration is needed to allow recent activity. Then, verify that it is correct (or correct it), install the new configuration, and turn off training mode.

Please be alert to the possibility of malicious log messages that have been crafted to look like sshdo log messages in order to trick the --learn option. Malicious (or just erroneous) command executions are also possible. So please don't be tempted to fully automate the learning process. Always verify the output of the --learn option. An attack taking place during training mode might be unlikely, but it is possible.

Note that sshdo isn't intended to be used with authorized keys that are needed for interactive logins. It is only for authorized keys that are only used to execute a fixed set of commands so as to make sure that they aren't used to execute anything else. If an authorized key is needed for interactive logins, there is nothing to be gained by using sshdo (except perhaps additional logging). If the --learn option does encounter interactive logins, it will include them in the output, but they will be commented out. You can manually uncomment them to allow interactive logins, but that's probably not a good idea.

Example use of the --learn option:

  # sshdo --learn > /etc/sshdoers.d/.learned
  # vim /etc/sshdoers.d/.learned # Verify that it is correct
  # sshdo --check /etc/sshdoers.d/.learned
  /etc/sshdoers.d/.learned syntax OK
  # cat /etc/sshdoers.d/.learned >> /etc/sshdoers.d/learned
  # rm /etc/sshdoers.d/.learned
-u, --unlearn [logfile...]

The --unlearn option reads the log files specified by the given logfile arguments, if supplied. Otherwise, it reads the log files specified in the logfiles directive(s) in the configuration file, if supplied. Otherwise, it reads the default log files, /var/log/auth.log*. Log files can be uncompressed or gzip(1)-compressed. If the dash character ("-") is supplied as a logfile argument, log messages are read from standard input (stdin).

It scans the log files looking for sshdo log messages. It examines log messages for allowed commands, including those that were allowed for training mode (see sshdoers(5)). If the --accepting option is also supplied, it examines log messages for disallowed commands as well.

It compares these log messages against the current configuration to identify any authorization directives that weren't encountered in the log files and so haven't been needed recently. These directives are candidates for removal from the configuration. This can assist in maintaining strict least privilege as requirements change over time.

Bear in mind that, depending on the system's logrotate(8) configuration, the log files might only be retained for four weeks. That might not be enough to rely on for the purpose of determining which authorization directives are no longer needed.

However, if the system retains the log files for long enough for you to know that the absence of a command from the log files means that the command is no longer needed, then you could replace the authorization directives in the current configuration with the output of the --unlearn option.

Authorization directives that have been used recently are output not commented out so that their commands will continue to be allowed. Authorization directives that have not been used recently are output commented out so that their commands will no longer be allowed. This can be used to replace the authorization directives in the current configuration, safe in the knowledge that all uses of sshdo that appear in the log files will continue to be allowed, but that nothing else will. Note that any negative authorization directives (see sshdoers(5)) will not be commented out.

If similar commands are encountered that vary only in the digits that appear on the command line (e.g. sequence numbers or date/time stamps), then a pattern that matches all of them will be determined (see sshdoers(5)). If similar authorization directives are encountered in the configuration, then a pattern that matches all of them will be determined. If there are any recent uses of such a pattern, the corresponding authorization directive that is output will not be commented out and will continue to allow all of the similar commands. If there are no recent uses of such a pattern, the corresponding authorization directive that is output will be commented out and will no longer allow any of the similar commands.

Please check that the output wouldn't disallow any command that hasn't been used recently but that nevertheless still needs to be allowed. That can happen if the log files aren't retained for long enough to capture infrequent but necessary commands.

If the --unlearn option encounters any interactive logins, they are ignored. If they are allowed by the current configuration, they will be included in the candidate configuration that is output, but they will be commented out. You can manually uncomment them to continue to allow interactive logins, but that's probably not a good idea.

If you need to permanently allow interactive logins, and still want to use sshdo, place the authorization directive(s) somewhere that won't be overwritten by subsequent uses of the --unlearn option.

Example use of the --unlearn option:

  # sshdo --unlearn --accepting > /etc/sshdoers.d/.learned
  # vim /etc/sshdoers.d/.learned # Verify that it is correct
  # sshdo --check /etc/sshdoers.d/.learned
  /etc/sshdoers.d/.learned syntax OK
  # mv /etc/sshdoers.d/.learned /etc/sshdoers.d/learned
-a, --accepting

The --accepting option affects the behaviour of the --learn and --unlearn options.

By default, the --learn option outputs commented out authorization directives for disallowed commands. With the --accepting option, these authorization directives are not commented out. In other words, commands that were disallowed in the log files will become allowed if the output is added to the current configuration.

It is not recommended to use the --accepting option with the --learn option without first inspecting the output of the --learn option without the --accepting option and verifying that all of the commented out authorization directives do indeed need to be allowed.

By default, the --unlearn option does not consider disallowed commands when determining the new candidate configuration that it outputs. With the --accepting option, it does consider them. In other words, commands that were disallowed in the log files, but that are allowed by the current configuration, will continue to be allowed if the output is used to replace the authorization directives in the current configuration.

It is recommended to use the --accepting option with the --unlearn option. It's safe in the sense that it won't introduce any additional authorization directives and not doing so might remove an authorization directive that was added recently to allow a command that has so far only appeared in the log files as a disallowed command. That can happen if training mode wasn't turned on before the new command was first attempted.

WHY?

Many systems use ssh keys for authenticating automated maintenance tasks such as remote backups. Normally, these keys are used to execute a small fixed set of commands. For fully automated use, the corresponding private keys will very likely be unencrypted so as not to require a passphrase to decrypt them before use. If such a private ssh key is compromised, the adversary can attempt to use it to execute arbitrary commands on any host where it is an authorized key.

The remote IP address might be controlled via a firewall or tcp wrapper (i.e. /etc/hosts.allow) or an AllowUsers directive in /etc/ssh/sshd_config or a from="" option in the ~/.ssh/authorized_keys file or all of the above, but if the adversary that compromises the private key is on the host where it resides, then remote IP controls don't help. They only prevent the adversary from copying the key to another host and using it from there.

The usual way to prevent an authorized key from being used for arbitrary command execution is by forcing ssh to execute a fixed command by using a command="" option in the ~/.ssh/authorized_keys file. But that is limited to forcing a single fixed command. If multiple commands are needed, then a separate authorized key would be needed for each command, or you might not bother using forced commands at all and just accept the risks instead.

sshdo makes it possible to use a single authorized key for any number of commands by specifying the set of allowed commands in separate configuration files. This means that even if a private key is compromised, the adversary can only use it to execute commands that are allowed for that key. It won't prevent denial of service by overusing those commands, but it can be of help in preventing post-compromise lateral movement by an adversary.

Even when ssh keys are used to authenticate human users, and their private keys are encrypted and do require a passphrase before use, or even if their private keys reside in FIPS 140-validated cryptographic modules, it might be desirable to limit those humans to executing only a fixed set of commands. After all, it's not only keys that can become compromised.

This also means that all of the policy relating to allowed commands can reside in a single file, /etc/sshdoers, or a small number of files, /etc/sshdoers.d/*, rather than being hard-coded into individual ~/.ssh/authorized_keys files. This might make it easier to audit your ssh infrastructure.

Also, by removing the actual forced commands from the keys in ~/.ssh/authorized_keys files, these keys can be installed as is on multiple hosts even where the commands that need to be executed are different on each of those hosts. The differences can be expressed in each host's /etc/sshdoers and /etc/sshdoers.d/* files, leaving the authorized keys the same on all hosts. This might make it easier to replace keys when they near the end of their cryptoperiod.

The hope is that sshdo will make it easy to start using forced commands where they are not used currently but could be. And training mode and the --learn and --unlearn options make it easy to achieve and maintain strict least privilege.

LOGGING

When used as a forced command, sshdo emits log messages to the auth syslog(3) facility by default. A different syslog facility can be specified in the configuration file (see sshdoers(5)). Log messages contain fields that look like: name="value". The type field can have the following values: "allowed", "training", "disallowed", "configerror" or "execerror". Log messages with type="allowed" are logged with the info priority. All other log messages are logged with the err priority. The following shows the fields that each type of log message can have:

  type="allowed"     user="..." remoteip="..." [label="..."] command="..." [group="..."] [config="..."]
  type="training"    user="..." remoteip="..." [label="..."] command="..." [group="..."] [config="..."]
  type="disallowed"  user="..." remoteip="..." [label="..."] command="..." [config="..."]
  type="configerror" user="..." remoteip="..." filename="..." error="..." [config="..."]
  type="configerror" user="..." remoteip="..." filename="..." linenumber="..." line="..." [config="..."]
  type="execerror"   user="..." remoteip="..." [label="..."] command="..." error="..." [config="..."]

The user field contains the name of the local user who is using sshdo as a forced command.

The remoteip field contains the remote IP address taken from the $SSH_CLIENT environment variable which is set by sshd(8).

The label field is included when the sshdo forced command has one or more command line arguments (e.g. command="/usr/bin/sshdo user@example"). It contains the command line argument(s) with any whitespace characters (e.g. " ") or colon characters (":") replaced with underscore characters ("_"). This can be used to distinguish between a user's multiple authorized keys and to identify the remote owner of each authorized key.

The command field contains the value of the $SSH_ORIGINAL_COMMAND environment variable which is set by sshd. It contains the command that was requested to be executed. Any leading or trailing whitespace characters in the command are removed. Any double quote characters (""") or backslash characters ("\") in the command are quoted with a preceding backslash character. Any binary/unprintable characters in the command are represented using hexadecimal notation (e.g. a newline character would be represented as "\x0a"). For interactive logins (i.e. where no command was requested), the command field contains "<interactive>".

The group field is included when the command was allowed because of the user's membership of a group. It contains the name of the group.

The error, filename, linenumber and line fields contain details about a configuration error (i.e. missing or unreadable file or syntax error) or an execution error (e.g. out of memory).

The config field contains the configuration file path that was specified with the --config option or by the $SSHDO_CONFIG environment variable. It is not included when the default configuration file is used.

DIAGNOSTICS

sshdo --check can emit the following success message to standard output (stdout):

  ... syntax OK

Or the following error and warning messages to standard error (stderr):

  error: Failed to read: ...
  error: Invalid config: ...
  warning: Invalid config: ...
  warning: No such user: ...
  warning: No such group: ...
  warning: No such banner: ...
  warning: No such logfiles: ...
  warning: No default log files: ...
  warning: Clashing training mode: ...
  warning: Clashing allow/disallow: ...
  warning: match specified more than once: ...
  warning: syslog specified more than once: ...
  warning: banner specified more than once: ...

sshdo --learn and sshdo --unlearn emit the following error message to standard error when unable to find any log files:

  error: No log files found: ...

sshdo --learn and sshdo --unlearn emit the following error message to standard error when unable to read a log file:

  error: Failed to read: ...

When invoked with the --accepting option, but without either the --learn or --unlearn option, sshdo emits the following error message to standard error:

  error: The --accepting option requires the --learn or --unlearn option

When invoked with mutually exclusive command line options (with the exception of the --help or --version option), sshdo emits an error message like the following to standard error:

  error: The --learn and --unlearn options are mutually exclusive

When invoked with the --config or -C option without its configfile argument, sshdo emits one of the following error messages to standard error:

  error: option --config requires argument
  error: option -C requires argument

When invoked with an invalid command line option, sshdo emits an error message like the following to standard error:

  error: option -? not recognized

EXIT STATUS

When sshdo is used as a forced command, and the requested command is allowed and executed, or training mode is turned on and the requested command is executed, the exit status is the exit status of the requested command.

When the requested command is disallowed, or when the user's login shell fails to execute, the exit status is 1.

When sshdo is invoked with the --check option, the exit status is 0 if the syntax is OK. Otherwise, the exit status is equal to the number of errors and warnings found (up to a maximum of 255).

When sshdo is invoked with the --learn or --unlearn option, and it is unable to find any log files, or an error occurs while trying to read a log file, the exit status is 1.

When sshdo is invoked with incorrect or mutually exclusive command line options (with the exception of the --help or --version option), the exit status is 1.

Otherwise, the exit status is 0.

ENVIRONMENT

When used as a forced command, sshdo uses the $SSH_ORIGINAL_COMMAND and $SSH_CLIENT environment variables which are both set by sshd(8).

The $SSHDO_CONFIG environment variable can be defined to specify the path to the configuration file to use instead of the default, /etc/sshdoers. This is mainly useful for interactive command line use of sshdo. For use in ~/.ssh/authorized_keys files, it's much easier to just use the --config option.

To specify a different configuration file via the $SSHDO_CONFIG environment variable for sshdo in a ~/.ssh/authorized_keys file, you need to define it either in a ~/.ssh/environment file or in an environment="" option in the ~/.ssh/authorized_keys file and you also need to enable sshd's environment processing by including a "PermitUserEnvironment yes" directive in /etc/ssh/sshd_config, but that's probably not a good idea.

Or you could just define it in the command="" option in the ~/.ssh/authorized_keys file (e.g. command="SSHDO_CONFIG=/usr/local/etc/sshdoers /usr/bin/sshdo"). But you might as well just use the --config option (e.g. command="/usr/bin/sshdo --config /usr/local/etc/sshdoers").

BUGS

No special effort is made to handle very long commands. The sshdo log messages that contain commands must be able to fit into a UDP packet to the syslogd(8)-compatible logging service on localhost. Fortunately, the MTU on the loopback interface is typically very large (e.g. 16KiB or 64KiB) so this shouldn't be a problem. However, if the log messages are forwarded to another host, use a modern logging system that won't lose anything. Or only use commands of a reasonable length. According to the syslog standard (RFC 5424), the maximum syslog packet length that you can rely on is 480 bytes, but larger packets might work. To be on the safe side, it is strongly recommended to only use commands that are no more than about 300 bytes in length. That should be more than enough. If a command were too long, its log messages would be truncated and so would not be recognized as sshdo log messages by the --learn and --unlearn options. Such excessively long commands would therefore require manually created authorization directives.

By default, the --learn option outputs a commented out authorization directive for similar commands when any of a user's uses of those similar commands were disallowed even if there were other uses that were allowed for training mode. If the --accepting option is also supplied in this situation, the authorization directive that is output will not be commented out. Personally, I think that if any of a user's uses of similar commands were allowed for training mode, then the authorization directive that is output should not be commented out and should allow all of the similar commands. I think that it should only be commented out if all of the user's uses of the similar commands were disallowed. However, I didn't want to make that decision on behalf of all system administrators. An old adage is called to mind: Any feature that can't be turned off is a bug. The chosen behaviour allows system administrators to decide for themselves whether or not the presence of any disallowed similar commands warrants disallowing all of the similar commands.

Most of the configuration directives may only appear in the main configuration file, /etc/sshdoers, not in /etc/sshdoers.d/* (see sshdoers(5)). This is intended to standardize sshdo's configuration, make it easier to audit, and to eliminate potential nasty surprises. But it does take choice away from system administrators and so is probably a mistake.

FILES

/etc/sshdoers - configures sshdo(8) and specifies allowed commands.
/etc/sshdoers.d/* - specifies additional allowed commands.
~/.ssh/authorized_keys - has ssh keys with command="/usr/bin/sshdo".
/var/log/auth.log - possible default destination for syslog messages.

SEE ALSO

sshdoers(5), ssh(1), sshd(8), sshd_config(5), syslog(3), syslogd(8), logrotate(8), RFC 5424.

AUTHOR

raf <raf@raf.org>