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.
- 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. - 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. - Head to the deployment user’s home directory and open the file
.ssh/authorized_keys
in your favourite text editor. You might need to usesudo -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. -
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 theauthorized_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 runningman authorized_keys
. In our case, theauthorized_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 thecommand
option to specify one command which this key is allowed to execute: in this case, that’srrsync
. Change/var/www/
to the directory you’d like to deploy to.-wo
(write only) means the directory can only be used as anrsync
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
).