NAME

sshdo - configurable control for ssh authorized_keys forced commands

SYNOPSIS

    usage:
     sshdo [label]           # For use as a forced command
     sshdo [options]         # For 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.

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"). The label might or might not be used for the purpose of allowing commands. Either way, it will be included in log messages, if supplied.

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 a (probably invalid) command line option.

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.

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 file specified on the command line. sshdo also consults the files in the directory whose name matches the configfile name 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 all cases. The $SSHDO_CONFIG environment variable can also be used to specify the configuration file, but the --config option takes precedence.

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

The --check option performs a syntax check on 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 name matches the configuration file name 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 won't necessarily prevent sshdo from functioning. They will just be ignored. But that can mean that commands that should be allowed won't be or vice versa (if you mistype a user name, 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. They are only reported by the --check option.

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

The --learn option reads 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 disallowed or that were allowed for training mode. 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 into a file in /etc/sshdoers.d/.

Commands that were allowed for training mode will appear not commented out in the resulting configuration output so that they will become allowed. Commands that were 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 so that they too will become allowed.

If similar commands are encountered that only differ in the digits used in the command (e.g. sequence numbers or date/time stamps), then a pattern that matches all of them will be determined. If the --accepting option isn't also supplied, and some 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.

This makes 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. Then, add /usr/bin/sshdo as the forced command in your ~/.ssh/authorized_keys files. Then, some time later, use sshdo --learn to see what configuration is needed to allow recent activity. Then, if all is correct, 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. Please don't be tempted to fully automate the learning process. Always verify the output of the --learn option. An attack taking place during learning 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. It might be useful temporarily, though.

Example:

    # sshdo --learn > /etc/sshdoers.d/.learned
    # vim /etc/sshdoers.d/.learned # Check 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 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. 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, auth.log files might only be retained for four weeks. That might not be enough to rely on for the purpose of deciding which authorization directives are no longer needed.

However, if the system retains auth.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 current configuration with the output of the --unlearn option.

Authorization directives that have been used recently are output not commented out so that they will continue to be allowed. Authorization directives that have not been used recently are output commented out so that they will no longer be allowed. This can be used to replace the current configuration, safe in the knowledge that all uses of sshdo that appear in the log files will continue to be allowed, but nothing else will. However, the next paragraph describes a possible situation where it's not so clear cut.

If similar commands are encountered that only differ in the digits used in the command (e.g. sequence numbers or date/time stamps), then a pattern that matches all of them will be determined. If the --accepting option isn't also supplied, and some 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.

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

Please also check that the output wouldn't disallow any command that hasn't been used recently but 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 output candidate configuration 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 somewhere that won't be overwritten by subsequent uses of the --unlearn option.

Example:

    # sshdo --unlearn --accepting > /etc/sshdoers.d/.learned
    # vim /etc/sshdoers.d/.learned # Check 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, the commands that were disallowed in the log files will be allowed from now on if the output of the --learn option is added to the current configuration.

It is not recommended to use the --accepting option with the --learn option without first seeing 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 are allowed by the current configuration, will continue to be allowed if the output of the --unlearn option replaces the current configuration.

It is recommended to use the --accepting option with the --unlearn option. It's safe in the sense that it can'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 (e.g. if training mode wasn't turned on before the new command was first attempted).

However, when digit matching is involved, it can allow a new key to execute a command that was disallowed for that key but is allowed for other keys. But the chances are very good that this is what you want to happen.

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 tcpwrapper or the AllowUsers directive in /etc/ssh/sshd_config or the 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 trying to use it from there.

The usual way to prevent an authorized key from being used for arbitrary command execution is by forcing it to execute a fixed command using the 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.

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 be compromised.

sshdo makes it possible to use a single authorized key for any number of commands by specifying the set of allowed commands in a separate configuration file. This means that even if a private key is compromised, the adversary can only use it to execute commands that the key is allowed to execute. It doesn't prevent denial of service by over-using those commands, of course, but it's still an improvement.

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 every ~/.ssh/authorized_keys file. 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 file, leaving the authorized keys the same on all hosts.

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

LOGGING

sshdo emits log messages to the auth syslog(3) facility by default. A different syslog facility can be specified in /etc/sshdoers. Each log message contains several fields that look like: name="value". The type field can be "allowed", "disallowed", "training", "configerror" or "execerror". Each log message type can have the following fields:

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

The user field contains the name of the local user that owns the authorized key.

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 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 and contains the command that was requested to be executed. Any double quote characters (""") or backslash characters ("\") in the command will be quoted with a preceding backslash character. Any binary/unprintable characters in the command will be 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 group membership. It contains the name of the group in question.

The filename, linenumber, line and error fields contain details about a configuration error (i.e. syntax error or unreadable or missing file).

The config field contains the name of the configuration file that was specified with the --config option or 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: ...
    error: Invalid config: ...
    warning: Invalid config: ...
    warning: No such user: ...
    warning: No such group: ...
    warning: No such banner: ...
    warning: No such logfiles: ...
    warning: Clashing allow/disallow: ...
    warning: Clashing training mode: ...
    warning: match specified more than once: ...
    warning: syslog specified more than once: ...
    warning: banner specified more than once: ...

sshdo --learn and sshdo --unlearn can emit error messages to standard error when unable to read a log file:

    error: ...

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

    error: option -? not recognized

When invoked with mutually exclusive command line options (except for --help and --version), sshdo emits an error message like this to standard error:

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

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, then the exit status is the exit status of the requested command.

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

When sshdo is invoked with incorrect or mutually exclusive command line options, 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 or warnings found (up to a maximum of 255).

Otherwise, the exit status is 0.

ENVIRONMENT

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 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 also enable sshd(8)'s environment processing by including the PermitUserEnvironment directive in /etc/ssh/sshd_config.

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

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).

AUTHOR

raf <raf@raf.org>