Services over SSH

2024-10-21

Update 2024-10-23: My ISP at home decided to screw my IPv6 setup and now they don't know (or don't care) to fix it, so the examples on this article will be offline for the time being.

Intro

Most people use SSH for two main applications, maybe three. That is remote login and, to a lesser extent, remote command execution and secure tunneling. But the main implementation of the protocol, OpenSSH, has a lot more features and capabilities.

I had this idea long time ago of providing services over SSH. This is not new, of course, The OpenBSD project does it for their anonymous CVS. Git (and the plethora of software forges that support it) have the capability of use SSH as a transport protocol. Same goes for Mercurial and many others. I guess we're so used to that, that we don't even think about it. Chances are that, if you transfered files via rsync lately, it was over SSH too.

SSH provides traffic encryption, but also authentication and identification which could be useful in some scenarios when providing a service.

So I though of a couple example applications:

But any program that interacts with stdin and stdout could be set up as a service to run over SSH.

Again, this is not new, there are BBSs running over SSH. And I know the people at Bitreich use that all the time for their fine services, to name some.

This article just provides examples of what can be done, so you can crate your own stuff.

Are they useful ? Probably not.

Would they have performance issues at scale ? For sure.

But this is just for fun :-)

Implementation

The key ingredient here is the ForceCommand configuration keyword for the SSH server.

According to the man page:

ForceCommand
    Forces the execution of the command specified by ForceCommand
    ignoring any command supplied by the client and ~/.ssh/rc if
    present ...

The idea would be to have a user per service, and a block like this on sshd_config:

Match User anonsvc
    AllowTcpForwarding no
    X11Forwarding no
    PermitTunnel no
    AllowAgentForwarding no
    PermitEmptyPasswords yes
    PasswordAuthentication yes
    ForceCommand /usr/local/bin/test_ssh_service

That would allow connection with an empty password. Useful for a public, "anonymous" service. At the same time, it restricts what this user can do to only execute the command referenced in ForceCommand.

One could even pass "commands" or "instructions" to the service program. Again, according to the man page:

The command originally supplied by the client is available in the
SSH_ORIGINAL_COMMAND environment variable.

So one could parse that environment variable to get all sorts of data from it. Stretching a bit the silly idea I pointed out before of a crude API, a client could execute ssh srv1@myhost.tld getItem 1234. The SSH_ORIGINAL_COMMAND environment variable when the SSH server executes the ForceCommand process, would contain getItem 1234. The process could parse that data and return to stdout the information about that item.

If, instead of PermitEmptyPasswords yes, we go with the default, or even allow only connections with SSH keys, one can have an authenticated service. Using the AuthorizedKeysCommand keyword, one can define an alternate command to get the authorized SSH keys, instead of the traditional $HOME/.ssh/authorized_keys file. That program could look up on a relational database or whatever you fancy. This is kind of important when one has a large number of SSH keys to authorize, as the traditional authorized keys file would grow quite large, and it would be a pain to maintain.

The program referenced on AuthorizedKeysCommand only has to return zero or more SSH keys one per line. One neat trick is to pass the fingerprint of the key offered by the SSH client to the server to the command, so it can filter out the key we're interested in, so the command returns just one key instead of the potentially many that would be stored on the database or what would be read from a file. To accomplish that, one can pass the %f token to the command (which will expand to the key fingerprint) in combination with the %u token (which expands to the user name). Like so: AuthorizedKeysCommand /usr/local/bin/my_auth_command %u %f

In order for the ForceCommand to work, the user must have a valid shell, as the SSH server will execute the shell to force the command execution. As we're exposing this to the internet, it's always nice to add a little bit of extra protection.

This can be done via chroot(2). The SSH server has a config keyword for that, ChrootDirectory. The user will be locked up on the directory specified by that config keyword.

In order to do this correctly, a set of requisites have to be met though.

Finally, add the necessary config to the Match block from before: ChrootDirectory /data/ssh_chroot (of course, set there the path for the chroot jail you prepared).

Keep in mind that all paths from that point are relative to the root of the jail, not the root of the system, so you'll have to copy the command from ForceCommand into the jail too. And you'll need to compile it statically or add to the jail any library or shared object your program expects to find.

The authorized_keys file goes on the actual home of the user, not into the jail, as the chroot happens AFTER authentication. Something to have in mind too if one uses that for authentication.

Examples

Those are offline at the moment, see above.

I've prepared a couple of silly test services to demostrate all this. I'm not sure for how long I'll keep them online. They will fall victim of all kinds of attacks for sure, so if you find they don't work is because I got tired of blocking abusers.

The first one is as basic as it gets. Anonymous service, no password or key required. It prints a message and closes the connection:

ssh anonsvc@playground.e1e0.net (IPv6 only. Sorry, not sorry).

You should see something like this:

Hi stranger !
I see you're calling from xxxx:xxxx:xxxx:xxxx::xxxx
Is it nice over there ?

Here, there's a UUID for you. Have fun.
912daa33-34c8-49fa-96f1-28d8858c8d96

Connection to playground.e1e0.net closed.

Try to send also some commands when making the connection. The message will change a bit.

The other one is a bit more elaborate. It requires SSH key authentication so, if you want to try it, send me an email with an SSH public key and I'll authorize you. But basically is the minimum implementation of the comment service I mentioned before. It goes something like this:

$ ssh comments@playground.e1e0.net j2afx8 my_nick
Hi my_nick !
Write here your message.
Lines should be less than 256 chars long and there's a max of 50 lines
To end the comment.  Insert a line with a single period '.'

Hey !
This is quite silly !

.


Your comment has been saved.  Thanks !

The program expects an "article ID" as the first argument and a nick as the second one. After that, interatively asks for a message. End with a line starting with a dot.

The comment will be added to the article in a text file:

---
Host: xxxx:xxxx:xxxx:xxxx::xxxx
Author: my_nick
Date: 2024-10-21 17:27:29 UTC

Hey !
This is quite silly !

I did not bother to fully implement this and publish the comments on the web, but you can see how it could work.

Conclusion

And there you have it. A way to expend your afternoon :-)

Please be aware of what you expose to the internet and the consequences of it.

Have fun.

Have any comments ? Send an email to the comments address.