Skip to content

The plugins' signed releases, run on the first XGS 3300, 2026-11-07

Until 2026-11-07 every plugin's self-update.py asked GitHub's API, or npuforge.org's copy of it, for its latest release and the release's commit, and installed that commit's tarball as it came: the copy's answer chose what the Plugins tab of XGS: Updates installed, as it had for this project's own update until signed releases (1072, G3 and U5).

Now a plugin's release is installed only as its owner signed it, as this project's is. Each release carries three more files: the tag's tree as an archive, release.json naming the tag, its commit and date and the archive's SHA-256 and size, and release.json.sig, the manifest's Ed25519 signature, made with the project's release key by the owner's private signing repository. A plugin's own release workflow makes the release a draft, with its package, and that repository signs and publishes it. Each plugin installs the public key beside its self-update.py, fingerprint 13ed7f86a4a7669bdaee666d01894443e151da4051c1f500cb6406d7888fd33b. A check reads the latest release's manifest from GitHub's download of it, not the API, and offers the release only when the signature verifies and the release is not dated before what is installed. The update confirms the tag's commit with GitHub's list of references, and installs the archive the manifest names only when its SHA-256 and size are the signed ones.

os-netreport went first (its pull request 43, with its own page). This page is the eight that followed - os-fanctl, os-frontpanel, os-linkhealth, os-watchdog, os-kernelfollow, os-blocklists, os-portguard and os-fallbackcrypto - all at once, and then the last two, opnsense-arabic and opnsense-rtl, whose self-update.py sits at the top of their repository (the last two).

How it was run

The first XGS 3300 (vendor version 23), 01:40 to 01:47 UTC. Each plugin's branch was installed by its own install/install.sh, as a hand install does. Every step after that went through the Plugins tab itself: its Check, and its Update all.

A release cannot be put up on GitHub for a test, so each plugin read its latest release from a lab place, as a test firewall may (releases-from, file://). The archive was made by git archive from the tag, with its manifest, and signed with a throwaway key whose public half stood in for the release key while the test ran. Each plugin's installed copy was said to be the release before it, so that the latest was offered. Each latest release carries the self-update.py from before signing, so the update left each plugin as it had been before the test. The log of every step is kept with the measurements (plugins-old3300.log).

What it did

GitHub's own latest releases, published before signing, are not offered: the latest release is not signed yet: it has no release.json. Every installed plugin will say this until its next release, the first signed one.

First XGS 3300: eight plugins whose latest release on GitHub is not signed yet

The lab releases, signed with the throwaway key, while only the real release key was installed: not offered - the signature of the latest release does not verify with any release key of this firewall.

Signed with a key the firewall does not hold: none of the eight offered

The throwaway key installed, and one byte of each archive changed after signing. All eight were offered, since each manifest was as signed. Update all stopped each one once its archive was down: the archive of v1.1.6 is not the one signed: 330461 bytes, SHA-256 bab56dee351636d7, where 330461 bytes, SHA-256 9c1541a37a62b02b were signed; nothing was installed, and the same for the other seven. Updated 0 of 8 in 21 s. No installed.json changed, and nothing was left in /var/tmp. The bars stop at Check for updates: the tab follows the files an update leaves in /var/tmp, and an archive read from a local place in under half a second was gone before it looked; the message is the update's own.

An archive one byte from the signed one: each update stopped, nothing installed

The last of the eight, and Update all offering them again

Everything as signed: all eight offered and installed by Update all in 32 s, each one updated to (), signed with lab-key-b20d1046.pub, its installed.json naming the signed commit with the date signed. The fan controller, the front panel's client and watchdogd were running afterwards, and the appliance was not restarted.

The eight installed as signed

Plugin Release Archive, bytes Commit
os-fanctl v1.1.6 330461 877803880c9a
os-frontpanel v1.0.6 141501 7a2300fd17ec
os-linkhealth v1.0.7 167263 27ad63267c09
os-watchdog v1.0.2 32324 aae739e071e7
os-kernelfollow v1.0.3 45771 12554c5b17b0
os-blocklists v1.0.1 45908 05bdc59d2507
os-portguard v1.0.1 38033 8498e3f5f64a
os-fallbackcrypto v1.0.1 36189 c68414407d80
opnsense-arabic v1.0.6 748814 694a3f6b6838
opnsense-rtl v1.1 14865 5dd540ee4d7e

The lab files, the lab places and the branches' trees were taken away after. The release-keys directories went too, since the self-update.py installed again reads none. Each plugin's self-update.py was the file it had been before the test, by its MD5.

Without a firewall, a plugin's test-self-update.py makes releases and signs them with throwaway keys: 42 checks of what a check takes and refuses and what an update installs. Each plugin's pull request for this change added it to that plugin's checks, and all 42 passed in each of the ten before it was run here.

The last two

opnsense-arabic keeps its self-update.py and its keys in /usr/local/share/opnsense-arabic, and opnsense-rtl in /root/opnsense-rtl; each one's install.sh puts them there. Neither had a release workflow - their releases were made by hand - so each was given one that makes the draft from an annotated tag and starts the signing. They ran the same way on the same appliance, 01:57 to 02:01 UTC: GitHub's own latest releases, not signed yet, not offered; signed with the throwaway key while only the release key was installed, not offered; one byte of each archive changed, both updates stopped with nothing installed (Updated 0 of 2 in 4 s); as signed, both installed by Update all in 10 s. With them, all eleven plugins of the family had installed a release as signed on this appliance.

opnsense-arabic and opnsense-rtl: an archive one byte from the signed one, both updates stopped

All eleven plugins installed as signed

A plugin that is not here

Until then the Plugins tab's Install took a missing plugin's self-update.py from raw.githubusercontent.com at its latest release's tag, as it came, and ran it as root (1072, G3). Now plugins.py reads that release's signed manifest and verifies it with this project's own release key. It downloads the archive the manifest names and checks its SHA-256 and size, then takes only the plugin's self-update.py and the release keys beside it out of that archive. That self-update.py checks the release again with those keys before it installs anything.

Run on the same appliance, 02:21 to 02:28 UTC, with that change's plugins.py put in place. os-portguard was taken away for the test by its own install/uninstall.sh (its settings stay) and put back after.

GitHub's own latest release of os-portguard, published before signing, is listed with the reason, and Install is not offered: its latest release is not signed yet: it has no release.json.

A plugin that is not here, whose latest release is not signed: listed with the reason, Install not offered

Then a lab release, v1.0.1 by the commit GitHub's tag names. Its archive was os-portguard's main as merged that day (its pull request 11), which holds the signed self-update.py, with the throwaway key in place of the release key; it was signed with that key, which stood in beside the release key while the test ran.

  • One byte of the archive changed: Install refused at once - the archive of its latest release, v1.0.1, is not the one signed; nothing was taken from it; nothing was installed - and nothing was left in /var/tmp.
  • As signed: Install installed it in 7 s - updated to v1.0.1 (8498e3f5f64a), signed with lab-key-b20d1046.pub - and the update's log named the archive as the one signed, 41015 bytes.

The same plugin installed by Install from its signed archive

Afterwards os-portguard v1.0.1 was installed again from its tag, and its self-update.py was the file it had been, by its MD5. The lab's files, places and key were taken away, and main's plugins.py was put back. Without an appliance, tools/test-plugin-install.py runs 20 checks of the same, with throwaway keys.

The releases

The same day nine of them were released, each signed with the release key: os-netreport 1.0.6, os-fanctl 1.1.7, os-frontpanel 1.0.7, os-linkhealth 1.0.8, os-watchdog 1.0.3, os-kernelfollow 1.0.4, os-blocklists 1.0.2, os-portguard 1.0.2 and os-fallbackcrypto 1.0.2, published from 11:44 to 12:15 UTC. Each release's workflow built its package and made the release a draft; the owner's signing repository signed it and published it, its archive, its manifest and the manifest's signature beside the package. opnsense-arabic and opnsense-rtl carry the same change and have no release of it yet.

Two faults on the way, both in the release workflows, none in what an appliance runs:

  • os-netreport's run started no signing and still said success: gh workflow run asked for the signing repository's default branch, which its token may not read. Its signing was started by hand; in all eleven plugins the start now names the branch, and a start that fails fails the run.
  • Seven tags pushed within a minute started seven signings. The signing repository signs one at a time, and GitHub keeps only the newest of the runs waiting, so four releases stayed drafts until one look over the family signed them, at 12:15.

Then each lab appliance took them through XGS: Updates' Plugins tab, Check and Update all: nine plugins in 65 s on each XGS 3300, eight in 59 s on the XGS 136, which has no front panel. What was installed ran the self-update.py from before signed releases, so each took its release the old way, once. Afterwards each plugin carries the release key, and its new self-update.py, asked to check, verifies the published release with it.

The three lab appliances taking the nine releases by Update all: each plugin at its release's commit

Each appliance's new self-update.py of three plugins, asked to check: the release installed, its signature verified with the release key, the one key it carries

Discussion

The discussion of this page is held in the project's GitHub Discussions. Showing it here loads giscus from giscus.app and GitHub, and writing in it needs a GitHub account - what that means.