--- 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 `` and `
` 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: - Use a custom theme. See [Bulma Configuration][s-bulma-configuration]. - Add support for [``][go-import]. - Add a [Mastodon][] `` tag. - Remove all unnecessary tags. - Combine, [minify][], and enable caching of [JavaScript][] and [CSS][] assets. - add `integrity` attributes to `` and `