5

Using a authorized_keys forced command with ssh and a wrapper-script like this:

#!/usr/bin/env bash

case "$SSH_ORIGINAL_COMMAND" in
  /var/lib/authorized-scripts/*)
    $SSH_ORIGINAL_COMMAND
    ;;
  *)
    exit 1
    ;;
esac

Can a malicious user somehow chain another command after /usr/bin/authorized-scripts/ and therefore overcome this security measure or is this secure?

This suggests that I could simply use ssh user@host '/var/lib/authorized-scripts/script.sh && cat /etc/passwd', but this did not work in my tests.

Can I somehow improve the security of this script while still allowing multiple commands with user-supplied arguments for a single ssh key?

I am aware that the allowed commands themselves should ofcourse not allow any kind of subshells (so, find for example is a nogo for example due to its exec functionality).

2 Answers 2

10

Your forced command is unsafe. Consider:

ssh -l user your-server /var/lib/authorized-scripts/../../../bin/rm -rf /

The command name starts with /var/lib/authorized-scripts/ so it's safe, right? Nope!

You'll want something more along these lines:

#!/bin/sh

set -- $SSH_ORIGINAL_COMMAND
case "$1" in
  /var/lib/authorized-scripts/*)
    ;;
  *)
    exit 1
esac

command="${1#/var/lib/authorized-scripts/}"
shift
case "$command" in
  */*)
    # Simplest is to reject anything with a slash...
    exit 1
  .*)
    # ...and anything starting with dot.
    # If you need to whitelist subdirectories of /var/lib/authorized-scripts
    # then you need much more sophisticated pathname parsing and care.
    exit 1
  *)
    ;;
esac

exec "/var/lib/authorized-scripts/$command" "$@"

As for shell syntax like && and ; embedded in the command, that should be safe. As you've already discovered, such things are not interpreted by the shell in your solution but passed verbatim to the command.

However, wildcards in the arguments of the command will be interpreted. If you want to avoid that, put set -f before the line with set -- $SSH_ORIGINAL_COMMAND.

Make sure that the authorized scripts interpret their arguments in a safe way. The arguments could be options that allow shell injection (like find -exec does), file names, including symbolic links (which point to a file in a different directory), etc.

Finally: #!/usr/bin/env bash is not necessary. Your script uses no bashisms, so why not let be more portable by running under /bin/sh?

4
  • Thanks for discovering this! What about using shopt -s extglob and changing the case pattern to /var/lib/scripts/+([^/]))? There are 2 other commands that are allowed, so using this approach would shorten the script alot in comparison to your example. Are there any positive security effects in using set -- $SSH_ORIGINAL_COMMAND? You are using exec to run the script, I am pretty certain about what it does, but maybe you could add a short explanation to your answer. I'd be glad about some more explanation about why things like subshells or chaining commands will never work. Thanks again!
    – Zulakis
    Mar 28, 2016 at 10:45
  • Your extglob pattern may be good enough (but not compatible with /bin/sh) although it still allows executing /var/lib/scripts/.. which might be okay because it will fail anyway (executing /var/lib as a directory is nonsense) but IMHO EIBTI. As for set -- $SSH_ORIGINAL_COMMAND that's just so you can reliably split the command into command name and separate arguments; not a security effect per se. Finally, I don't know what you mean by "subshells or chaining commands"?
    – Celada
    Mar 28, 2016 at 10:53
  • What I mean is the question why embedding $(rm -rf /) in a command or appending && rm -rf / to a command will never work? Can you add a few bits about why you are using exec to your answer? You are right about explicit being better than explicit, I will just use /var/lib/scripts/[^./]*([^/]).
    – Zulakis
    Mar 28, 2016 at 11:04
  • For embedding shell syntax in commands, try it and see what happens: $(rm -rf /) does not get interpreted by the shell but gets passed verbatim to the external command as 3 literal arguments $(rm, -rf, and /). Since we assume the command sanitizes its arguments, it's safe. As for exec, I am just using it because it makes sense to run the final command in the same process, replacing the wrapper script instead of spawning a new process. It's not strictly necessary.
    – Celada
    Mar 28, 2016 at 11:11
1

[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 connections. It's available for download at:

http://raf.org/sshdo/ (read manual pages here)
https://github.com/raforg/sshdo/

It has a training mode to allow all commands that are attempted, and a --learn option to produce the configuration needed to allow learned commands permanently. Then training mode can be turned off and any other commands will not be executed.

It also has an --unlearn option to stop allowing commands that are no longer in use so as to maintain strict least privilege as requirements change over time.

It is very fussy about what it allows. It won't allow a command with any arguments. Only complete shell commands can be allowed so it might not meet your needs.

But it does support simple patterns to represent similar commands that vary only in the digits that appear on the command line (e.g. sequence numbers or date/time stamps).

It's like a firewall or whitelisting control for ssh commands.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .