---
slug: "site-backend"
title: "Site Backend"
# date (optional for articles)
date: "2024-05-31T03:57:18-04:00"
# show on articles page
show: true
# uncomment to show table of contents
toc: true
pics:
editing:
css: "image"
tip: "Editing this article with Vim and Firefox."
sources:
- "/files/articles/site-backend/0-editing-1024.webp"
- src: "/files/articles/site-backend/0-editing-1024.png"
width: 1024
height: 640
ssl-labs-report:
css: "image"
tip: "SSL Labs SSL Test results for this site."
sources:
- "/files/articles/site-backend/1-ssl-labs-20240530-1024.webp"
- src: "/files/articles/site-backend/1-ssl-labs-20240530-1024.png"
width: 1024
height: 525
securityheaders-report:
css: "image"
tip: "Security headers report for this site from securityheaders.com"
sources:
- "/files/articles/site-backend/2-securityheaders-1024.webp"
- src: "/files/articles/site-backend/2-securityheaders-1024.png"
width: 1024
height: 320
---
## Overview
Site content is managed as [Markdown][] files stored in a [Git][]
repository. The theme is a modified version of [Bulma][]. The site is
statically generated by [Hugo][].
I create and edit site content with [Vim][] and preview the results
locally with `hugo serve -D ...`. Articles and blog posts are saved as
drafts and committed to the [Git][] repository so I can work on them as
time permits.
[{{< pe-figure "editing" >}}][editing-this-article]
## History
From 1998 to 2019 this site was generated by a custom [PHP][] backend,
written by me. In 2019 I resurrected the site after a several year
hiatus and migrated the site from the custom [PHP][] backend to
[Jekyll][]. In 2021 I switched from [Jekyll][] to [Hugo][].
## Goals
The content and layout of this site should be:
- Small
- Fast
- Secure
- Accessible
- Mobile-friendly
These goals are addressed as follows:
- Small: [Hugo][] is configured to aggressively pack content and minify
assets. Images are compressed and served in multiple formats. [HTTP
compression][http compression] is enabled (with caution). See
[Hugo Configuration][s-hugo-configuration], [Images][s-images] and
[Apache Configuration][s-apache-configuration].
- Fast: Static content can be served quickly. Assets are fingerprinted
and browser caching is enabled. See [Hugo
Configuration][s-hugo-configuration] and [Apache
Configuration][s-apache-configuration].
- Secure: Static content; no web-accessible endpoint which can make
changes. Additional security measures are discussed in
[Deployment][s-deployment], [Apache
Configuration][s-apache-configuration], and [Other][s-other].
- Accessible: Addressed when creating content and with custom
[shortcodes][shortcode]. See [HTML][s-html] and [Hugo
Configuration][s-hugo-configuration].
- Mobile-friendly: Addressed when creating content and with the site
theme. See [HTML][s-html] and [Bulma
Configuration][s-bulma-configuration].
## Content
This section describes how content is created for this site.
### HTML
The site content and layout is designed to be accessible and
mobile-friendly:
- links and table components have `title` and `aria-label` attributes
- images have captions, fallback formats, and `title` and `alt` attributes
- images scale gracefully to thumbnails on mobile
- the site supports [dark mode][], checks the [color scheme
preference][prefers-color-scheme] to determine the default theme,
and has a [manual theme switcher][post-theme-switcher]
- The menu bar collapes to a hamburger menu on mobile.
Here are a few articles which cover guidelines that I follow:
- [Progressive Enhancement][]
- [Why your website should be under 14kB in size][14kb]
- [5 things you don't need JavaScript for][you-dont-need-js]
### Images
Images are created as follows:
- Mathematical content is created using [Mathy][], saved as [SVG][],
minified using [minify][], and served cached and compressed by [Apache][].
- Charts are created with [Matplotlib][], saved as [SVG][], minified
with [minify][], and served cached and compressed by [Apache][]
Example: [This Python script][bench-chart] was used to generate the
bar charts in [this blog post][post-fips203ipd].
- Lossless raster images (e.g. screenshots) are scaled and cropped with
[GraphicsMagick][] and served as a lossless [WebP][] with a [PNG][]
fallback compressed by [pngquant][].
- Lossy raster images (e.g. pictures) are scaled and cropped with
[GraphicsMagick][] and served as a lossy [WebP][] with a [JPEG][]
fallback.
Other notes:
- The animated site logo is an [SVG][] generated by [this Ruby script][gen-logo.rb].
- Menubar icons are borrowed from [Bootstrap Icons][].
- I reviewed several [PNG][] compressors in [this post][post-png-compressors].
Managing images manually helps to keep the site small and fast. Here
are a couple common [GraphicsMagick][] commands:
```sh
# scale with antialiasing and convert from png to webp
gm convert -antialias -scale 1024x1024 some-image.{png,webp}
# get image dimensions
gm identify some-image.png
# convert from png to webp (lossless)
gm convert -quality 100 -define webp:lossless=true some-image.{png,webp}
# crop image to 256x256
gm convert -crop 256x256 some-image{,-cropped}.png
```
### JavaScript
This site has almost no [JavaScript][], by design. The [JavaScript][]
that is used is minimal, [modern][es2015], [deferred][], and only used
for the [theme switcher][post-theme-switcher] and burger menu.
Below is the unminified `script.js` for this site with some notes
removed. It is served as 777 bytes minified and 586 bytes minified and
compressed:
```js
'use strict';
//
// script.js - script which handles:
//
// - set theme
// - theme switcher and burger menu event handlers
//
const D = document,
C = D.body.parentElement.classList,
L = localStorage,
M = window.matchMedia,
on = (el, id, fn) => el.addEventListener(id, fn);
// use theme if set, otherwise fall back to browser preference
if (L && L.theme && L.theme === 'dark') {
C.add('dark'); // theme set to "dark"
} else if ((!L || !L.theme) && M && M('(prefers-color-scheme: dark)').matches) {
C.add('dark'); // prefers dark color scheme
}
document.addEventListener('DOMContentLoaded', () => {
// theme toggle event handler
on(D.querySelector('.navbar-item[data-id="theme"]'), 'click', (e) => {
e.preventDefault(); // stop event
L.theme = C.toggle('dark') ? 'dark' : 'light'; // toggle
});
// iterate through burgers, bind to click events
D.querySelectorAll('.navbar-burger').forEach(e => on(e, 'click', () => {
// then toggle is-active on burger and menu
[e, D.getElementById(e.dataset.target)].forEach(
e => e.classList.toggle('is-active')
)
}));
});
```
[Download][script.js]
## Deployment
Once I am ready to apply any outstanding changes to the public web site,
I push the outstanding commits to a remote [Git][] repository. This
triggers a [`post-receive` Git hook][post-receive], which sends an
[HMAC][]-authenticated `POST` request to a [web hook][] endpoint on the
web server.
The [web hook][] verifies the [HMAC][] and then runs [a deployment
script][deploy.rb] which does the following:
1. Verifies the authenticated timestamp (to prevent [replay attacks][]).
2. Clones the upstream repository.
3. Executes [Hugo][] (`hugo --minify -d ...`) to build the site in an
isolated output directory.
4. Updates the `htdocs` symlink for the public-facing web site to point
at the output directory from the previous step.
5. Removes any stale builds.
## Configuration
This section discusses the configuration for [Apache][], [Bulma][],
[Hugo][], and [Webhook][].
### Apache Configuration
This section disusses the [Apache][] configuration for this site. The
information is divided into several sub-sections in order to make it
easier to digest.
This site relies on the following [Apache][] modules:
- [mod\_deflate][mod-deflate]: Enable [HTTP compression][].
- [mod\_http2][mod-http2]: Enable [HTTP/2][].
- [mod\_macro][mod-macro]: Simplify common configuration.
- [mod\_proxy][mod-proxy]: Proxy [web hook][] requests to internal
[webhook][] daemon.
- [mod\_rewrite][mod-rewrite]: Unconditionally redirect from [HTTP][] to
[HTTPS][], strip `www.` from the path hostname, and redirect from legacy
[URLs][url].
#### Virtual Host Configuration
The [Apache][] virtual host configuration is modified as follows:
- Unconditionally redirect from [HTTP][] to [HTTPS][]
- Unconditionally redirect to strip the `www.` hostname prefix
- Enable [HTTP/2][]
- Set security headers (discussed in [Security
Headers][s-security-headers])
- Enable aggressive caching of image, script, and style assets
Below is the [Apache][] virtual host configuration for this site with
extraneous comments and configuration for logging, [TLS][], and legacy
redirects removed:
```apache
# unconditionally redirect to https://pablotron.org
RewriteEngine On
RewriteRule ^/(.*)$ https://pablotron.org/$1 [R,L]
# strip "www." prefix and enable mod_deflate
Use STRIP_WWW https://pablotron.org
Use MOD_DEFLATE
# enable http2
Protocols h2 http/1.1
# set restrictive content security policy
Header append "Content-Security-Policy" "default-src 'self'; img-src 'self' https://pmdn.org"
# set remaining security headers
Header append "Strict-Transport-Security" "max-age=31536000"
Header append "X-Frame-Options" "SAMEORIGIN"
Header append "X-Content-Type-Options" "nosniff"
Header append "Cross-Origin-Opener-Policy" "same-origin"
Header append "Cross-Origin-Resource-Policy" "same-origin"
Header append "Access-Control-Allow-Origin" "https://pablotron.org"
Header append "Referrer-Policy" "strict-origin-when-cross-origin"
# set permissions policy
Header append "Permissions-Policy" "camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=()"
# POST needed for /hooks
Header append "Access-Control-Allow-Methods" "POST, GET, HEAD, OPTIONS"
# cache images, stylesheets, and javascript for 1 year
Header set Cache-Control "max-age=31536000, public"
# allow style-src-attr unsafe-inline for svgs
# (without this svgs do not render in firefox)
Header set "Content-Security-Policy" "default-src 'self'; img-src 'self'; style-src-attr 'self' 'unsafe-inline'"
# expose webhook
ProxyPass "http://localhost:9000/"
ProxyPassReverse "http://localhost:9000/"
```
[Download][apache-vhost.conf]
#### HTTP Compression
[HTTP compression][] is supported via [mod\_deflate][mod-deflate].
It is safe for this site to enable [mod\_deflate][mod-deflate] because
it does not use [cookies][] and is not vulnerable to [BREACH][].
In 2022 I tried [mod\_brotli][mod-brotli], but the improvement over
[mod\_deflate][mod-deflate] was minimal (deflate: 125k, brotli: 117k) so
I abandoned it.
#### Security Headers
This site uses a strict [Content-Security-Policy][] header; it rejects
links to all external assets with one exception: references to images
hosted on are allowed.
The trickiest part of restricting [Content-Security-Policy][] was
`style-src`; many content generation tools (including the [Markdown][]
table generator in [Hugo][]) break without `style-src 'unsafe-inline'`
(e.g., the ability to emit arbitrary `style` attributes).
In order to work around the `style-src` issues I ended up [reconfiguring
the Hugo syntax highlighter](#hugo-configuration) and writing [a custom
Hugo table shortcode][hugo-shortcode-table] which only emits `class`
attributes for inline styling.
The journey to a strict [Content-Security-Policy][] is documented in
this series of blog posts from October 2021:
- [Hugo/Content-Security-Policy Impedance Mismatch (October 19, 2021)][post-headers-1]
- [TLS and Header Fixes (October 21, 2021)][post-headers-2]
- [The Nuclear Option (No More unsafe-inline) (October 25, 2021)][post-headers-3]
The remaining security headers are explained in the following articles:
- [Security headers quick reference][security-headers-quick-ref]
- [Goodby Feature Policy and hello Permissions Policy!][permissions-policy]
- [Permissions Policy Explainer][]
- [Referrer-Policy][]
The security headers on this site earn an A+ rating from
[securityheaders.com][]:
[{{< pe-figure "securityheaders-report" >}}][securityheaders-report]
#### TLS Configuration
The certificate for this site is issued by [Let's Encrypt][] and managed
automatically by [certbot][] with the [certbot-dns-linode][] plugin.
The [Apache][] [TLS][] configuration is based on the intermediate
settings from the [Mozilla SSL Configuration Generator][ssl-config]. In
particular, only [TLS 1.2][] and [TLS 1.3][] are enabled, and the
insecure ciphers from [TLS 1.2][] have been disabled.
```apache
# explicit list of cipher suites (from ssl-config.mozilla.org)
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# use server priorities for cipher algorithm choice
SSLHonorCipherOrder on
# protocols to enable (only TLS 1.2 and TLS 1.3)
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
```
[Download][apache-tls.conf]
This [TLS][] configuration earns an A+ result from the [SSL Labs SSL
Test][ssl-labs-ssl-test]:
[{{< pe-figure "ssl-labs-report" >}}][ssl-labs-report]
### Bulma Configuration
The site [CSS][] based on the [Bulma][] with the following
modifications:
1. All unused components removed
2. monokai style for [Chroma][] added
3. Styles for navbar icon highlighting and table captions added
4. dark mode styles added
Here is the [SASS][]:
```sass
// style.sass: based on bulma-0.9.3/sass/bulma.sass with the following
// changes:
//
// 1. all unused components removed
// 2. monokai style for chroma added
// 3. styles for navbar icon highlighting and table captions added
// 4. dark mode styles added
@charset "utf-8"
// import chroma style
//
// generated with the following command:
//
// cd themes/hugo-pt2021/assets
// hugo gen chromaclasses --style=monokai > chroma.css
//
@import "chroma"
@import "bulma-0.9.3/sass/utilities/_all"
@import "bulma-0.9.3/sass/base/_all"
// elements
@import "bulma-0.9.3/sass/elements/button"
@import "bulma-0.9.3/sass/elements/container"
@import "bulma-0.9.3/sass/elements/content"
@import "bulma-0.9.3/sass/elements/image"
@import "bulma-0.9.3/sass/elements/table"
@import "bulma-0.9.3/sass/elements/title"
@import "bulma-0.9.3/sass/elements/other"
// components
@import "bulma-0.9.3/sass/components/media"
@import "bulma-0.9.3/sass/components/navbar"
// grid (reenabled, used for images)
@import "bulma-0.9.3/sass/grid/_all"
// helpers
@import "bulma-0.9.3/sass/helpers/_all"
// layout
@import "bulma-0.9.3/sass/layout/section"
@import "bulma-0.9.3/sass/layout/footer"
// dim navbar icons by default
.navbar-item .menu-icon
opacity: 60%
// highlight icons on hover
.navbar-item:hover .menu-icon
opacity: 100%
// table captions below tables
table.table
caption-side: bottom
// dark mode (2024-05-27)
@import "dark"
```
[Download][style.sass.txt]
With these changes the generated [CSS][] is 137 kB minified and 16 kB
minified and compressed.
### Hugo Configuration
[Hugo][] is configured to use [Chroma syntax higlighting][chroma] with
inline styles disabled in order to support the strict
[Content-Security-Policy][]. See [Security Headers][s-security-headers]
for details.
Tables are are generated by [a custom table
shortcode][hugo-shortcode-table], because the [Hugo's][hugo] native
[Markdown][] table generator in [Hugo][] uses inline styles.
I have also written a couple of custom [shortcodes][shortcode] to
generate `