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:
- Prevents the allocation of pty for a terminal session.
- Allows only the execution of
internal-sftp
. - Chroots to the user’s home directory after the authentication.
- 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 :)