Connor's Blog

Hardening OpenSSH Server for File Transfer

I recently tasked myself with hardening an sshd configuration on a machine used for file transfer via rsync. The machine in question is used by our internal software running on a large number of clients to push and pull files. Why our software uses the SSH protocol for this is something I won’t get into. The clients are considered partially trusted because they’re in our jurisdiction but because there’s such a large volume of them, it is good practice to treat them as malicious by default.

Switching off the scary defaults

The sshd config I went to take a look at had some scary defaults, bearing in mind that the machine is exposed to the Internet.

PasswordAuthentication yes
AllowRootLogin yes

Luckily we already use pubkey authentication so PasswordAuthentication could be changed to no, and we had no need for AllowRootLogin to be enabled.

Locking down logins

The way we have sshd configured is to listen on two ports, one of them being 22. We then only allow port 22 internally and expose the other port to the Internet. Some people like to use ports other than 22 as a form of obfuscation; personally I think it’s inconsequential. However, this enabled a nice little trick. We could lock down the config so that only our designated user for rsync clients can log in on the exposed port.

This is achieved using a Match block. These always need to go at the end of an sshd config.

Match LocalPort 9001
    AllowUsers rsyncuser
Match All

Disabling forwarding

OpenSSH can forward TCP and X11 connections. We don’t want our untrusted rsync to be able to do this, so we disabled it by appending DisableForwarding to the Match block:

Match LocalPort 9001
    AllowUsers rsyncuser
    DisableForwarding yes
Match All

Locking down shell access

It’s probably obvious that the biggest risk if a client is malicious and can successfully authenticate is that they’ll get a shell to do whatever they want on the machine.

PermitTTY can be added to prevent the client from getting a PTY:

Match LocalPort 9001
    AllowUsers rsyncuser
    DisableForwarding yes
    PermitTTY no
Match All

This doesn’t completely stop the client from executing commands though. Binaries can still be executed directly by appending arguments to ssh like so:

ssh user@machine /bin/echo hello

Although, in our case, we couldn’t completely prevent the user from executing commands because rsync and scp actually use a server-side component which is executed on connection. What we needed is a sort of basic sandbox to only allow specific commands.

There’s existing software out there specifically for this use case, but none of it seemed actively maintained or readily available.

Instead, I came up with this shell script:

#!/bin/bash

rsync()
{
    shift
    exec -c -- rsync "$@"
}

scp()
{
    shift
    exec -c -- scp "$@"
}

case $SSH_ORIGINAL_COMMAND in
    # Only allow rsync/scp to be executed on this server
    # Other commands will silently fail
    rsync*)
        rsync $SSH_ORIGINAL_COMMAND
    ;;
    scp*)
        scp $SSH_ORIGINAL_COMMAND
    ;;
esac

Note that silent failure is intentional - it’s better not to give an attacker an idea of why their command is rejected. Then, I added a ForceCommand directive which would cause this script be invoked when a client connects:

Match LocalPort 9001
    AllowUsers rsyncuser
    DisableForwarding yes
    PermitTTY no
    ForceCommand /etc/ssh/entrypoint.sh
Match All

Is this completely bulletproof? I doubt it. I can imagine a security hole that involves the backspace escape character, or uploading a file to /tmp, or something crazy like that. Hopefully it’s enough to keep bad guys out with a similar attention span to myself.