aboutsummaryrefslogtreecommitdiff
path: root/content/articles/site-backend.md
blob: 3ea2b38a61985d68384f81a9e48e828966784312 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
---
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
<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][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 <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](#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 `<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:

- Use a custom theme.  See [Bulma Configuration][s-bulma-configuration].
- Add support for [`<meta name='go-import' ...>`][go-import].
- Add a [Mastodon][] `<link rel='me' ...>` tag.
- Remove all unnecessary tags.
- Combine, [minify][], and enable caching of [JavaScript][] and [CSS][] assets.
- add `integrity` attributes to `<link>` and `<script>` tags.

### Webhook Configuration

[webhook][] configuration with [HMAC][] key removed:

```json
[{
  "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][webhook.conf]

## Validation

I periodically use the following tools to verify this site:

- Developer Console: Check page load time, cached and uncached page size.
- [Lighthouse][]: Check accessibility, desktop score, and mobile score.
- [Security Headers][securityheaders.com]: Check [HTTP][]
  security headers.
- [SSL Labs SSL Test][ssl-labs-ssl-test]: Check [TLS][] configuration.

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][git-repo].

Write access to the [Git repository for this site][git-repo] 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][ip] 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:

- [NFTables Examples][]
- [Wireguard is Awesome][]

## Updates

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

- 2024-06-03: [Apache Configuration][s-apache-configuration]: Make
  [SVGs][svg] render properly in [Firefox][] by adding a `<FileMatch>`
  directive for [SVGs][svg] which relaxes [Content-Security-Policy][] by
  allowing `style-src-attr 'unsafe-inline'`.  Without this exception
  inline [SVGs][svg] work fine, but viewing them directly just shows a
  black box.

[s-hugo-configuration]: #hugo-configuration
  "Hugo Configuration"
[s-security-headers]: #security-headers
  "Security Headers"
[s-images]: #images
  "Images"
[s-deployment]: #deployment
  "Deployment"
[s-html]: #html
  "HTML"
[s-bulma-configuration]: #bulma-configuration
  "Bulma Configuration"
[s-apache-configuration]: #apache-configuration
  "Apache Configuration"
[s-other]: #other
  "Other"
[hugo]: https://gohugo.io/
  "Hugo static site generator."
[bulma]: https://bulma.io/
  "Bulma minimal CSS framework."
[mathy]: https://mathy.pmdn.org/
  "Online LaTeX editor."
[svg]: https://en.wikipedia.org/wiki/SVG
  "Scalable Vector Graphics vector image format (SVG)"
[png]: https://en.wikipedia.org/wiki/PNG
  "Portable Network Graphics lossless raster image format (PNG)"
[webp]: https://en.wikipedia.org/wiki/WebP
  "WebP raster image format"
[vim]: https://www.vim.org/
  "vim text editor"
[markdown]: https://en.wikipedia.org/wiki/Markdown
  "Markdown lightweight markup language"
[matplotlib]: https://matplotlib.org/
  "Python charting library"
[pngquant]: https://pngquant.org/
  "Command-line tool for \"lossy\" compression of PNG images"
[graphicsmagick]: http://www.graphicsmagick.org/
  "Command-line swiss army knife tool for image processing"
[php]: https://www.php.net/
  "PHP programming language"
[bootstrap icons]: https://icons.getbootstrap.com/
  "Free high quality open source icon library"
[jekyll]: https://jekyllrb.com/
  "Jekyll static site generator"
[git]: https://git-scm.com/
  "Free and open source distributed version control"
[javascript]: https://en.wikipedia.org/wiki/JavaScript
  "JavaScript scripting language"
[minify]: https://github.com/tdewolff/minify
  "Minifier Go library and command-line tool"
[mastodon]: https://en.wikipedia.org/wiki/Mastodon_(social_network)
  "Mastodon distributed social network"
[css]: https://en.wikipedia.org/wiki/CSS
  "Cascading Style Sheets"
[editing-this-article]: /files/articles/site-backend/0-editing.png
  "Editing this article with Vim and Firefox"
[jpeg]: https://en.wikipedia.org/wiki/JPEG
  "Joint Photographic Experts Group (JPEG) lossy raster image format"
[es2015]: https://en.wikipedia.org/wiki/ECMAScript_version_history#6th_Edition_%E2%80%93_ECMAScript_2015
  "Modern JavaScript"
[post-theme-switcher]: {{< relref "posts/2024-05-27-site-updates-dark-mode-and-about-pics" >}}
  "Blog post about dark mode and theme switcher"
[prefers-color-scheme]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
  "prefers-color-scheme media query"
[dark mode]: https://en.wikipedia.org/wiki/Light-on-dark_color_scheme
  "Dark mode"
[webhook]: https://github.com/adnanh/webhook
  "Lightweight webhook server to run shell commands"
[webhook.conf]: /files/articles/site-backend/webhook.conf.txt
  "Download webhook.conf"
[hmac]: https://en.wikipedia.org/wiki/HMAC
  "Hash-based message authentication code (HMAC)"
[apache]: https://apache.org/
  "Apache web server"
[apache-vhost.conf]: /files/articles/site-backend/pablotron.org.conf.txt
  "Download Apache virtual host configuration"
[apache-tls.conf]: /files/articles/site-backend/tls.conf.txt
  "Download Apache TLS configuration"
[web hook]: https://en.wikipedia.org/wiki/Webhook
  "HTTP-accessible callback"
[deferred]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer
  "defer attribute of script tag"
[replay attacks]: https://en.wikipedia.org/wiki/Replay_attack
  "Network attack where a valid data transmission is repeated or delayed."
[tls]: https://en.wikipedia.org/wiki/Transport_Layer_Security
  "Transport Layer Security"
[post-receive]: https://git-scm.com/docs/githooks#post-receive
  "Git hook triggered on a remote repository after a push operation"
[git-repo]: https://git.pablotron.org/sites/pablotron.org/
  "Read-only web view of the Git repository for this site."
[deploy.rb]: https://git.pablotron.org/sites/pablotron.org/plain/bin/hook/deploy.rb
  "Deployment script invoked as a web hook."
[gen-logo.rb]: https://git.pablotron.org/sites/pablotron.org/plain/bin/logo-0/gen-logo.rb
  "Ruby script to generate animated SVG logo."
[post-png-compressors]: {{< relref "posts/2023-06-02-end-of-may-miscellany" >}}
  "Blog post with a review of several PNG compressors"
[bench-chart]: https://github.com/pablotron/fips203ipd/tree/main/scripts/bench-chart
  "Scripts to build and run fips203ipd bench utility with several combinations of backend and compiler, generate a summary CSV, and render a bar chart as an SVG."
[post-fips203ipd]: {{< relref "posts/2024-05-15-c11-fips203ipd-v0.6.md" >}}
  "Blog post about release of fips203ipd v0.6."
[script.js]: /files/articles/site-backend/script.js.txt
  "Unminified JavaScript for this site with some comments removed."
[http]: https://en.wikipedia.org/wiki/HTTP
  "Hypertext Transfer Protocol (HTTP)"
[https]: https://en.wikipedia.org/wiki/HTTPS
  "Hypertext Transfer Protocol Secure (HTTPS)"
[http/2]: https://en.wikipedia.org/wiki/HTTP/2
  "Hypertext Transfer Protocol, version 2 (HTTP/2)"
[post-headers-1]: {{< relref "posts/2021-10-19-hugo-csp-impedance-mismatch.md" >}}
  "Hugo/Content-Security-Policy Impedance Mismatch - October 19, 2021"
[post-headers-2]: {{< relref "posts/2021-10-21-tls-and-header-fixes.md" >}}
  "TLS and Header Fixes - October 21, 2021"
[post-headers-3]: {{< relref "posts/2021-10-25-the-nuclear-option-no-more-unsafe-inline.md" >}}
  "The Nuclear Option (No More unsafe-inline) - October 25, 2021"
[mod-http2]: https://httpd.apache.org/docs/current/mod/mod_http2.html
  "Apache module which provides HTTP/2 support."
[mod-proxy]: https://httpd.apache.org/docs/current/mod/mod_proxy.html
  "Apache module which provides proxy and reverse proxy support."
[mod-rewrite]: https://httpd.apache.org/docs/current/mod/mod_rewrite.html
  "Apache module which provides rules-based rewriting and redirecting engine."
[mod-macro]: https://httpd.apache.org/docs/current/mod/mod_macro.html
  "Apache module which provides macros within configuration files."
[mod-deflate]: https://httpd.apache.org/docs/current/mod/mod_deflate.html
  "Apache module which provides DEFLATE output compression."
[mod-brotli]: https://httpd.apache.org/docs/current/mod/mod_brotli.html
  "Apache module which provides brotli output compression."
[ssl-config]: https://ssl-config.mozilla.org/
  "Mozilla SSL Configuration Generator"
[tls 1.2]: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_1.2
  "TLS version 1.2"
[tls 1.3]: https://en.wikipedia.org/wiki/Transport_Layer_Security#TLS_1.3
  "TLS version 1.3"
[ssl-labs-report]: https://www.ssllabs.com/ssltest/analyze.html?d=pablotron.org&hideResults=on
  "SSL Labs SSL Test results for this site."
[ssl-labs-ssl-test]: https://www.ssllabs.com/ssltest/
  "SSL Labs SSL Test"
[security-headers-quick-ref]: https://web.dev/articles/security-headers
  "Security headers quick ref"
[permissions-policy]: https://scotthelme.co.uk/goodbye-feature-policy-and-hello-permissions-policy/
  "Goodbye Feature Policy and hello Permissions Policy!"
[permissions policy explainer]: https://github.com/w3c/webappsec-permissions-policy/blob/main/permissions-policy-explainer.md
  "Permissions Policy Explainer"
[referrer-policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#strict-origin-when-cross-origin
  "Documentation for \"Referrer-Policy: strict-origin-when-cross-origin\""
[content-security-policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
  "Content-Security-Policy header"
[hugo-shortcode-table]: https://github.com/pablotron/hugo-shortcode-table
  "Powerful Hugo table shortcode which emits HTML using CSS classes rather than inline style attributes."
[breach]: https://en.wikipedia.org/wiki/BREACH
  "Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext (BREACH)"
[cookies]: https://en.wikipedia.org/wiki/HTTP_cookie
  "Small blocks of data created by a web server when a user is browsing a web site."
[let's encrypt]: https://letsencrypt.org/
  "Free, automated, and open certificate authority (CA)"
[certbot]: https://certbot.eff.org/
  "Free, open source software tool for automatically using let's Encrypt certificates."
[certbot-dns-linode]: https://certbot-dns-linode.readthedocs.io/en/stable/
  "Certbot plugin which automates the process of completing an ACME dns-01 challenge by creating TXT records using the Linode API."
[securityheaders-report]: https://securityheaders.com/?q=pablotron.org&hide=on&followRedirects=on
  "Security headers report for this site from securityheaders.com"
[securityheaders.com]: https://securityheaders.com/
  "Free and easy to use tool to check the web site security headers."
[sass]: https://en.wikipedia.org/wiki/Sass_(style_sheet_language)
  "Syntactically Awesome Style Sheet (SASS)"
[style.sass.txt]: /files/articles/site-backend/style.sass.txt
  "Download SASS configuration"
[html]: https://en.wikipedia.org/wiki/HTML
  "HyperText Markup Language (HTML)"
[shortcode]: https://gohugo.io/content-management/shortcodes/
  "Snippets in content which call built-in or custom templates."
[chroma]: https://github.com/alecthomas/chroma
  "Chrome syntax highlighter"
[archives]: {{< relref "archive.md" >}}
  "Archived blog posts"
[go-import]: https://go.dev/ref/mod#vcs-find
  "HTML tag which allows Go to map a module name to a version control system repository."
[htmltest]: https://github.com/wjdp/htmltest
  "htmltest static HTML checker"
[w3c validator]: https://validator.w3.org/
  "W3C HTML validator"
[htmltidy]: https://www.html-tidy.org/
  "htmltidy"
[lighthouse]: https://developer.chrome.com/docs/lighthouse/overview/
  "Open source automated tool for improving the quality of web pages."
[chrome]: https://www.google.com/chrome/index.html
  "Google Chrome web browser."
[firefox]: https://www.mozilla.org/en-US/firefox/new/
  "Mozilla Firefox web browser."
[ssh]: https://en.wikipedia.org/wiki/Secure_Shell
  "Secure Shell"
[wireguard]: https://wireguard.com/
  "Fast and simple VPN which uses modern cryptography."
[vpn]: https://en.wikipedia.org/wiki/Virtual_private_network
  "Virtual Private Network"
[ip]: https://en.wikipedia.org/wiki/IP_address
  "Internet Protocol (IP) address"
[nftables examples]: {{< relref "articles/nftables-examples.md" >}}
  "NFTables Examples"
[wireguard is awesome]: {{< relref "posts/2021-11-06-wireguard-is-awesome.md" >}}
  "Wireguard is Awesome"
[ed25519]: https://en.wikipedia.org/wiki/EdDSA#Ed25519
  "Ed25519 digital signature algorithm."
[14kb]: https://endtimes.dev/why-your-website-should-be-under-14kb-in-size/
  "Why your website should be under 14kB in size"
[you-dont-need-js]: https://lexoral.com/blog/you-dont-need-js/
  "5 things you don't need JavaScript for"
[http compression]: https://en.wikipedia.org/wiki/HTTP_compression
  "HTTP compression"
[url]: https://en.wikipedia.org/wiki/URL
  "Uniform resource locator (URL)"
[progressive enhancement]: https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement
  "Web design which puts an emphasis on content first."