Souji Thenria

Blog CI/CD

Summary: A short guide on how to set up CI/CD for a personal blog.

Created on:

-----

Some background: I wanted to store the data for this blog in a remote repository and build the site every time a change is pushed. To do this, I store the website on sourcehut, use the build service sourcehut provides to build the site, and send it via SSH/SFTP to my web server.

The Problem I was facing was that I needed to add the private key (used for the login on my server) to my Sourcehut account. In case I mess up something in my configuration or the key is exposed in some other way, whoever gets the key gets access to the account on my server. So, I wanted to restrict the user’s access as much as possible.

(A note: I did this setup on OpenBSD 7.5, but it should work on other systems using OpenSSH.)

Server Configuration

User creation

On the server, I created a user solely for uploading and deleting files for the website. For the purpose of this guide, the user is called _upload_user. The following snippet shows the user configuration.

login   _upload_user
passwd  *
uid     1002
groups  _upload_user
change  NEVER
class
gecos   Website upload
dir     /var/www/htdocs/example.com
shell   /sbin/nologin
expire  NEVER

Most notable is that the user’s home directory is placed in the path for the webserver /var/www/htdocs/example.com. Furthermore, the shell is set to /sbin/nologin because the user does not need a shell to upload files using SFTP.

OpenSSH configuration

In the OpenSSH configuration file /etc/ssh/sshd_config, I placed the following code snipped:

Match User _upload_user
    PermitTTY no
    ForceCommand internal-sftp
    ChrootDirectory %h
    AuthorizedKeysFile /etc/ssh/authrized_keys/_upload_user

For the user _upload_user the configuration:

  1. Prevents the allocation of pty for a terminal session.
  2. Allows only the execution of internal-sftp.
  3. Chroots to the user’s home directory after the authentication.
  4. Specifies where the public key is located for that user.

Chrooting the session to the user’s home directory prevents him from accessing the rest of the system. However, the entire path, including the home directory itself, must be owned by root and not writeable by anyone else. This should not be an issue on OpenBSD since the path is already owned by root, and the permissions for all directories in that path are drwxr-xr-x and are owned by root:daemon.

Since the user cannot write in its own home directory because it is owned by root, I created a data directory inside the user’s home which is owned by the _upload_user and where the user can write.

I did not want to include the AuthorizedKeysFile in the user’s home directory because it is located in the chrooted web server environment, and I felt uncomfortable putting it there.

Sourcehut build configuration

This section might be very specific to sourcehut; however, the configuration is relatively straightforward and can be applied to other sites like GitHub or GitLab.

The following is the .build.yml file I created to build and upload the website:

image: freebsd/latest
packages:
  - gohugo
  - lftp
sources:
  - <git repository>
environment:
  deploy: _upload_user@example.com
secrets:
  - <SSH key>
tasks:
  - build: |
      cd website
      hugo
      chmod -R 755 ./public
  - transfer: |
      cd website
      lftp --password "none" -e "set sftp:connect-program 'ssh -o StrictHostKeyChecking=no -i ~/.ssh/<SSH key>'; mirror -Re ./public ~/data; quit" sftp://$deploy

I had an issue with the SFTP upload, so I wanted to take some time to explain my configuration. The problem I was facing was that if I removed a file in my git repository, I obviously wanted to delete it also on my server, so my idea was to just remove everything in the entire web directory and upload everything anew. However, the SFTP client provided by OpenSSH does not allow the removal of directories when the directory still includes some files. This means I would either need to write a script to recursively remove all files and directories or look for another SFTP client. I chose the latter option.

After some hours of searching and experimenting, I found lftp to be the perfect fit. It not only supports the removal of entire directories, even if the directory includes files, but it also supports a mirror function, which can act very similar to rsync in the sense that if a file does not exist in the source directory but in the destination one, it is removed from the destination. This is exactly what I was looking for :)

Tags: