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.