Problem
A command line nobody can install is a repository. Developers expect one line in their
own package manager: brew install on a Mac, apt install on Debian and Ubuntu,
winget install on Windows, or one command that works anywhere. Both
need repositories we run, because neither distribution will carry a new tool from a
small company on day one: Homebrew's core collection asks for a notable, widely used
project (Acceptable formulae) and Debian
packaging takes a sponsor and a release cycle.
Running a package repository makes us a link in our users' supply chain. Whoever can publish to it can run code as root on every machine that installed from it. The repository is therefore a security product in its own right: signed, built only by CI, never by hand, with every artifact traceable to the commit and the workflow that built it.
And it is a product in the sense of the EU Cyber Resilience Act. The command line is free and open source, but it exists to serve a commercial platform, which makes InOrbit its manufacturer, not an open-source steward (CRA summary). While it is in development, that work is scheduled, not done; see the last section.
Proposal
Install, as a user sees it
macOS and Linux with Homebrew:
brew install inorbithr/tap/iohr
Debian and Ubuntu, following Debian's current guidance for third-party repositories
(UseThirdParty): the signing
key as a binary keyring readable only through Signed-By, a deb822 .sources file, no
apt-key and nothing in the global trusted keyring, so our key can vouch for our
repository and for nothing else:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://packages.<domain>/iohr.gpg | sudo tee /etc/apt/keyrings/iohr.gpg >/dev/null
sudo tee /etc/apt/sources.list.d/iohr.sources >/dev/null <<'EOF'
Types: deb
URIs: https://packages.<domain>/apt
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/iohr.gpg
EOF
sudo apt update && sudo apt install iohr
The docs publish the key's fingerprint next to these lines, so a careful reader can check what they fetched.
Windows:
winget install InOrbit.iohr
Anywhere, without a package manager:
curl -fsSL https://packages.<domain>/install.sh | sh # Linux, macOS
powershell -c "irm https://packages.<domain>/install.ps1 | iex" # Windows
The installers are short enough to read before running, and the docs link to their
source. Each prints what it is about to do, downloads the archive for the machine from
the GitHub release over HTTPS, checks its SHA-256 against the release's attested checksum
file (and the archive's own build attestation when the GitHub CLI is signed in), and installs into the user's own directory (~/.local/bin, or
%LOCALAPPDATA%\Programs\iohr on Windows) without sudo or administrator rights unless
asked. They change no shell profile without saying so, and iohr itself never updates
itself behind the developer's back: updates come from whichever channel installed it.
Every release has plain archives (.tar.gz
for Linux and macOS, .zip and an .msi for Windows) with checksums for everyone else.
The release pipeline
Our own workflow in the SDK repository (its ADR 0010), run when release-please tags a
command-line release (iohr/vX.Y.Z; the repository already releases each package
independently):
- Build in GitHub Actions, each target natively on its own runner: Linux x86-64
and arm64, macOS Apple silicon and Intel, Windows x86-64 and arm64. Linux builds
link statically against musl, so one binary and one
.debinstall on every supported Debian and Ubuntu release. Each target gives an archive with the binary, licence, notice and shell completions; Linux adds a.deb(cargo-deb), Windows an.msi(WiX 5, the last release under its original licence). The same build runs on every pull request that changes it, so a release never finds a broken target. - Attest: a build provenance attestation for every file
(GitHub artifact attestations,
SLSA build level 2, checkable with
gh attestation verify), a CycloneDX SBOM per target attested against that target's files, and SHA-256 checksums. - Publish, after a maintainer approves the release in a protected environment that
only
maincan deploy to: the files to the GitHub release (a pre-release before 1.0); the.debinto the APT repository, with the key and the installers beside it on the packages host, then an install from it in a clean Debian as the docs describe; the installers run against the release on Linux, macOS and Windows; a pull request with the new formula toinorbithr/homebrew-tap, opened with a token minted for that repository alone; the winget manifests, generated and checked against winget's schemas. crates.io follows once its names are claimed, through trusted publishing.
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
The APT repository
A static repository, regenerated by CI on every release and stored in R2
behind 's CDN, at packages.<domain>. The domain's DNS is already on
, so the packages host is one record and one bucket, with no egress fees for
downloads. Static means there is no server of ours to patch or break into: the
repository is a directory of files, signed.
reprepro builds the dists/ and pool/
tree for the newest version on each architecture, signs InRelease and Release.gpg,
and the job uploads package files first and the signed index last; older package files
stay in the pool for anyone who pinned one.
It is served from , not from the that runs the platform today: an install must not depend on , and the platform's own known gaps () should not become our users' supply-chain gap.
The signing key is a dedicated OpenPGP key used for nothing else. The primary key is kept offline; a signing subkey with a two-year expiry signs the repository.
Rotation is announced in the docs and the release notes a release ahead, with both keys in the keyring during the overlap.
The Homebrew tap
inorbithr/homebrew-tap holds one formula per release that downloads the prebuilt,
attested archive for the machine's platform and checks its hash, so no one compiles
anything and nothing runs at install time beyond copying the binary, completions and
man page. Homebrew's core collection is the goal once the command line is used widely
enough to qualify; the tap then becomes a redirect.
macOS code signing and notarisation are not needed for a formula (Homebrew does not
quarantine formula downloads) but are needed for a direct download of the archive to
run without a warning. That needs an Apple Developer ID, which is being arranged; signing
and notarisation join the pipeline when it arrives. Until then the docs point Mac users
to Homebrew, and the shell installer, which downloads with curl and so is not
quarantined either.
Windows
winget is the channel Windows developers have by default, so the first release ships
there: a manifest in the community repository pointing at the .msi on the GitHub
release. The pipeline writes and checks the manifests on every release; submission
starts with the first stable release, because the community repository lists
pre-releases under a separate identifier. Scoop is not worth a second
manifest yet: its users are a small subset of winget's and are served by the .zip and
the PowerShell installer meanwhile. Windows code signing (Authenticode) follows the same
path as Apple's: the certificate is a purchase, and until it exists SmartScreen may warn
on a direct download, which winget and the installer avoid.
The Cyber Resilience Act, at release 1.0
The command line is in development, and the support period and the rest of the Cyber Resilience Act work (vulnerability handling as declared obligations, technical documentation, the conformity declaration and CE marking) are declared with release 1.0, not before. The SBOM, the provenance and the signed packages above are built from the first release anyway, because they are good practice regardless.
One obligation does not wait for 1.0: reporting an actively exploited vulnerability or a severe incident applies to any version already made available, and has since 11 September 2026. Releases before 1.0 are therefore published and labelled as pre-release, and our compliance programme's reporting runbook covers them.
Alternatives considered
- Only
curl … | sh. Offered, but not alone: it brings no updates through the system's package manager and no signature the system checks, and some security teams forbid the pattern. The packages are the main channel; the installer is for machines without one. - A hosted package service (Cloudsmith, Packagecloud, Gemfury). Less to build, but a third party then holds our signing key or signs for us, and our users' trust extends to them. The repository is a directory of signed files; we can run that.
- aptly instead of reprepro. Both are sound. aptly keeps its own database and snapshots, which a stateless CI job does not need; reprepro works on the tree itself.
- Our own small CDN, or the platform's own edge. The edge already serves our sites, but it is today, and a CDN of our own is servers in several places to run and patch. Packages need better availability than the platform has yet, and already serves the domain's DNS.
- dist (formerly cargo-dist) instead of our
own workflow. It builds the matrix, checksums, installers and the tap formula, but
generates and regenerates its own release workflow and expects to create the GitHub
release, where release-please already owns tags and releases and every job of ours
runs a task we can run locally. The
.deband the installer would have stayed custom jobs anyway. Our workflow is about two hundred lines. - Snap, Flatpak, an RPM repository, Scoop. Later, by demand. RPM is the likely next one and reuses the same pipeline and key.
Decision
Decided 2026-10-02:
- The packages host is R2 behind 's CDN, not a CDN of our own.
- The installers ship in the first release:
install.shfor Linux and macOS andinstall.ps1for Windows, checking checksums, installing per user, saying what they do. - macOS signing and notarisation land when the Apple Developer ID arrives; Homebrew and the shell installer serve Macs until then.
- The Cyber Resilience Act's support period and declared obligations come with release 1.0; releases before it are labelled pre-release.
With RFC 0019, Windows is in the first release: winget, an .msi and a .zip, and the
PowerShell installer.
Publication
When it ships: the docs gain install instructions for every channel, with the key fingerprint and links to the installers' source, the packages host serves the repository, the SDK repository's security docs gain "verifying a package", and this RFC's status log records the first release published through each channel.
Status log
- 2026-10-02: proposed with RFC 0019 (the command line) and RFC 0020 (SDK generation).
- 2026-10-02: decided with the user's answers: R2 and CDN for the packages host, shell and PowerShell installers in the first release, Windows through winget, macOS signing when the Developer ID arrives, the CRA's support period with release 1.0.
- 2026-10-02: first pre-release,
iohr0.1.0-alpha.2, built for six targets and attested in the SDK repository's own release workflow (dist was not used: it owns the release workflow and the GitHub release, which release-please already does here). Published through the APT repository atpackages.<domain>(installed from it in a clean Debian), the shell and PowerShell installers, and a Homebrew formula proposed to the tap. winget manifests are generated and checked on every release; submission starts with the first stable release. 0.1.0-alpha.1 was tagged but never published: its release job stopped at the SBOM step. - 2026-10-07: Checked: nothing since the last line changes the decision. Status unchanged.