Trent Steenholdt's Blog

A simple blog sharing my findings, interests, and work in the Information Technology industry, as well as anything else that interests me.

From LAMP to GitHub Pages - Rebuilding My Blog (and 'securing' My Markdown)

trentsteenholdt
June 16, 2025

6 minutes to read

Disclaimer: This setup does not protect against brute force attacks. Anyone with access to the encrypted files in the public repository could theoretically attempt to crack them given enough time and computing power. If you’re following this approach, use a long, complex passphrase and understand that this is not as secure as using a private repository or a content management system. This is security through obscurity with some added complexity at best!

Blog site

In June 2025, I finally migrated my blog from an old (but reliable) WordPress setup running on a LAMP stack to this new, static site hosted on GitHub Pages using Jekyll. The move had been on my mind for a while, but like all migrations, it came with a mix of hesitation, motivation, and a fair bit of scripting.

Why I Left WordPress

I had been running WordPress for years — Apache, MySQL, PHP, and all. It worked well. But over time, maintaining the server became more of a chore than a joy. The LAMP box needed regular patching, plugin updates, the occasional Nginx + Let’s Encrypt adventure, and worst of all — me remembering what I last configured six months ago.

It was time to simplify.

GitHub Pages with Jekyll offered the low-overhead, zero-backend solution I was after. But one thing held me back…

The Privacy Problem

My blog has always had two audiences:

  1. The public internet (like this post)
  2. A more private side — posts I’d share with one or two people, or sometimes keep entirely to myself

With WordPress, private publishing was easy — just set visibility to “Private” or “Password Protected.” But GitHub Pages doesn’t support dynamic content, authentication, or access control. And GitHub only allows private repositories with a paid GitHub Pro license — and if you know me, paying to not share something felt a bit backwards.

So I got creative.

DIY Static Post Encryption

Instead of hiding posts behind authentication, I decided to encrypt the Markdown files themselves. The idea was simple:

  • Store encrypted versions of private posts in .encrypted/
  • Use a passphrase to decrypt them at deploy time
  • Build the site from the decrypted files in _posts/
  • Never commit decrypted content to Git

Here’s a snippet from my decryption script:

#!/usr/bin/env bash

set -uo pipefail

ENCRYPTED_DIR=".encrypted"
POSTS_DIR="_posts"

if [[ -z "${POST_DECRYPT_PASSPHRASE:-}" ]]; then
  echo "ERROR POST_DECRYPT_PASSPHRASE environment variable not set."
  exit 1
fi

mkdir -p "$POSTS_DIR"

for enc_file in "$ENCRYPTED_DIR"/*.enc; do
  base_name=$(basename "$enc_file" .enc)
  out_file="$POSTS_DIR/$base_name"

  if [[ -e "$out_file" && "$out_file" -nt "$enc_file" ]]; then
    echo "  Skipping $out_file is newer than $enc_file"
    continue
  fi

  openssl enc -aes-256-cbc -pbkdf2 -d \
    -in "$enc_file" \
    -out "$out_file" \
    -pass env:POST_DECRYPT_PASSPHRASE
done

And the reverse — for encrypting before committing:

#!/usr/bin/env bash

set -euo pipefail

ENCRYPTED_DIR=".encrypted"
POSTS_DIR="_posts"

for post_file in "$POSTS_DIR"/*.md; do
  base_name=$(basename "$post_file")
  enc_file="$ENCRYPTED_DIR/$base_name.enc"

  openssl enc -aes-256-cbc -pbkdf2 -salt \
    -in "$post_file" \
    -out "$enc_file" \
    -pass env:POST_DECRYPT_PASSPHRASE
done

Automating It All with GitHub Actions

At deployment time, a GitHub Actions workflow decrypts the posts using a secret stored in the repository:

- name: Decrypt posts
  run: |
    chmod +x scripts/decrypt_posts.sh
    POST_DECRYPT_PASSPHRASE="${`{ secrets.POST_DECRYPT_PASSPHRASE }`}" ./scripts/decrypt_posts.sh
  env:
    POST_DECRYPT_PASSPHRASE: ${`{ secrets.POST_DECRYPT_PASSPHRASE }`}

Then Jekyll builds and publishes the site as usual.

Local Dev with Tasks and Prompts

For local development, I created VS Code tasks that prompt for a passphrase and decrypt posts on the fly. From there, I can preview with jekyll serve like normal.

{
  "label": "Decrypt Posts",
  "type": "shell",
  "command": "bash scripts/decrypt_posts.sh",
  "options": {
    "env": {
      "POST_DECRYPT_PASSPHRASE": "${input passphrase}"
    }
  }
}

It’s a bit hacky, definitely not secure in any enterprise-grade sense, but it works. And critically, no decrypted content ever ends up in Git.

Client Side Protection

For the client side, I wanted to mimic WordPress-style password-protected posts. To achieve this, I took inspiration from Andrea Carriero’s guide and modified the plugin slightly to work directly on _posts files that include a password field in their front matter.

This lets each protected post have its own unique password, which is used in the browser to decrypt the page content using JavaScript. But just like the earlier disclaimer — there’s no brute force protection. A determined attacker could sit on the page and try passwords all day. There’s no backend, no rate limiting, no timeout. Remember it’s static javascript that’s protecting you!

Final Thoughts

Moving away from WordPress was always a concern for me because I wanted to retain control over what was public and what remained private.

Yes, I’ve traded a few GUI conveniences for command-line scripts and encrypted markdown, but in return I’ve gained peace of mind — and some free compute on my Hyper-V box that used to run the LAMP server. Win.

If you’re thinking of doing something similar and want to avoid paying for private repos, this is one way to do it with nothing but bash, openssl, and a bit of YAML glue.

I don’t call this encryption in the strict sense; I think of it more as “casual privacy.”