Tauri Updater — Signing & Feed Setup
This document covers the Tauri updater plugin wiring for
sortOfRemoteNG: how the Ed25519 signing keypair is generated, where the
private key must live, and the JSON schema of the latest.json feed included
with every successfully promoted GitHub Release. A signed platform entry uses
the cryptographically verified automatic installation path. An unsigned entry
from the fixed official public GitHub feed can use the separate unsigned
installer only after an explicit warning acknowledgement for that attempt; it
is never trusted automatically. Private unsigned entries and artifacts remain
non-executable and manual-only unless a future release defines an additional
pinned trust policy.
The production updater path is intentionally narrow: the signed
tauri-plugin-updater runtime verifies and installs artifacts, and the
application exposes it through the backend-owned updater_* commands in
sorng-updater. The separate backend-owned updater_install_unsigned command
ignores configured private endpoints, re-resolves the version and platform
target from the fixed PUBLIC_ENDPOINT_URL, requires an explicit risk
acknowledgement for each invocation, and accepts only the exact canonical
GitHub release artifact URL for this repository. Frontend code must call those commands through
src/hooks/updater/useUpdater.ts; it must not call
@tauri-apps/plugin-updater directly.
Unsigned completion is platform-specific. Windows launches the matching NSIS or MSI installer and Linux replaces the running AppImage. macOS cannot replace an installed app merely by opening a DMG: the action downloads and opens the validated DMG, closes sortOfRemoteNG, and tells the user to drag the app to Applications and reopen it. The native status keeps that update available instead of claiming the DMG handoff installed it.
The old custom downloader, copy installer, scheduler, channel, history, and rollback paths are not production update mechanisms. Automatic P1 installs are signed Tauri updater installs only, and only package types compatible with the feed payload may use them: Linux AppImage, Windows NSIS, Windows MSI, and the macOS app bundle. Private feed configuration is managed by Settings > Updater and documented in private updater endpoint guide.
Public OS installers and updater delivery are separate release outputs. Every
successful main push may publish Windows, Linux, and macOS installers without
OS-signing certificates. The workflow generates latest.json with or without
the updater signing key and includes it in every promoted release. When
TAURI_SIGNING_PRIVATE_KEY is absent, its platform entries have empty
signatures. For the official public feed, supported install modes expose both
the manual-download link and a separate unsigned-install action that remains
disabled until the user acknowledges the warning for that attempt. The
backend re-resolves the fixed public feed, version, and platform target and
does not trust a URL supplied by the UI. Configured private endpoints are not
consulted by the unsigned command; their unsigned entries remain
manual-download metadata. Signed updater archives and signatures are published
only when the key is configured.
The feed’s Linux payload is the architecture-matched AppImage. Windows carries
two payloads per architecture, selected by how the running app was installed:
the NSIS setup.exe under the bare windows-<arch> key, and the .msi under
the per-installer windows-<arch>-msi key (see §4.1). A different package type
must never install those bytes over itself. Debian, RPM, Flatpak, and the
architecture-matched Windows x64 and ARM64 portable ZIP builds therefore use
externally managed updates: download and reinstall or replace the newer
matching public asset from GitHub Releases.
Flatpak builds additionally live under read-only /app; update them manually,
for example with
flatpak install --user --reinstall ./sortOfRemoteNG_<version>_linux-<arch>.flatpak.
Those package types are intentionally absent from latest.json and do not
expose in-app update check/install controls.
1. Pubkey (committed) / Privkey (never committed)
Public key — inline in src-tauri/tauri.conf.json under
plugins.updater.pubkey. This is the base64-wrapped minisign public key
embedded into every build so that the runtime updater can verify the
signature of any downloaded update artifact. Rotating it requires a
coordinated release (see §5).
The current committed pubkey:
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDMwNDBGMTM2QTFFRUU2RDQKUldUVTV1NmhOdkZBTUpMeUFxYXNzMHdBRi9PemFUOUludnVYUHFpTTJVVXAyRGN4TUdJMmlhYjQK
Private key — must never be committed. It is generated via
tauri signer generate -w <path> (minisign-format Ed25519) and lives in
exactly two places, with a third (developer workstation) as an opt-in for
local signed-build testing.
| Location | Purpose | Access |
|---|---|---|
1Password vault: sortOfRemoteNG / Release signing |
Canonical master copy. Holds sortofremoteng.key, the password used at generation time, and a dated rotation log entry. |
Release maintainers only. |
GitHub secret TAURI_SIGNING_PRIVATE_KEY (+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD) |
Consumed by the release CI workflow (tauri-action). Paste the entire *.key file contents including the untrusted comment: header line. |
Repository admins; rotates by overwrite. |
Developer workstation at ~/.tauri/sortofremoteng.key |
Optional. Only for maintainers running signed local builds. Must be 0600 / ACL-restricted. Not required for normal development — tauri dev and unsigned tauri build both work without it. |
Individual maintainer. |
The repository’s .gitignore contains the patterns .keys/, *.key,
*.key.pub to stop accidental commits of either half of the keypair.
The public *.key.pub file is also gitignored — we commit the pubkey
inline in tauri.conf.json, never as a loose file, so that a single
well-reviewed location is the source of truth.
Regenerating the keypair (bootstrap / one-time)
npx tauri signer generate -w ~/.tauri/sortofremoteng.key
# Follow prompts; choose a strong password; record it in 1Password.
Then:
- Copy
~/.tauri/sortofremoteng.key.pubcontents (single base64 blob). - Paste it into
src-tauri/tauri.conf.json→plugins.updater.pubkey. - Paste
~/.tauri/sortofremoteng.keycontents into the GitHub secretTAURI_SIGNING_PRIVATE_KEY. If you set a password, also setTAURI_SIGNING_PRIVATE_KEY_PASSWORD. - File a 1Password vault entry and delete the workstation file once the GitHub secret is confirmed working (or keep it for local signed-build testing — see table above).
2. CI signing env vars
The GitHub release workflow (.github/workflows/release.yml) consumes these
environment variables when invoking tauri-apps/tauri-action:
| Env var | Required | Notes |
|---|---|---|
TAURI_SIGNING_PRIVATE_KEY |
for signed updater artifacts and automatic installation | Full contents of sortofremoteng.key, including the untrusted comment: header. |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
when that private key is password-protected | Plaintext password; store it as a protected secret. |
No other updater-specific secrets are required — the pubkey is embedded at
build time from tauri.conf.json. Apple Developer ID/notarization credentials
and Windows Authenticode credentials are optional OS-signing inputs described
in the linked platform runbooks. Their absence must not block publication of
public installers.
Release identity has two deliberate representations:
- Bare Git tags and GitHub Release names use rolling
YY.N(26.1) with no prefix. YYis the two-digit UTC year andNis that year’s monotonically increasing counter. The counter starts at 1 after each UTC year rollover;26.1is the first-release example for 2026.- Package manifests, bundle filenames, and updater feeds use the machine-only
SemVer projection
YY.N.0(26.1.0). version.jsonand the other projections are synchronized in an immutable, detached release snapshot. ItsRelease-Source-SHAcommit trailer records the successfulmainsource commit.
Every successful push to main queues this workflow only after the CI-internal
jobs and the exact-source Audit, Backend Coverage, Frontend Build, and
Docker e2e (nightly) workflows pass. The workflow receives that commit as
source_sha, atomically allocates or recovers its bare tag, derives YY.N.0,
and runs the version checks against the prepared snapshot. A rerun for the same
source SHA reuses its tag and GitHub Release rather than incrementing N.
The eight updater platform keys are windows-x86_64, windows-aarch64,
windows-x86_64-msi, windows-aarch64-msi, linux-x86_64, linux-aarch64,
darwin-x86_64, and darwin-aarch64. They are published from six build
targets: the two Windows targets each contribute both an NSIS and an MSI
platform key, so the per-installer keys have no build target and no
*.provenance.json record of their own. If the
Tauri private key is absent, the workflow omits updater signatures and signed
macOS app archives but still publishes the corresponding public OS installers
and an unsigned latest.json. Compatible installations may use its official
public artifacts only after explicit per-use warning acknowledgement; the app
does not automatically trust them. The workflow records that intentional
optional-off state in the job summary rather than a warning annotation. A
password secret configured without the private key fails closed as an
incomplete updater configuration. If the key is present, missing or invalid
updater artifacts fail publication of the feed. Public .rpm and .flatpak
files do not add updater platform keys or signature entries.
3. Feed URL and runtime endpoint policy
The committed public endpoint in tauri.conf.json is:
"endpoints": [
"https://github.com/supermarsx/sortOfRemoteNG/releases/latest/download/latest.json"
]
GitHub serves latest/download/<asset> as a 302 redirect to the most recent
release’s asset with that filename, so publishing latest.json is sufficient
to expose the newest version — no edits to the committed endpoint list are
required per release. The feed’s updater_signing marker and selected
platform signature distinguish the verified automatic path from the
warning-gated official-public unsigned path. Private unsigned entries remain
manual-only.
At runtime, sorng-updater may prepend a Settings-managed private endpoint
by constructing a plugin updater with updater_builder().endpoints(..). The
same embedded pubkey verifies both private and public artifacts. The legacy
UPDATER_PRIVATE_ENDPOINT_URL build-time env var is ignored and should not
be used for production configuration.
4. latest.json schema
The updater expects a JSON document of the following shape (this is the
standard Tauri v2 schema; see the
Tauri updater docs for the
upstream reference). The file must be a release asset named exactly
latest.json and must sign the installer artifacts, not itself.
The feed’s version is always the machine projection (YY.N.0), while its
release URL and human-facing notes retain the bare public tag/version (YY.N).
updater_signing is a sortOfRemoteNG release-contract marker used by CI and
release auditing. Tauri ignores the extra field, and the runtime/manual-install
UX derives the signed path from the selected platform entry’s signature and
applies the separate official-public policy before offering an unsigned install
action. The native command then rechecks the fixed public feed rather than the
configured endpoint list.
{
"version": "26.1.0", // machine SemVer; must be > the running app's version
"notes": "Release 26.1",
"pub_date": "2026-04-20T00:00:00Z", // RFC 3339 / ISO 8601
"updater_signing": true,
"platforms": {
"windows-x86_64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_windows-x86_64-setup.exe>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_windows-x86_64-setup.exe",
},
"windows-aarch64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_windows-aarch64-setup.exe>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_windows-aarch64-setup.exe",
},
"windows-x86_64-msi": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_windows-x86_64.msi>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_windows-x86_64.msi",
},
"windows-aarch64-msi": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_windows-aarch64.msi>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_windows-aarch64.msi",
},
"darwin-x86_64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_darwin-x86_64.app.tar.gz>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_darwin-x86_64.app.tar.gz",
},
"darwin-aarch64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_darwin-aarch64.app.tar.gz>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_darwin-aarch64.app.tar.gz",
},
"linux-x86_64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_linux-x86_64.AppImage>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_linux-x86_64.AppImage",
},
"linux-aarch64": {
"signature": "<base64 minisign signature of sortOfRemoteNG_26.1.0_linux-aarch64.AppImage>",
"url": "https://github.com/supermarsx/sortOfRemoteNG/releases/download/26.1/sortOfRemoteNG_26.1.0_linux-aarch64.AppImage",
},
},
}
Platform identifier rules (per Tauri v2):
<os>-<arch>whereos ∈ {windows, darwin, linux}andarch ∈ {x86_64, aarch64, i686, armv7}, optionally suffixed with-<installer>to address one installer format specifically.- The
signaturefield is the base64 contents of the.sigfile thattauri buildemits next to the bundle (NOT the bundle hash). - The
urlMUST resolve over HTTPS to the same artifact that was signed. pub_dateis optional for the runtime but required by our release CI for audit purposes.
The release workflow owns the generator that emits this file from the
tauri build output. scripts/ci/validate-updater-feed.mjs enforces the
schema, platform entries, artifact URLs, and signatures before a feed can be
published. It also requires valid SemVer transport metadata and the exact
expected machine projection supplied by the release workflow.
Unsigned releases use the same eight platform keys and set
"updater_signing": false plus "signature": "" on every entry. Linux
entries reference the published AppImages, Windows entries retain their NSIS
and MSI mapping, and macOS entries reference the published DMGs instead of the
signed-only app archives. Tauri requires the signature field to be a string,
so it cannot be omitted or set to null. Empty signatures are a metadata
marker, not a verification bypass. The signed Tauri path and its embedded-key
verification remain unchanged. The backend can instead re-resolve the fixed
official public feed and launch its exact matching canonical GitHub artifact
after the user explicitly acknowledges the unsigned warning for that one
attempt. The artifact is not automatically trusted, and the UI cannot choose
or substitute its URL. Configured private endpoints are never consulted by
this command; their unsigned entries and artifacts remain non-executable and
manual-only unless a future release introduces another pinned trust policy.
Clients released before the explicit official-public unsigned path may display an unsigned feed as installable and download its artifact before Tauri rejects the empty signature. They still fail closed before installation. A signed bridge release avoids that one-time UX and bandwidth limitation.
Signing-key continuity is required after publishing a signed updater release.
If the newest public release has a signed feed and a later build lacks the
updater key, promotion fails instead of replacing the usable signed
/releases/latest/download/latest.json with an unsigned feed. Restore
the signing secret (or perform an explicitly reviewed channel transition)
before retrying that release.
4.1 Per-installer Windows platform keys
tauri-plugin-updater resolves {os}-{arch}-{installer} before falling back
to {os}-{arch}, so one latest.json carries both Windows payloads:
| Install mode | Platform key resolved | Payload |
|---|---|---|
NSIS setup.exe |
windows-<arch> |
..._windows-<arch>-setup.exe |
| MSI | windows-<arch>-msi |
..._windows-<arch>.msi |
| Portable ZIP | none | externally managed |
The bare windows-<arch> keys keep their existing meaning — the NSIS
setup.exe — so already-installed NSIS clients and clients built before the
MSI keys existed are unaffected. There is deliberately no windows-<arch>-nsis
key: adding one would change nothing for current clients and risks regressing
the fallback they rely on.
MSI installs do not rely on that fallback. sorng-updater passes an
explicit .target("windows-<arch>-msi") for installMode = msi, which
disables fallback entirely. A feed without the -msi key therefore produces a
legible UpdaterTargetMissing error telling the user to install the newer
.msi from GitHub Releases, rather than silently downloading the NSIS
setup.exe and installing a second, parallel copy of the app beside the MSI
one. Operators of a
private updater endpoint
must publish the -msi keys for MSI clients to update. Note that the explicit
target also changes `` expansion in a templated endpoint URL for MSI
installs — it expands to windows-x86_64-msi, not windows-x86_64. No
endpoint in this repository uses templating.
4.2 What an MSI in-app update actually does
Tauri’s WiX template is InstallScope="perMachine", so:
- The plugin verifies the downloaded
.msiagainst the embedded pubkey before anything is executed. A bad signature aborts here;msiexecis never launched. - It then runs
msiexec /i <temp>.msi /passive /promptrestart AUTOLAUNCHAPP=TrueviaShellExecuteW, which always raises a UAC administrator consent prompt. - The app process exits immediately and unconditionally — before the outcome of that prompt is known. Declining UAC therefore cancels the update and leaves the app closed at its current version; reopening it is all that is needed. This is why Settings > Updater warns about it before the install starts.
msiexecperforms an in-place major upgrade andAUTOLAUNCHAPP=Truerelaunches the app.
The in-place upgrade depends on the new MSI carrying the same UpgradeCode
as the installed one. bundle.windows.wix.upgradeCode in
src-tauri/tauri.conf.json is pinned to exactly the value Tauri derives today
from productName, and tests/release/wix-upgrade-code.test.mjs fails if the
two ever diverge. Without that pin, renaming productName would silently start
installing every future MSI beside the installed one instead of upgrading
it. Upstream Tauri derives that string with a hardcoded x64, so the x64 and
ARM64 MSIs share an UpgradeCode; that is pre-existing behaviour and is not
changed here.
bundle.windows.wix.enableElevatedUpdateTask is deliberately not enabled.
It would suppress the UAC prompt by installing a permanently elevated Scheduled
Task, which is a standing local privilege-escalation surface; a per-update
consent prompt is the correct trade.
CI proves the MSI artifact, its .sig, and the manifest entry are correct and
cryptographically verifiable, but no CI job installs an MSI. On the first
release carrying this change, smoke-test manually: install release N’s
.msi, update in-app to N+1, and confirm a single entry in Apps & Features
and a single install directory.
5. Key rotation
Rotating the keypair is a breaking change for already-installed clients: an installed build with pubkey A cannot verify an update signed by private key B, so a rotation requires at minimum one “bridge” release signed by the old key that ships the new pubkey, followed by releases signed with the new key.
Procedure:
- Generate a new keypair (
tauri signer generate) and store in 1Password under a new entry (sortOfRemoteNG / Release signing (YYYY-MM)). - Ship release N signed with the old key; it only changes
plugins.updater.pubkeyintauri.conf.jsonto the new pubkey. Clients on version ≤ N-1 verify N with the old pubkey, install it, and thereafter hold the new pubkey. - Ship release N+1 signed with the new key; update the GitHub
secret
TAURI_SIGNING_PRIVATE_KEYto the new private key. - Retire the old entry in 1Password (keep archived, do not delete — may be needed to forensically verify historical releases).
If the old private key is compromised, skip step 2 and publish an out-of-band advisory instructing affected users to reinstall from a trusted channel; there is no way to forcibly re-key an installed client whose embedded pubkey you can no longer sign for.
6. Local verification
To confirm the plugin is wired correctly after any change to this setup:
cargo check -p sorng-updater
cargo check -p sorng-commands-core -p sorng-commands-tools
cargo check -p app # embeds pubkey at compile time
npm run lint && npm run test # covers the Settings updater UI and command facade
node scripts/ci/validate-updater-feed.mjs <latest.json> --expected-version 26.1.0 --updater-signing signed
npm run tauri:build # produces signed bundle if env vars set
A bundle built without TAURI_SIGNING_PRIVATE_KEY in the environment
will still compile but will not emit updater .sig files — that is
intentional so day-to-day local builds and public OS-installer releases do not
require the secret. Local builds do not create a release feed; the release
workflow constructs and validates its unsigned public feed from the public
artifacts instead. The explicit acknowledgement path always rechecks that
fixed official feed; configured private endpoints and their unsigned artifacts
remain manual-only.
7. Rerun, recovery, and bad-release policy
Normal releases are driven by successful main CI. If a run stops before
reserving its identity, rerun the failed CI job or manually dispatch with the
same source_sha, mode: rolling, and release_tier. If its tag already
exists, dispatch with mode: existing, the same source_sha and
release_tier, and that exact bare tag. The workflow must reuse and complete
that release idempotently.
Rolling tags are immutable evidence. Never force-move, delete, or reuse one for
another source commit. If a release is bad, do not repoint its tag or edit its
generated feed to impersonate another version. Remove it from updater
promotion where practical and publish the next YY.N as a forward fix signed
with the same updater key. Clients that already installed the bad build cannot
be safely downgraded through the normal version-monotonic updater.