Simple Sourcehut Deployments

I started using Sourcehut for a project. It features a build system that is really simple, but very powerful, so I decided to use it to build and deploy the project’s artifacts to my server. I wanted something equally simple for my deployment solution, but restricted enough to make me not worry much about the consequence of any secrets being leaked to a third party.

Drew (the creator of Sourcehut) posted about Sourcehut’s solution to allowing shell access for build jobs, and I realized I could use a very similar system for my deployments.

In this post, we’ll go over all the steps to set up the same system to deploy new blog posts to this blog.

Overview

This blog uses Hugo to generate its page. When I finish writing a post and want to publish it, I push a commit with the new content to my Sourcehut repository. Sourcehut will automatically trigger a build job for the new commit, which will in turn download Hugo’s source, build it, then use the built binaries to generate this blog’s pages.

After that, the build job runs rsync to send the content to the server hosting this blog. The server restricts the build job to running only rsync, to prevent bigger problems if the key used by the build job ends up getting leaked.

Initial setup

Generate an SSH key to be used by the build job to connect to the server hosting the blog. The SSH key doesn’t need a password. I prefer ed25519 keys, but if you’re replicating this, feel free to pick whatever key type you prefer.

# Do this locally so you have easy access to the key.

ssh-keygen -t ed25519 -f ~/.ssh/blog_deployer

Server setup

We need to create a user specifically to deploy the contents of the blog. This user will only be able to rsync files for the blog.

# In the server hosting the blog.

useradd blog-deployer
# Lock the password because we won't be using it.
passwd -l blog-deployer
mkdir -p /home/blog-deployer/.ssh

# You'll need to manually add the contents of blog_deployer.pub to the authorized_keys file.
nano /home/blog-deployer/.ssh/authorized_keys

chown -R blog-deployer:blog-deployer /home/blog-deployer

We also want to restrict what the blog-deployer user can do in the server, to prevent any big accidents if the SSH key ends up leaking.

My initial solution involved a very complicated chroot jail, but it turns out there’s a simpler approach.

To restrict what the user can do, we’ll use a script called rrsync. I found this script when reading the rsync man page. Here’s a quote from it:

[…] the support directory of the rsync distribution has an example script named rrsync (for restricted rsync) that can be used with a restricted ssh login.

In an Ubuntu 19.04 server, you can find it in /usr/share/doc/rsync/scripts/rrsync. In older Ubuntu releases, it’s possible that this script will be gzipped, so you’ll need to decompress it first.

cp /usr/share/doc/rsync/scripts/rrsync /usr/local/bin/rrsync
# The script calls for this link. It's not really needed, but I'm creating it anyway.
ln -s /usr/bin/rrsync /usr/local/bin/rrsync

With the script in place, we can restrict the command used in the SSH session. Add the following in front of the content added to /home/blog-deployer/.ssh/authorized_keys:

restrict,pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,command="/usr/local/bin/rrsync -wo /var/www/blog"

The initial bits are just for ease of mind. The important part is here: command="/usr/local/bin/rrsync -wo /var/www/blog". By passing these arguments to rrsync, you’re telling it to only allow connections that push data to /var/www/blog. Additionally, /var/www/blog will be used as the root path for any files synced with rsync. We’ll see what that means during the repository setup.

Sourcehut setup

We need to add our SSH key to Sourcehut’s secrets vault. This will allow it to use this SSH key to deploy the new blog content.

Head over to the secrets page. Give the new secret a name, and paste the private SSH key content in that page. Select “SSH Key” as the secret type, and create it.

You need to use the private key because you want the build job to open an SSH connection for you. When I first used Sourcehut’s secrets, I accidentally pasted the public key (I was used to every other place asking for public keys only), and it took me a while to figure out why SSH wasn’t working from the build job.

After adding the secret, Sourcehut will show you the secret’s UUID. In my case, the UUID was c61add59-56b4-420f-93cb-b3a9a417cb0e. Note this UUID somewhere. We’ll need it for the next step.

Repository setup

In the repository, we need to create a file named .build.yml. This is one of the ways to tell Sourcehut to start a build job whenever a new commit is pushed to the repository.

This file is very simple, but if you want to learn how this build integration works, the documentation is here.

This is how my .build.yml looks:

image: alpine/edge
packages:
- go
- rsync
sources:
- https://git.sr.ht/~ky3ga39x/blog
secrets:
- c61add59-56b4-420f-93cb-b3a9a417cb0e
tasks:
- build-hugo: |
    git clone https://github.com/gohugoio/hugo.git
    cd hugo
    go install --tags extended
- build-blog: |
    cd blog/blog
    ~/go/bin/hugo -D
- deploy-blog: |
    cd blog/blog
    rsync --rsh="ssh -p 709 -o StrictHostKeyChecking=no -i ~/.ssh/c61add59-56b4-420f-93cb-b3a9a417cb0e" -rzP public/ blog-deployer@sidhion.com:/

Most of the options are straightforward. However, it’s worth noting two SSH arguments:

  • Using -o StrictHostKeyChecking=no is required, otherwise the build job will hang because it doesn’t recognize the server to connect to (and will ask for confirmation before proceeding).
  • Using -i ~/.ssh/<secret UUID> should be a best practice. In some more complex build definitions, you’ll likely be using many different SSH keys, so it’s just better to specify which key you want to use.

Also important is the destination of the rsync command: blog-deployer@sidhion.com:/. Notice how the path only specifies /. This is because back in the server, we told rrsync to use /var/www/blog as the root path of any file synced with rsync.

With all of this, things are done! Submit a new commit with these changes, and you’ll see Sourcehut starting a new build job which will automatically publish the latest content to the server.

Extras

This same trick can be used if more tasks are required after deploying build artifacts to a server. For example, in my project I’m building an application that is managed with systemd. After deploying build artifacts, I need to issue a systemctl restart app, so I created a second SSH key, and set the following command in the authorized_keys file:

command="sudo /usr/bin/systemctl restart app"

By also allowing passwordless sudo execution of this specific command with these specific arguments, all I need to do in the build job to trigger a restart of the app is:

ssh -i ~/.ssh/<secret uuid> -o StrictHostKeyChecking=no user@server

Conclusion

By now, you should be able to set up a very simple and flexible deployment solution using Sourcehut’s build system. I hope you learned something new with this!

So far I’m really liking the experience with Sourcehut. Since everything there is so simple and straight the point, it also encourages me to find simple solutions for the things I want to do.