www.axelknauf.de

Home > Git push publishing

January 16, 2024

Goal

Disclaimer: Security matters!

This post shows a working setup that can run in a trusted network setup. There are a variety of implications using this approach that allow certain attack vectors. Please to not simply copy this without thinking about your needs first.

There are possibly many more issues with this setup, so please be considerate in your own approach.

Preliminaries

I am using a RaspberryPi with Raspbian bookworm as base for this setup, but pretty much any Linux system will work as long as the necessary tools are available.

Required:

How to

Add a remote user and git repo

Set up a new user and basic software on the remote Linux box:

ssh sudoer@example.org
sudo adduser git
sudo apt update && sudo apt install podman git

You’ll use the password for pushing git commits to this remote. Alternatively, you can set up SSH keys.

Switch to the new user and set up git config and a bare git repo:

ssh sudoer@example.org
sudo su git
git config --global init.defaultBranch main
git config --global user.name "Some Name"
git config --global user.email "git@example.org"

cd ~
mkdir -p repos/somerepo.git
cd repos/somerepo.git
git init --bare

Set up access to web space

Then make sure that SSH access to the webspace is available for this new user. All of this needs to be run logged in as git@example.org:

# generate an SSH key for the git user
ssh-keygen -t ed25519 -C "git@example.org"

# log into remote web space once, checking and accepting the host key
ssh user@webspace-host
logout

# set up authorized key with the webspace host
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@webspace-host

# Try out logging in without having to give a password
ssh user@webspace-host
logout

It’s a good idea to keep backups of this key. I am not using a passphrase because I want the publishing process to be fully automated. But security matters.

Add the new remote to your local working copy

Then, locally, set up the new remote for your local git repo. In my case, I already have a git repo with a remote called origin where I keep my stuff. This repo contains markdown files that can be converted to a web page using hugo, a static website generator.

In your local git repo with all your content:

git remote add publish git@example.org:repos/somerepo.git

I called the new remote publish, but you can choose other names, if you like. Try pushing your main branch to this remote to see if it works:

git push publish main

You may want to set up exchanged SSH keys here, too. Or, depending on the git client you are using, maybe save the password to a keychain.

Interim result: We now have a second git remote we can push to. Now comes the magic.

Containerized publishing toolchain

I did not want to install all required tools on the remote host, but instead abstract the toolchain from it a bit. Esp. since hugo is not available as regular Debian package I went for a containerized solution. Alternatives could be nix-shell or devenv.sh.

Containerized build and publish script:

#!/usr/bin/env bash
set -euo pipefail

root="$(cd "$(dirname "$0")" && pwd)"
image=my-publishing-image

function docker-bootstrap() {
    echo "---------------------------------------"
    echo "Creating container with toolchain"
podman build -t $image - <<-EOF
    FROM alpine:3.19.0
    RUN apk add -U bash hugo rsync openssh
EOF
podman run --rm --network host -v "$root:/workspace" -v "/home/git/.ssh:/ssh:ro" -w /workspace $image "./${0##*/}" foo
}

function do-in-docker() {
    echo "---------------------------------------"
    echo "Building new version with hugo"
    hugo
    echo "---------------------------------------"
    echo "Publishing new version to webspace"
    rsync -rlptv \
        --delete \
        --rsh='ssh -i /ssh/id_ed25519 -o UserKnownHostsFile=/ssh/known_hosts' \
        public/ 'user@webspace-host:/path/to/htdocs/'
}

if [[ "${1:-}" = "" ]]; then
    docker-bootstrap
else
    do-in-docker
fi

exit 0

This script lives inside my git repo, so I can tweak and maintain it. It does a few things, so let’s take this apart:

Make sure this script is set to be executable!

chmod +x build-and-publish.sh

Then commit and push it to the remote.

Add a post-update hook on the remote

Now we need to set up the second part of the toolchain: What happens when something gets pushed to the publish remote? The goal is to:

As for the working copy, we’ll simply clone the bare repo locally (as in: on the remote machine):

ssh git@example.org
mkdir publish
cd publish
git clone /home/git/repos/somerepo.git

/home/git/publish/somerepo is now your (non-bare) working copy used for publishing. This is separate from the bare repo which you use as remote!

We’ll do the second step, the publishing toolchain, using a post-update hook on the git repo:

ssh git@example.org
cd repos/somerepo/hooks
vim post-update

The file post-update gets the following content:

#!/usr/bin/bash
set -euo pipefail

cd /home/git/publish/somerepo
env -i git pull -r
exec ./build-and-publish.sh

Make sure that this post-update script is executable!

chmod +x /home/git/repos/somerepo/hooks/post-update

The post-update hook will cd into the working copy, pull the latest files and then invoke the publishing script which does the hugo build and the rsync publishing step.

Important note: git hooks come with environment variables pre-set, that’s why we need to use env -i here to get a clean, new environment. Otherwise the git pull -r will not work at all.

Putting it all together:

If all pieces have been put together corretly, this is what happens:

By using a separate remote, I can choose when to backup my files on a git server (a regular git push that goes to origin) or trigger my publishing pipeline (git push publish main).

Thanks for reading!

Tags: