How a change on this machine reaches Settings → Updates on every install.
git tag -a v0.2.0 -m "Evidence reminders now include the control they belong to."
git push origin v0.2.0GitHub then builds everything, signs it, and publishes it. Every install that checks for updates afterwards sees 0.2.0, with the tag message as its notes.
Pushing commits without a tag publishes nothing. Customers only ever see what you tag.
The Release workflow (.github/workflows/release.yml):
- Runs the full test suite. A failing test stops the release.
- Builds and pushes the Docker images for all four products to
ghcr.io/offsetsecurity: the application and its updater. - Builds the Linux tarballs, and the Windows installers.
- Writes
release.json, naming every file by its SHA-256 and every image by its digest. - Signs it with
RELEASE_SIGNING_KEY. - Checks the signature with the key compiled into the application. If the secret and the application disagree, the release stops here, not on a customer's server.
- Publishes each product to its own releases repository, with a manifest naming only that product.
It takes about half an hour, most of it the Windows and Linux builds.
vMAJOR.MINOR.PATCH, always three numbers, always going up.
- Patch (0.2.0 → 0.2.1): fixes only.
- Minor (0.2.1 → 0.3.0): new features.
- Major (0.9.0 → 1.0.0): anything a customer has to act on.
Installs refuse to move to a lower or equal version, so a mistaken tag cannot be fixed by tagging a lower one. Tag the next version up instead.
Do these once. None of them is needed again for later releases.
This repository's own Releases page. A tag builds the packages, signs one manifest naming them, and attaches the lot to a release named after the tag.
Nothing else publishes there, which is what lets an install ask for "the
latest release" and get this product. Five products once shared a downloads
repository for exactly that reason - they could not each have a latest of
their own while one repository built them all. They can now, so that
repository is gone, and with it the cross-repository token a release used to
need: offsetsecurity/offset-align publishes to itself with the workflow's own GITHUB_TOKEN.
grc-suite-releases, the old single page, stays where it is. Copies installed
before this change still ask it for updates, and will until they are
reinstalled.
The private key was created on 2026-09-17 at:
%USERPROFILE%\.offset-release\release-signing-key.pem
In offsetsecurity/grc-suite → Settings → Secrets and variables → Actions →
New repository secret:
- Name:
RELEASE_SIGNING_KEY - Value: the whole contents of that file, including the
BEGINandENDlines
Back that file up somewhere safe and offline, such as an encrypted USB drive. Anyone who has it can publish an update every install will accept. If it is ever lost, or you think anyone else has seen it, see Replacing the signing key below.
GitHub → your profile → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token:
- Repository access: the
offsetsecurity/downloadsrepository - Permissions: Contents: Read and write
- Expiry: a year, with a calendar reminder to renew it
Save it in offsetsecurity/grc-suite as a repository secret named
RELEASES_REPO_TOKEN.
Adding a product later means adding its repository to this token, or the release fails at the last step with a 403 that reads like a write-permission problem.
The packs are in repositories of their own and the build fetches them. A second
fine-grained token, read-only on assure-pack-for-iso-27001,
irtiqa-pack-for-sama, nist-packs and abide-pack-for-hipaa, saved as
PACK_REPO_TOKEN. See docs/dev/product-parity.md.
In offsetsecurity/grc-suite → Settings → Environments → New environment,
name it release. Optionally add yourself as a required reviewer: every
release then waits for you to click Approve before it is signed.
GitHub creates the container images as private. Customers' servers pull them without logging in, so each one has to be made public, once:
GitHub → offsetsecurity profile → Packages → for each of
offset-align, offset-align-updater, offset-align, offset-align-updater,
offset-assure, offset-assure-updater, offset-anchor, offset-anchor-updater
→ Package settings → Change visibility → Public.
- The manifest is signed with Ed25519. The public key is compiled into the
application (
TRUSTED_KEYSinapps/api/src/update/release.ts). - Every file is checked against the SHA-256 in the signed manifest, and every image is pulled by digest, so nothing swapped after signing can install.
- An install only moves forward, and only to the signed latest release.
- The part that installs (a container, a root service, a SYSTEM task) never takes its settings from anything the web application can write.
- Nothing contacts the internet until an administrator presses the button.
node deploy/release/keygen.mjs <new-key-file.pem>- Add the printed public key to
TRUSTED_KEYS, keeping the old one, and release that version signed with the old key. - Once customers have updated to it, put the new private key in
RELEASE_SIGNING_KEY. Later releases are signed with the new key, which every updated install now trusts. - Remove the old public key from
TRUSTED_KEYSin a later release.
If the old key has leaked rather than been lost, do step 2 as quickly as possible and tell customers to update. Installs that never update cannot be protected from a leaked key.
To get an image onto a machine before any release exists, for a demonstration
or a test install, use the Build a Docker image workflow
(.github/workflows/docker-image.yml). It builds nothing else and publishes
nothing to customers.
- In
offsetsecurity/grc-suite→ Actions → Build a Docker image → Run workflow. - Pick a version such as
0.1.0. Press Run workflow. It takes a few minutes. - The images are stored, private, as
ghcr.io/offsetsecurity/offset-align:<version>andghcr.io/offsetsecurity/offset-align-updater:<version>, each also taggedlatest.
To download them, the machine needs a GitHub token that can read packages: GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic), with only read:packages ticked. Then:
docker login ghcr.io -u <your GitHub username>
docker pull ghcr.io/offsetsecurity/offset-align:<version>
docker pull ghcr.io/offsetsecurity/offset-align-updater:<version>Paste the token when it asks for a password. Nothing appears as you paste;
press Enter. Then, in the install folder's .env, point the stack at them:
OFFSET_IMAGE=ghcr.io/offsetsecurity/offset-align:<version>
OFFSET_UPDATER_IMAGE=ghcr.io/offsetsecurity/offset-align-updater:<version>
and run docker compose up -d. Customers do not install this way: they get
the published images once a release has been made.
Point a test install at your own channel instead of GitHub:
- Docker: in its
.env, setUPDATE_URL,UPDATE_TRUSTED_KEYS(your test public key) and, for a plain-HTTP test server,UPDATE_ALLOW_HTTP=true. - Linux: create
/etc/offset-align/updater.jsonas root, with{"url": "...", "trustedKeys": ["..."], "allowHttp": true}, and set the same threeUPDATE_*values in/var/lib/offset-align/.envfor the screen. - Windows: the same JSON in
updater.jsonbesidenode.exein the install folder.
Settings → Updates shows a warning whenever a non-standard key is trusted.