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:
Overview • Repos • Keys • Secrets • Workflow • Pages • DNS • Validation
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_KEYsecret. - 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
- Watches for commits to
main - Installs MkDocs dependencies
- Builds static files under
/site - Uses the deploy key to push to your public repo’s
gh-pagesbranch
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:

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.
FeedbackAdd something useful. Ask good questions. Help someone else learn.