Site Backend

May 31, 2024

Table of Contents

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.

Editing this article with Vim and Firefox.

Editing this article with Vim and Firefox.

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:

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 to determine the default theme, and has a manual theme switcher
  • The menu bar collapes to a hamburger menu on mobile.

Here are a few articles which cover guidelines that I follow:

Images

Images are created as follows:

Other notes:

Managing images manually helps to keep the site small and fast. Here are a couple common GraphicsMagick commands:

# 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, deferred, and only used for the 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:

'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

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, 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 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:

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)
  • 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:

# unconditionally redirect to https://pablotron.org
<VirtualHost *:80>
  RewriteEngine On
  RewriteRule ^/(.*)$ https://pablotron.org/$1 [R,L]
</VirtualHost>

<VirtualHost *:443>
  # 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
  <FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|svg|js|json|css)$">
    Header set Cache-Control "max-age=31536000, public"
  </FilesMatch>

  # allow style-src-attr unsafe-inline for svgs
  # (without this svgs do not render in firefox)
  <FilesMatch "\.svg$">
    Header set "Content-Security-Policy" "default-src 'self'; img-src 'self'; style-src-attr 'self' 'unsafe-inline'"
  </FilesMatch>

  # expose webhook
  <Location /hooks/>
    ProxyPass "http://localhost:9000/"
    ProxyPassReverse "http://localhost:9000/"
  </Location>
</VirtualHost>

Download

HTTP Compression

HTTP compression is supported via mod_deflate.

It is safe for this site to enable mod_deflate because it does not use cookies and is not vulnerable to BREACH.

In 2022 I tried mod_brotli, but the improvement over 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 https://pmdn.org/ 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 and writing a custom Hugo table shortcode 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:

The remaining security headers are explained in the following articles:

The security headers on this site earn an A+ rating from securityheaders.com:

Security headers report for this site from securityheaders.com

Security headers report for this site from securityheaders.com

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. In particular, only TLS 1.2 and TLS 1.3 are enabled, and the insecure ciphers from TLS 1.2 have been disabled.

# 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

This TLS configuration earns an A+ result from the SSL Labs SSL Test:

SSL Labs SSL Test results for this site.

SSL Labs SSL Test results for this site.

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:

// 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

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 with inline styles disabled in order to support the strict Content-Security-Policy. See Security Headers for details.

Tables are are generated by a custom table shortcode, because the Hugo’s native Markdown table generator in Hugo uses inline styles.

I have also written a couple of custom shortcodes to generate <picture> and <figure> elements in order to support progressive enhancement.

Custom archetypes have been added for the Archives section, blog posts, articles, and projects.

The generated HTML has been modified to:

Webhook Configuration

webhook configuration with HMAC key removed:

[{
  "id": "deploy-pablotron-org",
  "execute-command": "/data/www/pablotron.org/git/bin/hook/deploy.rb",

  "pass-arguments-to-command": [{
    "source": "payload",
    "name": "time"
  }],

  "pass-environment-to-command": [{
    "source": "string",
    "envname": "DEPLOY_HTDOCS_PATH",
    "name": "/data/www/pablotron.org/builds/current"
  }, {
    "source": "string",
    "envname": "DEPLOY_REPO_DIR",
    "name": "/data/www/pablotron.org/git"
  }, {
    "source": "string",
    "envname": "DEPLOY_BUILDS_DIR",
    "name": "/data/www/pablotron.org/builds"
  }],

  "trigger-rule": {
    "match": {
      "type": "payload-hmac-sha256",
      "secret": "(omitted)",
      "parameter": {
        "source": "header",
        "name": "X-Hub-Signature"
      }
    }
  }
}]

Download

Validation

I periodically use the following tools to verify this site:

I also manually check the site in the desktop and mobile versions of Chrome and Firefox.

I am investigated doing automated validation with htmltest, htmltidy, and the W3C validator, but have not added them yet.

Other

I do not store credentials (e.g., the HMAC key for the deployment web hook) in the Git repository for this site.

Write access to the Git repository for this site is only accessible via SSH.

SSH access firewalled off and is only available via Wireguard.

Here are recommendations regarding SSH, in order of preference:

  1. Only allow SSH access via a VPN and do not expose it to the public.
  2. Only allow key-based authentication and disable password authentication. Use an Ed25519 key, if possible.
  3. Limit the IP addresses which can connect to SSH at the firewall.
  4. Always disable remote root logins.

Note that these options are not mutually exclusive.

I have written about firewall configuration and Wireguard elsewhere on this site:

Updates

This section contains a list of updates to this article it was initially published.