Things that came with the house

Before the move nobody had listed what a build pulled in. The old blog alone pulled Hugo at a pinned version, its theme as a Go module from a GitHub repository, and a search tool through npx from npm, on every build at the host, and none of that was in the repository. A website can have an entirely European service architecture and still depend on software published, maintained, hosted or updated elsewhere, and the scraps for this layer are the branch called “things nobody knowingly installed”. There is “this service was chosen” and there is “this service chose seven dependencies on its own”. The second category is where the surprises live. Purple Lantern’s documentation sites are built with Sphinx at statichost, its blog with Zola and its main site with Frozen-Flask at home, and the packages and registries they pull are real.

Pulling

The Sphinx sites are the example. A repository holds requirements.txt with Sphinx, MyST and the immaterial theme pinned, and that looks like a local build specification. It is a list of requests to package infrastructure: Sphinx, then the index, then the package, then its dependencies, then theirs. One declared dependency produces a considerable tree. What went into the scraps for each package is name, version, source, lock file, transitive dependencies, licence, maintainer, whether the package is needed at all, and whether it can be cached or mirrored. Nobody is avoiding PyPI. Knowing that PyPI is in the build graph is enough.

The operating system is in the graph too. A Sphinx build needs Python, system libraries, sometimes a compiler, image tools, fonts, compression tools, Git and SSL libraries. The workstation has all of them, which makes the build look self-contained until it moves to a fresh machine. So the description keeps the Python dependency, the operating system dependency and the external service apart.

The theme

Themes and extensions add Markdown, diagrams, highlighting, search, redirects, custom directives, PDF output and link checking, and each is another package and another maintainer. For each one the question was what it buys and what happens if it disappears. An extension for one small convenience is better removed. The theme that provides the entire navigation is a significant dependency, and the immaterial theme also downloads its fonts from Google at build time into a cache directory. The cache is committed, so a clean build at statichost reads it and never asks Google, and the fonts are served from the site, not from Google, at page view. CSS that references a font provider introduces an external request, a third party, a visitor privacy dependency, an availability dependency and possibly a jurisdiction, at every page view. Self-hosting the files turns visitor, site, font provider into visitor, site, font. The same goes for icons and other static assets.

Zola

Zola’s docs say to clone a theme’s repository into themes/, which makes the site depend on that repository existing; a theme that exists only in somebody else’s repository is a remote dependency wearing a nice hat. The blog has no theme at all. Its templates and its CSS, JavaScript and images are its own, in the repository. The one Zola dependency is the binary, a single file from the project’s GitHub releases or a package manager, and it lives on the workstation, because the blog is built there and its public/ is committed.

Actions

A uses: someone/some-action@... line executes somebody else’s code inside the build, and the chain is repository, GitHub Actions, third-party action, third-party repository. Moving a repository off GitHub does not remove the dependency if the workflow stays; on Forgejo a relative uses: resolves against data.forgejo.org by default, which is a mirror of the same code under a different roof. No workflow came along. The checks run on the workstation, and any workflow that turns up later is a code dependency to inventory, not CI configuration.

One container

Containers make the same problem larger. FROM something:latest is a registry, an image maintainer, a base OS, system packages, application dependencies and update feeds in one line, and latest means a build that works today may be a different build next month. The statichost build uses python:3.12-slim from Docker Hub, so the branch exists, pinned by tag, where a digest would pin it properly. There are no other containers, and not having them removes a whole branch.

What the visitor pulls

JavaScript creates two dependencies: the one needed to build the site, from npm, and the one imposed on every visitor, from a CDN. The second is the important one. A static site can be entirely static while making visitors contact Google, Cloudflare, an analytics provider, an advertising system, a JavaScript CDN or an embedded service. This problem is different from the build one, and much easier to remove: a locally served script does not need a multinational to be operational at the moment somebody reads a page. The main site’s search is lunr.js, a copy kept in the repository; the blog’s is elasticlunr, built by Zola; the documentation sites’ is Sphinx’s own. Nothing is fetched from a CDN. And a decorative animation that a few lines of CSS can replace takes everything beneath it out of the graph when it goes.

Hearing about holes

What runs when something is pushed got an inventory even with no CI: build scripts, shell scripts, the statichost builder, the two local build scripts. The trusted computing base is larger than the repository. The chain is CodeFloe, webhook, statichost builder, Sphinx or Zola, dependencies, artefact, and every arrow is a dependency. It is still understandable, which is half the battle. The build tools themselves, Python, Sphinx, Zola, Git, shell utilities, image tools and link checkers, are written down because they are usually invisible. A short document in each repository holds the required runtime, packages, system packages, build command and expected output. It is cheap insurance, and it makes moving between hosts easier because the host does not need to know how the site works.

Updates need information from somewhere saying that a package is vulnerable and here is the fix: distribution security feeds, Python and npm advisories, GitHub security alerts, vendor feeds, CVE infrastructure, project mailing lists. The software update is one thing, and the information that an update is needed is another. A provider can disappear while the software stays installed, and without an independent way of hearing about vulnerabilities the software goes stale without anyone noticing. This dependency cannot be eliminated, because security information is distributed by nature. The answer is diversity of sources, not owning the vulnerability database.

Provenance

For the dependencies that count, the scrap holds where they came from:

Dependency

Version

Source

Maintainer

Licence

Used by

Replaceability

Sphinx

pinned

PyPI

project

open source

docs

high

sphinx-immaterial

pinned

PyPI

project

open source

docs

medium

MyST

pinned

PyPI

project

open source

docs

medium

Zola

pinned

GitHub releases, on the workstation

project

open source

blog

high

lunr.js

vendored

repository

project

MIT

main site search

high

Fonts

Roboto, committed cache

Google Fonts, once

Google

OFL

docs

high

python:3.12-slim

tag

Docker Hub

Docker

open source

build

medium

The fields can change. What a row gives a dependency is an identity. “Some package Sphinx needs” is not an identity.

The inventory

Dependencies listed and pinned, lock files committed, the important inputs cached, third-party JavaScript at zero, fonts self-hosted, no GitHub Actions, one container image pinned by tag with a digest still to do, the theme and the absence of a Zola theme inventoried, build tools documented, update sources diversified, licences recorded. Not a hermetically sealed laptop, which produces a very secure machine that can no longer install a security update, a charming sort of success.

Supply chain exposure differs from the hosting and financial layers because the provider may never have had an account, was never knowingly chosen, and is just somewhere in the build: Sphinx, extension, package, US-maintained repository. A designation affecting that maintainer does not stop an existing build. It becomes relevant when the package disappears, the repository closes, updates stop, the index cannot retrieve it, a dependency is withdrawn, the build wants the latest version, or a security fix needs a replacement. That is what pinned dependencies and local caches are for. A world with no US software in it is nobody’s target. A build that does not depend on a particular repository still being there tomorrow morning is.

The package index is unreachable, what now?

  • Does an existing checkout still build? With the virtual environment deleted, can it be recreated from retained dependencies?

  • Without GitHub, does the build still run? Without the Zola releases page, can the binary be found elsewhere?

  • Without one Sphinx extension, is the documentation still produced, minus one feature?

  • Without an external font or a JavaScript CDN, does the site still render? It never asked for either.

  • Without Docker Hub, does anything important stop? The documentation builds do, until the last good build is uploaded by hand.

These distinguish a dependency that is part of the system from one that is convenient today. What entered the build without being invited is known, pinned, written down, cached where needed and removed where it added little, and enough of the supply chain stays locally recoverable to build when the Internet, a repository or a maintainer has other plans.


Nanny Ogg’s cottage is full of things her sons, daughters-in-law and grandchildren have carried in over the years. She could not say where half of it came from, and she knows exactly which half she would carry out in a fire.