Skip to main content

Secure file transfer deployments with restricted SSH keys and rsync

This site is built in a GitHub Action, which means that I end up with a big folder of HTML files (and other stuff) which I need to get from the build environment onto my production server. To keep it simple, I wanted to use an SSH-based method to do that (so I don’t need to set up FTP or something), and rsync is a nice utility which fits the bill – it even handles deleting old files which aren’t needed any more.

I didn’t like the idea of supplying my Action with a set of keys which can access my entire server: that seems like too much power. Introducing rrsync, or restricted rsync, which allows you to limit where a given set of SSH keys can read and write. Let’s set it up.

  1. First of all, generate a set of SSH keys. I’m going to do that on my local machine using ssh-keygen. That should give you one file containing a private key and one containing a public key. We’ll need them in a moment.
  2. SSH into the server you want to deploy to. I’m going to create a new user without sudo access to handle the deployments. To do that on Ubuntu, it’s sudo adduser <username> and follow the instructions. Set the password to something long and random, we won’t be using it.
  3. Head to the deployment user’s home directory and open the file .ssh/authorized_keys in your favourite text editor. You might need to use sudo -u <deployment username> to do this if you’re not logged in as your deployment user, and you’ll probably also need to make the .ssh folder.
  4. Now we get to configuring what the deployer is allowed to access!

    rrsync relies on restricted SSH keys: as well as specifying permitted keys in the authorized_keys file, we also specify what those keys are allowed to do. You can read documentation about the kind of restrictions you can enact by running man authorized_keys. In our case, the authorized_keys file is going to look like this:

    restrict,command="/usr/bin/rrsync -wo /var/www/" ssh-rsa 3PLaw6dB0YpGi434Xz74BYXnancBhgLu08VXV4drLjVoaCM79u7fKW8NEQDFYojuMYLNkNkKLYoTfMT1mn5fmuqTHzTCaoFz3X1G3PLaw6dB0YpGi434Xz74BYXnancBhgLu08VXV4drLjVoaCM79u7fKW8NEQDFYojuMYLNkNkKLYoTfMT1mn5fmuqTHzTCaoFz3X1G label
    

    Breaking that down a bit, we first supply a comma-separated list of options. Make sure you don’t add any spaces! The first option, restrict, locks down the key completely by enabling every possible restriction (including those which don’t exist yet). Then, we use the command option to specify one command which this key is allowed to execute: in this case, that’s rrsync. Change /var/www/ to the directory you’d like to deploy to. -wo (write only) means the directory can only be used as an rsync destination, not a source (the opposite would be -ro, for “read only”). After that, it’s pretty standard: the key type and public key itself (which we generated in step 1), followed by a label (perhaps the name of the repository this key deploys for?).

That’s it for configuration! Now, you can copy the private key to your build environment (put it in ~/.ssh/id_rsa) and rsync your files over like this:

rsync -avz --del content-to-deploy/ <deployment-username>@<deployment-host>:/

You don’t need to specify the exact destination path – the path you specified when configuring the restricted SSH key is treated as the “root” directory. The -avz options mean the entire directory is recursively synced (including some file permission stuff), there’s more verbose output, and compression is used in transit. The --del option means files in the target directory which aren’t in the source directory will be deleted.

If you can’t put the private key into ~/.ssh/id_rsa, look into rsync’s -e option to specify extra options for the ssh command.

If you’re using GitHub Actions, here’s a snippet which might help. It syncs the _site directory to the server specified in the DEPLOY_HOST secret, with some extra ssh options so it can run unattended. The deployment username should be stored as a secret named DEPLOY_USER, and the private key stored in DEPLOY_KEY. Put this after your build step (and change the source directory from /_site if you need to):

- name: Deploy the site
  run: |
        mkdir ~/.ssh
        echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_rsa
        chmod 400 ~/.ssh/id_rsa
        rsync -avz --del -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=quiet" ${{ github.workspace }}/_site/ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/

Expanding on this idea, there’s other permissions you can grant to a specific SSH key, as documented in man authorized_keys. One option set I like is restrict,port-forwarding,permitopen="localhost:8000", which means that the key is only able to open a tunnel to port 8000 (like ssh -NL 8000:localhost:8000 host).


Next post:

Previous post: