Skip to content

Building a Secure GitHub Pages Pipeline

Estimated Time: 10–15 min Skill Level: Intermediate   Goal: Private → Public GitHub Pages Automation

Learn how to build and secure a GitHub Pages deployment pipeline where a private repo automatically publishes to a public repo using deploy keys, GitHub Actions, and MkDocs.
This walkthrough covers setup, DNS configuration, security hardening, and validation — all in one guide.

You can follow step-by-step or jump directly to the part you need:

OverviewReposKeysSecretsWorkflowPagesDNSValidation


Overview

Stage Description
1 Create private and public repositories
2 Generate and configure deploy keys
3 Add secrets and limit access scope
4 Automate builds with GitHub Actions
5 Serve the site using GitHub Pages
6 Connect your custom domain
7 Validate and secure the setup

Create the Repos

Separate your source code from your public output.

Purpose Repo Name Visibility
Source / MkDocs site d3fnd-site-src Private
Published content D3FND Public
Why separate them?

Keeping source and output in different repos reduces exposure and simplifies access control.
- The private repo holds your Markdown, config, and workflow.
- The public repo only contains generated HTML, CSS, and assets.
- Even if someone cloned your public site, they’d see only static content — not source logic or secrets.


Generate Deploy Keys

We’ll use SSH deploy keys instead of personal access tokens (PATs) — safer, scoped, and easy to rotate.

Generate a key pair

ssh-keygen -t ed25519 -C "pages-deploy" -f d3fnd_pages_deploy_key -N ""
Why not use a passphrase?

GitHub Actions runs in a non-interactive environment.
A passphrase would block the workflow from unlocking the key — the runner can’t prompt for input.
The key itself is stored securely as an encrypted secret, so a passphrase isn’t needed here.

Files generated

d3fnd_pages_deploy_key       # private key
d3fnd_pages_deploy_key.pub   # public key

Assign the keys

  • Public key → D3FND (public repo) → Settings → Deploy Keys → Add Key → Allow write access
  • Private key → d3fnd-site-src (private repo) → Settings → Secrets → Actions → New Repository Secret → Name: DEPLOY_KEY

Configure Secrets & Permissions

Security Model
  • Private repo holds your workflow and DEPLOY_KEY secret.
  • Public repo only accepts commits signed by that deploy key.
  • No PATs, no org-wide tokens — least privilege only.

This creates a clean separation:

Private Source → GitHub Actions → Public Site


Create GitHub Action Workflow

Goal

Automate the MkDocs build → publish flow.

Create a file:
.github/workflows/deploy.yml

name: Build & Publish (MkDocs → D3FND.github.io)

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install dependencies
        run: |
          pip install mkdocs-material mkdocs-minify-plugin mkdocs-git-revision-date-localized-plugin

      - name: Build site
        run: mkdocs build

      - name: Deploy to public repo
        uses: peaceiris/actions-gh-pages@v4
        with:
          deploy_key: ${{ secrets.DEPLOY_KEY }}
          external_repository: D3FND/D3FND
          publish_dir: ./site
          publish_branch: gh-pages
What this workflow does
  1. Watches for commits to main
  2. Installs MkDocs dependencies
  3. Builds static files under /site
  4. Uses the deploy key to push to your public repo’s gh-pages branch

Configure GitHub Pages

Go to your public repo (D3FND) → Settings → Pages

  • Source: Deploy from branch
  • Branch: gh-pages
  • Folder: / (root)
Why not 'GitHub Actions'?

“GitHub Actions” is meant for builds that occur in the same repo.
Since our Actions run from the private repo, we must point Pages to a deployed branch.


Set DNS & HTTPS

If you’re using a custom domain (Squarespace, Cloudflare, or Google Domains):

Record Type Name Value
A @ 185.199.108.153
A @ 185.199.109.153
A @ 185.199.110.153
A @ 185.199.111.153
CNAME www d3fnd.github.io
DNSSEC

DNSSEC can break GitHub’s verification.
Temporarily disable it, wait for your domain to resolve, then re-enable if your provider supports GitHub’s DNSSEC integration.

Expected result

Visiting https://andrewzamora.io or https://www.andrewzamora.io should load your MkDocs site with HTTPS enforced.
You can toggle “Enforce HTTPS” once the certificate is issued automatically by GitHub.


Validate & Secure

Validate the deployment

  • Check GitHub Actions logs — look for “Published to gh-pages”
  • Visit your custom domain
  • Test both root and www versions

Secure your setup

  • Rotate your deploy key periodically
  • Add branch protection rules on main
  • Limit who can modify workflow files
  • Enable Dependabot to auto-update Actions dependencies
Bonus — monitoring ideas
  • Use GitHub’s “Check DNS” button in Pages settings
  • Add a status badge in your README:
    ![pages-build-deployment](https://github.com/D3FND/D3FND/actions/workflows/deploy.yml/badge.svg)
    

Why This Approach Works

This is essentially a mini CI/CD pipeline:

Private Source (Markdown + Config)
   ↓
GitHub Actions (Automated Build)
   ↓
Public Repo (Static Output)
   ↓
GitHub Pages (Hosting)

Secure
Automated
Easy to maintain and audit


TL;DR

Keep your source repo private
Use a public repo only for static files
Deploy via SSH key (no passphrase)
Pages → “Deploy from branch /root”
DNSSEC off, HTTPS on
Document everything

Automation is only as strong as its access model.


Join the Discussion

Got a question, idea, or a better way to do it? Drop it below — I read every comment and update guides based on real-world feedback.

Add something useful. Ask good questions. Help someone else learn.