November 1, 2014
Engineers say the darndest things. For example: "I need to log in on these machines as the user X to run some commands", where 'X' is a service or system account not mapping 1-1 to a human being. The options to restrict the service account at least somewhat and not grant a full interactive shell are limited:
Enforce /sbin/nologin. You're basically saying "sorry, no can do". The account won't be able to log in to run rsync(1) or scp(1) to retrieve files. Users will need to log in with their own credentials and then use the service account via 'sudo -u X'.
This is great from a security perspective, since that keeps a full audit-log and eliminates the possibility of the service account being accessed remotely. It's rather not so great from the perspective of the engineer who has a legitimate need to run some commands automatically or unattended remotely.
Use sshd(8)'s ForceCommand (or 'command=' restrictions in the service account's public key). This ensures that a given public key can only be used to run one specific command. However, this still requires the account to have a valid login shell, even if its not used at login time. Setting the shell to /sbin/nologin will prevent sshd(8) from executing the forced command. That is, your service account now has a valid shell that could be invoked on the system, thus allowing such a process to run commands beyond those you authorized via ForceCommand.
A second problem arises if you need to grant some flexibility and want to allow more than one command, or perhaps a command with different possible options or arguments. ForceCommand (and 'command=') don't allow for this directly, so you'd have to create a shell script that inspects the SSH_ORIGINAL_COMMAND environment variable, determines whether or not that should be allowed and then executes it. This isn't terribly complicated, but it now requires you to also distribute this script. If you have multiple service accounts needing to execute distinct sets of commands, you now have to create and distribute multiple such commands.
Use a signature verifying shell, such as sigsh(1). This approach allows great flexibility, but incurs a notable overhead in usability, as all commands to be issued need to be signed. This works well for large-scale automated ad-hoc command-execution, but is a burden for semi-interactive or very simple use cases.
To address this problem, I wrote a limited shell, lish(1). This very simple, restricted command-line interpreter allows you to define a set of commands that all users of this shell are allowed to execute as well as to define additional commands on a per-user basis. lish(1) intentionally does not provide for I/O redirection, command-line history editing, or logical control operators. As such, it is primarily intended as a non-interactive shell, set either in /etc/passwd as the login shell of a service account, or invoked via sshd(8)'s ForceCommand / 'command=' restriction.
lish(1) does not grant elevated privileges or change the (effective or real) uid of the user running the commands. Any commands that are allowed to be executed will run with the uid of the invoking process, and thus will have all the privileges of that user. That is, lish(1) does not try to lock down a given command or otherwise restrict its use. General Unix access semantics continue to apply: if you add the command 'grep *' to your /etc/lishrc, then all users of lish(1) will be able to read all files to which their uid has read access.
lish(1) does not use any startup files beyond those provided by the system administrator in /etc/lishrc and /etc/lish/$USER, which should be owned by root and mode 0444 or possibly 0644. At startup, lish(1) will look for the SSH_ORIGINAL_COMMAND environment variable and, if found, try to execute its contents as a command (if permitted). Otherwise, it will read commands from the '-c' flag or, failing that, from standard in. lish(1) clears the environment at startup and explicitly sets only the PATH, USER, and SHELL variables.
Commands given may be either a single command, or a sequence of commands separated by semicolons. This sequence is iterated over unconditionally: any command that is permitted is executed, regardless of whether or not a previous command in a sequence was successful or not permitted to run.
lish(1) returns the exit status of the last command it executed or a status of 127 if the given command was not allowed.
Finally, lish(1) logs all commands via syslog(3) in the AUTH facility; commands that are not permitted, are logged at LOG_ERR level, commands that are permitted at LOG_INFO.
November 1, 2014