sshdo - controls which commands may be executed via incoming ssh(1)
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]
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.
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
configfileThe --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.
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.
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.
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
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
.
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"
).
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.
/etc/sshdoers
- configures sshdo(8) and specifies allowed commands./etc/sshdoers.d/*
- specifies additional allowed commands./var/log/auth.log
- possible default destination for syslog messages.sshdoers(5), ssh(1), sshd(8), sshd_config(5), syslog(3), syslogd(8), logrotate(8), RFC 5424.
raf <raf@raf.org>