mandag 3. januar 2011

Restrict ssh access to one command, but allow parameters

Sometimes one needs to allow a script to login to a server using a SSH key to do a job. That can be achieved by adding the scripts SSH public key to the remote user's authorized_keys file. The private keys are often stored without password to allow the script to use the key and not stopping execution by an interactive prompt for the private key password.

This puts the remote server at risk because if the originating server is compromised an attacker would easily gain access to the remote server as well. To reduce the damage of a compromised private key, one often restricts the access of the key to the minimum required to get the script's job done. This can be accomplished by using SSH-key options in the authorized_keys file.

from="",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/home/user/bin/" ssh-rsa  AAAAB3NzA..Dxq=

This is quite effective, and restricts the attacker to executing the specified forced command, and only from a specified host, with no terminal or X11 access.

Other times one needs to do a sync job using rsync. Let's see how that works out with the above scheme.

Example command:
/usr/bin/rsync /some/dir/

Example entry in authorized_keys:
from="",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/usr/bin/rsync" ssh-rsa  AAAAB3NzA..Dxq=

Trying that, you'll soon noticed that all your command line arguments are ignored, and you'll get the help text from rsync as output. The forced command option does not allow arbitrary command line arguments to the command.

There is a workaround by using the $SSH_ORIGINAL_COMMAND environment variable that the ssh program creates. The problem with that is that you'll need a wrapper script to deal with the arguments because it will have the executable twice in the argument list:
from="",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/usr/bin/rsync $SSH_ORIGINAL_COMMAND" ssh-rsa  AAAAB3NzA..Dxq=

This will produce this command:
/usr/bin/rsync /usr/bin/rsync /some/dir/

A solution to this is the wrapper script that interprets the environment variable and executes the correct command:
    "/usr/bin/rsync "*)
        echo "Permission denied."
        exit 1

This adds complexity and requires a script to be installed on the remote server. It is also possible to achieve a similar solution without the wrapper script:

from="",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/usr/bin/rsync ${SSH_ORIGINAL_COMMAND#* }"

Notice the '#* ' right after SSH_ORIGINAL_COMMAND. The # modifier in bash is used to remove the smallest prefix pattern, and so '#* ' will remove everything up until and including the first space.

There are some security concerns however. For instance this would be possible from the machine "":

rsync authorized_keys user@server:/home/user/.ssh/authorized_keys

That would allow an attacker to swap the file with anything, and thus the security is easily breached. To prevent that,  make the file owned by root and remove write permissions on the file, or make it immutable with chattr.

chown root:root /home/user/.ssh/authorized_keys
chmod a-w       /home/user/.ssh/authorized_keys
chattr +i       /home/user/.ssh/authorized_keys

Allowing the unrestricted use of the rsync program will allow an attacker to replace any files the user has write access to. Beware that there probably are other holes in this that I didn't find.

6 kommentarer:

Anonym sa...

nice work

Unknown sa...

couldn't you then execute a command like 'rsync whatever ; doNastyThings' ?

Anonym sa...

this is quite secure wrapper script. I put lots of debug in so you see what's happening

(html pre tag not possible, so no idea how this gets formatted)

# to execute a command you have to replace all spaces separating parameters
# with a # character (or change that in the $separator)
# example: ssh server 'ls#-l#my file with spaces#;#pwd#;#is not executed'
if [ "$1" != "ls" ]
echo "only ls is allowed"
echo "executing: '${1}' '${2}' '${3}' '${4}' '${5}' '${6}' '${7}' '${8}"
for i in "$@"
echo "> $i"
echo "run:"

larstobi sa...

João Antunes: The semicolon would be passed as an argument to rsync.

Anonym sa...

The reason you cannot pass rsync parameters is that the command option in the authorized_keys file is not a filter, it is the actual command which gets run. The command option doesn't just restrict which commands the remote user can execute.

An alternative approach would be to pass the environment variable $SSH_ORIGINAL_COMMAND through a regexp and execute the result. This looks ugly as hell, but it works:

command="`echo $SSH_ORIGINAL_COMMAND | grep '^rsync --server -[vlDtprzea]\+\.[iLs]\+ \(--delete\) \. /var/remote_backups\(/[a-Z0-9_-]\+\)\+\(/\|\.[a-Z]\+\)\?$'`"

The gives you increased flexibility and security:
* It only allows the rsync command
* It allows only a restricted set of rsync command line parameters, which you can set.
* It restricts the directory into which rsync can write. So no need to protect ~/.ssh/authorized_keys

The downside is the spectacularly ugly grep which is difficult to read and interpret.

Anonym sa...

[Disclosure: I wrote sshdo which is described below]

There's a program called sshdo for doing this. It controls which commands may be executed via incoming ssh.

It's available for download from (read manual pags here) and from (GPLv2+).

It has a training mode to allow all commands and a --learn option to construct the configuration needed to allow the encountered commands in future. Then training mode can be turned off and any unknown commands after that will not be executed. It also has an --unlearn option to remove commands from the configuration that are no longer in use to maintain strict least privilege as requirements change over time.

It's easier and safer than hard-coding allowed commands in a shell script.

It's secure because the system administrator has to verify and install the configuration. It's fussy about what it allows. It only allows complete commands. It doesn't allow arbitrary arguments but it does support limited pattern matching to represent commands that vary only in the digits that appear on the command line (e.g. sequence numbers and date/time stamps).