sortOfRemoteNG

Private Updater Endpoint — Enterprise Deployment Guide

This document covers the Settings-managed private update feed that supplements the public GitHub Releases feed wired in updater setup guide. It lets an enterprise admin or field-support user configure a private HTTPS endpoint (typically an S3 bucket behind CloudFront, or any TLS-terminated static host) while keeping the public feed available as a fallback.

Signature verification parity: both endpoints are verified against the same embedded Ed25519 pubkey (plugins.updater.pubkey in tauri.conf.json). The private feed does NOT ship its own keypair; the release maintainer signs the same installer artifacts and uploads the matching .sig values to latest.json on the private bucket, exactly as for the public feed. If you rotate the pubkey, follow the bridge- release procedure in updater-setup.md § 5 — both feeds must continue to serve artifacts signed by the current private key.

Unsigned execution boundary: the warning-gated unsigned command does not use this configured endpoint. On every acknowledged invocation it re-resolves only the fixed PUBLIC_ENDPOINT_URL, checks the requested version and platform target there, and accepts only that feed’s exact canonical GitHub release artifact. An empty-signature private entry can report a version and offer a manual link, but the app will not download or execute its artifact. Even if a private entry copies the official URL, the command independently fetches the public feed rather than trusting that metadata. A future executable private-unsigned design would require a separately reviewed, pinned trust policy; none exists today.


1. How the two endpoints are combined

Tauri’s updater plugin queries endpoints in order and uses the first that returns a valid latest.json. sortOfRemoteNG composes the runtime list inside sorng-updater:

  1. Settings-managed private endpoint (settings.json → updater.privateEndpointEnabled + updater.privateEndpointUrl), when enabled and valid.
  2. Public GitHub Releases, always present: https://github.com/supermarsx/sortOfRemoteNG/releases/latest/download/latest.json.

The runtime uses UpdaterExt::updater_builder().endpoints(..) to pass the resolved list to tauri-plugin-updater. A private endpoint therefore checks first, falling back to public automatically if no private update is available.


2. Build-time endpoint configuration

Build-time private endpoint mutation is retired. UPDATER_PRIVATE_ENDPOINT_URL is intentionally ignored by src-tauri/build.rs; production configuration belongs in backend updater settings so it can be inspected and changed from Settings > Updater without changing the signed app bundle.


3. Enable at runtime (Settings / MDM)

Users can configure the endpoint in Settings > Updater. MDM systems can also push the same settings file to each machine:

Windows: %APPDATA%\com.sortofremote.ng\settings.json macOS: ~/Library/Application Support/com.sortofremote.ng/settings.json Linux: ~/.config/com.sortofremote.ng/settings.json

Contents (merges with any existing keys — Intune / Jamf / Ansible etc. should preserve other top-level entries):

{
  "updater": {
    "privateEndpointEnabled": true,
    "privateEndpointUrl": "https://updates.corp.example.com/sortofremoteng/latest.json"
  }
}

Legacy updater.private_endpoint is still read for migration, but new writes use the camelCase keys above. Invalid production URLs are rejected by the backend settings command; local debug HTTP is the only non-HTTPS exception. The app never allows a private endpoint to override the embedded pubkey; signature verification always uses the committed key.


4. S3 bucket layout

A minimal layout that works out of the box with the Tauri updater:

s3://sortofremoteng-updates/
├── latest.json                                      (public-read)
├── 26.1.0/
│   ├── sortOfRemoteNG_26.1.0_x64_en-US.msi           (public-read)
│   ├── sortOfRemoteNG_26.1.0_x64_en-US.msi.sig       (public-read)
│   ├── sortOfRemoteNG_26.1.0_x64.app.tar.gz
│   ├── sortOfRemoteNG_26.1.0_x64.app.tar.gz.sig
│   ├── sortOfRemoteNG_26.1.0_aarch64.app.tar.gz
│   ├── sortOfRemoteNG_26.1.0_aarch64.app.tar.gz.sig
│   ├── sortOfRemoteNG_26.1.0_amd64.AppImage
│   └── sortOfRemoteNG_26.1.0_amd64.AppImage.sig
└── 26.2.0/…

Conventions:

  • latest.json is always at the bucket root and overwritten on each release (cache-control max-age=60 so the CDN picks up new releases within a minute).
  • Installer + .sig file names match the tauri build output exactly; renaming breaks the embedded signature check.
  • Directory, artifact, and feed versions use machine SemVer YY.N.0. Public GitHub tags and release labels both use bare YY.N (no v prefix).
  • The bucket is fronted by CloudFront (or your CDN of choice) with ACM-issued TLS. Origin access is typically OAI / OAC so only the CDN can read — clients only ever talk to the CDN hostname.
  • The URL configured in Settings or managed settings is the CDN hostname plus /latest.json, e.g. https://updates.corp.example.com/sortofremoteng/latest.json.

5. latest.json schema (signed-feed variant)

{
  "version": "26.1.0",
  "notes": "Enterprise build for release 26.1.",
  "pub_date": "2026-04-20T00:00:00Z",
  "updater_signing": true,
  "platforms": {
    "windows-x86_64": {
      "signature": "<base64 minisign signature of the x64 setup.exe artifact>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_windows-x86_64-setup.exe",
    },
    "windows-x86_64-msi": {
      "signature": "<base64 minisign signature of the x64 .msi artifact>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_windows-x86_64.msi",
    },
    "windows-aarch64": {
      "signature": "<base64 minisign signature of the ARM64 setup.exe artifact>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_windows-aarch64-setup.exe",
    },
    "windows-aarch64-msi": {
      "signature": "<base64 minisign signature of the ARM64 .msi artifact>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_windows-aarch64.msi",
    },
    "darwin-x86_64": {
      "signature": "<base64 minisign signature>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_x64.app.tar.gz",
    },
    "darwin-aarch64": {
      "signature": "<base64 minisign signature>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_aarch64.app.tar.gz",
    },
    "linux-x86_64": {
      "signature": "<base64 minisign signature>",
      "url": "https://updates.corp.example.com/sortofremoteng/26.1.0/sortOfRemoteNG_26.1.0_amd64.AppImage",
    },
  },
}

The bare windows-<arch> entries are always the NSIS setup.exe payloads. MSI installs are pinned to windows-<arch>-msi and do not fall back to the bare key, so a private feed supporting Windows must publish both keys for each architecture it distributes.

The private endpoint must serve the signed variant to use the verified automatic installation path; it is not a place to bypass updater verification. The signature values are the same ones you’d publish on a signed public GitHub release — both feeds point at different URLs but the bytes at those URLs are byte-identical (or at least both signed by the same private key). An official public unsigned release can use the separate per-use warning and acknowledgement path. That command always re-resolves the fixed public feed and never consults this private endpoint. A private empty-signature entry is available only for version discovery and manual download; its artifact remains non-executable unless a future app release defines another pinned trust policy.


6. IAM policy example

Two principals typically need access to the bucket:

6.1 CI / release publisher (write)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublishReleaseAssets",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl",
        "s3:AbortMultipartUpload",
        "s3:ListBucketMultipartUploads"
      ],
      "Resource": [
        "arn:aws:s3:::sortofremoteng-updates/latest.json",
        "arn:aws:s3:::sortofremoteng-updates/*/*"
      ]
    },
    {
      "Sid": "InvalidateCdnCache",
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::<ACCOUNT>:distribution/<DIST_ID>"
    }
  ]
}

Attach this policy to the IAM role assumed by the release CI workflow (e.g. via OIDC federation from GitHub Actions). Never use long-lived access keys.

6.2 CloudFront origin access (read)

Use an Origin Access Control (OAC) on the CloudFront distribution; the bucket policy then allows s3:GetObject only from that OAC:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontReadOnly",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::sortofremoteng-updates/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT>:distribution/<DIST_ID>"
        }
      }
    }
  ]
}

Block all public access at the bucket level (BlockPublicPolicy, IgnorePublicAcls, BlockPublicAcls, RestrictPublicBuckets = true).


7. Deploy steps — release maintainer runbook

  1. Build the bundle with signing env vars set (see updater setup guide § 2). tauri build emits installers + .sig files under src-tauri/target/release/bundle/.
  2. Assemble latest.json with the base64 .sig values and the CloudFront URLs for each platform (see § 5 above). Prefer the validated feed and artifacts from the automatic release for the same source SHA; do not copy unsigned artifacts into this private automatic-install channel. Private unsigned entries remain manual-only; the warning-gated command independently checks the fixed public feed instead.
  3. Upload to S3 (versioned dir + root latest.json):
    aws s3 cp src-tauri/target/release/bundle/ \
      s3://sortofremoteng-updates/26.1.0/ --recursive \
      --exclude "*" --include "*.msi" --include "*.msi.sig" \
      --include "*.tar.gz" --include "*.tar.gz.sig" \
      --include "*.AppImage" --include "*.AppImage.sig"
    aws s3 cp latest.json s3://sortofremoteng-updates/latest.json \
      --cache-control "max-age=60"
    
  4. Invalidate the CDN for /latest.json:
    aws cloudfront create-invalidation \
      --distribution-id <DIST_ID> --paths "/latest.json"
    
  5. Smoke-test on a staging workstation with the private endpoint configured in Settings > Updater: launch the app, check for updates, confirm the new version is reported, then run Download and install to verify the Tauri updater accepts the signed artifact.
  6. Confirm public promotion by verifying that the automatic GitHub Release for the same source SHA contains byte-identical signed artifacts and latest.json. If its CI run failed, recover that same release rather than reserving a new rolling identity.

8. Bad release handling

If a release is bad, overwrite latest.json at the bucket root with the previous release’s manifest and invalidate the CDN. Clients that have NOT yet installed the bad version will simply see the prior version again.

Clients that already installed the bad version must receive a forward fix release signed with the same updater key. P1 does not provide an in-app rollback installer, copy installer, or channel-history rollback command. Never force-move or delete the immutable public rolling tag to simulate a rollback.


9. Security notes

  • TLS-only: https:// is required for production. Local debug HTTP is allowed only for development builds. Do not deploy an http:// endpoint to real users — the signature check still protects the payload, but metadata (release cadence, version numbers, user IPs) is sent in cleartext.
  • Pubkey never changes per-endpoint. If the private feed needs a different trust anchor, that is a new app build — both feeds must sign against whatever key is embedded.
  • Unsigned acknowledgement is not endpoint trust. It authorizes one exact public-feed artifact attempt. The command ignores the configured private endpoint and rechecks PUBLIC_ENDPOINT_URL; private unsigned artifacts must be downloaded and handled manually until a future release provides an additional pinned trust policy.
  • Settings file is plaintext. The private endpoint URL is not a secret (its hostname is visible on the wire anyway); if you need the URL to be confidential, gate access at the CDN / VPN / firewall layer rather than encrypting the settings file.
  • Windows note: %APPDATA% resolves to C:\Users\<user>\AppData\Roaming. MDM pushes should target that path per user and preserve other settings keys; there is no build-time private-endpoint override.

Find your next step

Search documentation

Search page titles and the full documentation text.

    ↑ ↓ Browse Enter Open Esc Close Search stays in this tab.