OpenSSH port-forwarding only account
OpenSSH is a very powerful and flexible tool (If you didn't know it, go ahead and visit http://openssh.org to learn more about it).
Today I had to give access to some localhost-only service to an external user in one of my servers. I've used SSH port forwarding for quite some time now, I'm familiar with it and I thought it would be the better approach. Using port forwarding I wouldn't have to "open" that locahost-only service to the Internet and all the traffic between the external user and the service will pass through the Internet encrypted.
Let me explain you how I achieved that, because It was trickier than expected.
For the purpose of this article, this was tested using FreeBSD 8.2 and OpenSSH 5.4p1 both in the server and the external user box, but it would work with any other Unix-based OS (other BSDs, Linux, OSx, etc)
First - Create the user account
Obviously, if we want the external user to be able to connect to our server, first we need to create an account for him.
In FreeBSD, we can create the account using adduser, passing the needed parameters as a string:
echo "sshguest::::::Guest account:/home/sshguest:/usr/sbin/nologin:" | sudo adduser -w random -f "-"
The username is sshguest (use whatever you like instead) and we let adduser to asign automatically the UID and GID values for this new user (a new group will be created using the username value as the new group name).
We have set /home/sshguest as the home of the user because a home folder will be needed later (to store ssh public key authentication data) but you can set the home for this user to wherever you like in the filesystem (/tmp/sshguest, /var/tmp/sshguest could be good places for that)
As you probably noticed, In the example above we have set the shell to /usr/sbin/nologin, because we don't want the user to have shell access to our server.
Finally, we passed the "-w random" parameter to adduser so the password for this account would be automatically generated. The adduser tool creates some really strong passwords, so this is a good idea (and we are not going to use password-based authentation anyway).
Second - Get the public key for SSH authentication
Now that we've the account, we have to ask our external user for a public key to enable public key authentication.
If your external user has no idea about what public key authentication is (I would bet there is 90% posibilities of that) first point him to the OpenSSH documentation (RTFM!) and then tell him that he can generate the needed keys using, for example, the following command:
ssh-keygen -t rsa -b 2048
Two files will be generated, he has to send you the one called id_rsa.pub
(Of course, you can generate the keys yourself and send him the private key, instead of asking him for the public one)
Third - Enable public key authentication
Once we've received the key, we have to put that key in the file .ssh/authorized_keys inside the sshguest user (if you choose that name in the first place). In our previous example, it would be something like:
# cat /home/sshguest/.ssh/authorized_keys ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAMEAyjtJMRtejQaDafYfaL+qc9LrhqHhAQnc7TwF/M3BDTJrZ9ucXLejRwvq/OOcRSsOnUGQyYc2w/TbKQE77cHzujd+PBKgdnQfu8hjJ2Q8to+q8gAEJEOscvLwAACq74llZBmD8gHWBXP6vthkPTFZyd7fkEsKkSgGFiWCZBl+YkibLoRrJpuk462XzPWC4KkOjiUwtJ+2PP1ZvMYYvxnWn+IxwQtbDqwDhY3DnN1dYTeUPT1umtrCCtzQjjynHFFb someuser@example.net #
Note: we have to create the .ssh directory within the sshguest home directory and we need to be sure proper permissions are applied to both the directory and the file:
mkdir /home/sshguest/.ssh echo id_rsa.pub > /home/sshguest/.ssh/authorized_keys chown -R sshguest /home/sshguest/.ssh chmod 700 /home/sshguest/.ssh chmod 600 /home/sshguest/.ssh/authorized_keys
Once we have added the authorized_keys file, we can test the authentication connecting from the external user box (or asking him to try):
ssh sshguest@example.net
What the user will see in the shell/console will be something like:
$ ssh sshguest@example.net
Copyright (c) 1980, 1983, 1986, 1988, 1990, 1991, 1993, 1994
The Regents of the University of California. All rights reserved.
FreeBSD 8.2-STABLE (GENERIC) #1: Tue Apr 12 12:27:33 CEST 2011
[ ... skip ... ]
This account is currently not available.
Connection to example.net closed.
$
What happened here? Well, the SSH connection was opened, authentication procceded correctly and the user's shell was loaded. As the shell was set to /usr/sbin/nologin, the system refuses to give a shell to the user and simply drops the connection.
Which, in the end, was what we were looking for, wasn't it?
Four - Fixing the shell problem
Well, that was what we wanted from the beginning, if it would allow us to use SSH's port forwarding features.
Sadly, that's not the case. If we try to open an SSH connection doing some port forwarding, the connection will be dropped too, and the tunnel will not be created:
$ ssh -L 8080:localhost:80 sshguest@example.net [ ... skip ... ] This account is currently not available. Connection to example.net closed. $
This happens because we have set /usr/sbin/nologin as the shell for the sshguest user. That, at least in FreeBSD, disables that user logins.
We have to replace it but replacing it with sh, tcsh or bash is not an option (we don't want to give shell access to this user) so we need a fix for that.
One option would be to set a real shell (like sh, tcsh or bash) and then restrict the commands the user will be able to execute. But, as I've said, I don't like the idea of the user being able to start a shell, even if we restrict the comments he will be able to execute later.
Another option, which I personally prefer, is to create a small script that will act as the shell for this user, restricting access completely.
This is a small python script that will do the trick:
#!/usr/local/bin/python
command = ''
while command != 'exit':
command = raw_input('Type the "exit" command to close the connection: ')
The script will wait for user input and, when the user provides the "exit" command, the script will stop execution and the SSH connection will be closed.
We could save this script inside the home of the sshguest user:
mkdir /home/sshguest/bin # then put the contents of the script inside /home/sshguest/bin/shell chown -R sshguest /home/sshguest/bin chmod -R 700 /home/sshguest/bin
and then we could use vipw to modify the user entry, from:
sshguest:$1$ICdMwdEn$3d3X83jdyT8ifLPnl8D8x/:1117:1117::0:0:Guest account:/home/sshguest:/usr/sbin/nologin
to:
sshguest:$1$ICdMwdEn$3d3X83jdyT8ifLPnl8D8x/:1117:1117::0:0:Guest account:/home/sshguest:/home/sshguest/bin/shell
Important: These lines above apply for my current example, in your case it would be different because of the password hash and the UID/GID values.
Obviously, while the python script is being executed (with minimal resources usage) the SSH connection will be kept opened and then the SSH tunnel will work.
$ ssh -L 8080:localhost:80 sshguest@example.net [ ... skip ... ] Type the "exit" command to close the connection:
If the user provides the "exit" command, the connection is dropped:
Type the "exit" command to close the connection: exit Connection to example.net closed. $
Conclusions
This is a quick hack. As you can imagine, the script could be improved a lot. We could provide a more complete shell-like interface so our external user can perform certain operations on the server or the connection.
One thing I miss here is the posibility to restrict port forwarding too, allowing the external user to create tunnels only to, for example, a given set of ports or even only one port.
BTW, this hack helped me a lot this morning, I hope it will help any of you one day.