Oct 4, 2022 Tags: meta
This is the first “meta” post I’ve done since 2018, when I updated the blog to support IPv6.
Unlike those changes, these ones aren’t really visible to you (dear reader). But I figured I’d announce them anyways, as both a curiosity and potential reference to other Jekyll users1.
This is a Jekyll blog; it’s been one since its inception (nearly a decade now).
It used to be hosted on GitHub Pages, but has been self-hosted since 2018. My main reason for that was HTTPS support2, although control of the entire deployment has afforded me other liberties.
In particular, the blog has grown a couple of extensions (as custom Jekyll plugins):
I use a LaTeX engine to render equations and expressions in some posts (I really should use something like MathJax, but I’ve successfully avoided any JavaScript thus far.)
I have a couple of plugins to generate (and cache) “preview” thumbnails
for a handful of services, allowing me to approximate the <iframe>
user experience without the accompanying privacy (and stability) concerns.
For example, for YouTube and asciinema:
1
{% youtube https://www.youtube.com/watch?v=cw1k2WSAMhg %}
produces:
and
1
{% asciinema https://asciinema.org/a/524144 %}
produces:
Finally, I have a handful of plugins for other repetitive formatting tasks, including:
centered containers for pre-formatted text
Tables from raw CSV data:
foo | bar | baz |
---|---|---|
69 | 420 | 1312 |
…and so forth.
These are all relatively small things, but they add up in terms of productivity and a pleasant blogging experience.
They also add up in terms of dependencies: I couldn’t go back to GitHub pages even if I wanted to, and a complete rebuild of the blog3 requires all kinds of third-party utilities (ImageMagick and LaTeX, among them).
As a result, writing for my blog on new machines is a bit of a pain. Thus, the plan: to move blog deployment (the actual final HTML generation and copying to my server) to a CI provider.
The current deployment workflow, from my deskop, is a shell script that (more or less) does the following:
1
2
3
4
5
# generate the site's HTML
jekyll build
# sync with the remote site
rsync -avz --delete --progress _site/ blog:/var/www/html/blog.yossarian.net/
The goal was to take that and stuff it into GitHub Actions4. That means:
rsync
, including access restrictions.It turns out this was really easy: the
ruby/setup-ruby
gave me just about everything
I needed, including automatic bundle
invocation and caching.
With four lines of YAML, I had all the (Ruby) dependencies I needed:
1
2
3
4
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
bundler-cache: true
rsync
This part was a little more involved: I wanted to reuse my existing deployment
strategy (rsync
over SSH), but without sharing an overly broad SSH key
to my server.
To begin, I created a brand new ed25519 keypair:
1
ssh-keygen -t ed25519 -f blog-deploy
On the server side, I used two settings in my .ssh/authorized_keys
to restrict the key’s
use:
The restrict
setting, which disables all of SSH’s normal forwarding techniques as well as PTY allocation
and a few sources of command execution, like ~/.ssh/rc
(a file I didn’t know about)!
The command=
setting, which specifies the singular command that gets run after successful key authentication.
The latter setting composes well with rrsync
, or
“restricted rsync”. rrsync
does exactly what it sounds like it does: it restricts
the underlying command (supplied by the SSH daemon via SSH_ORIGINAL_COMMAND
)
to an rsync
invocation with a small handful of restricted flags.
Put together, this results in an .ssh/authorized_keys
line that looks a big like this:
1
command="/usr/bin/rrsync -wo /var/www/html/blog.yossarian.net/",restrict ssh-ed25519 LONG-KEY-HERE william@blog
The end result: if all functions correctly, someone who manages to steal my CI’s SSH key will
be unable to do anything besides execute rsync
and, even then, be unable to do anything
other than write to the directory my blog is in. In other words, they could deface the
blog, but that’s about it.
To actually use my new blog-deploy
key in GitHub Actions I tried to start
with a ssh-agent
setup, but ran into all kinds of problems with passing
the right environment variables around. I eventually gave up and did things
the silly way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: configure SSH
env:
SSH_KEY: $
run: |
mkdir -p ~/.ssh/
echo "${SSH_KEY}" > ~/.ssh/blog-deploy.key
chmod 600 ~/.ssh/blog-deploy.key
cat >>~/.ssh/config <<END
Host blog
HostName HOST
User william
IdentityFile ~/.ssh/blog-deploy.key
StrictHostKeyChecking no
END
(I disabled StrictHostKeyChecking
because, for these limited purposes,
a MITM attack is not a serious concern of mine. An attacker, at worst,
would only be able to see the files that will immediately become public anyways.)
From there my deployment script only needed one tweak:
1
2
3
4
5
6
# If we're running in GitHub Actions, rsync is restricted to the right directory.
if [[ -n "${CI}" ]]; then
rsync -avz --delete --progress _site/ blog:.
else
rsync -avz --delete --progress _site/ blog:/var/www/html/blog.yossarian.net/
fi
In English: rrsync
changes directories to the directory specified with -wo
and concatenates any requested paths to the working directory, so the original
invocation fails because no such nested directory exists. The “fix” is simply
to sync at the host’s current directory in those cases.
Put together, this all results in a relatively tidy GitHub Actions workflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
on:
workflow_dispatch:
inputs:
dry-run:
description: "Perform a dry run"
required: true
default: "false"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1
bundler-cache: true
- name: configure SSH
env:
SSH_KEY: $
run: |
mkdir -p ~/.ssh/
echo "${SSH_KEY}" > ~/.ssh/blog-deploy.key
chmod 600 ~/.ssh/blog-deploy.key
cat >>~/.ssh/config <<END
Host blog
HostName HOST
User william
IdentityFile ~/.ssh/blog-deploy.key
StrictHostKeyChecking no
END
- name: deploy
run: |
if [[ "$" = "true" ]]; then
echo "[+] dry run; not deploying"
else
echo "[+] deploying"
./deploy.sh
fi
and the deploy.sh
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash
set -eo pipefail
installed() {
cmd=$(command -v "${1}")
[[ -n "${cmd}" ]] && [[ -f "${cmd}" ]]
return ${?}
}
bundle check || bundle install
bundle exec jekyll build
[[ -d _site/ ]] || { echo "No _site dir?"; exit 1; }
# If we're running in GitHub Actions, rsync is restricted to the right directory.
if [[ -n "${CI}" ]]; then
rsync -avz --delete --progress _site/ blog:.
else
rsync -avz --delete --progress _site/ blog:/var/www/html/blog.yossarian.net/
fi
With all this, I can trigger blog deployments at any time, including dry runs:
With any luck, this will be a strong foundation for many years of blogging ahead.
If there are any others left. ↩
I think GitHub Pages supports HTTPS for custom subdomains now, but I haven’t checked. The last time I tried, the “standard” way to achieve that was to use a CDN company’s free tier to provide HTTPS. ↩
Which I almost never need to do, thanks to asset caching. ↩
Since GitHub is where the blog’s source is currently stored, and GitHub Actions is the CI platform I know best. ↩