Documentation

Policy Signing, Rollout and Rollback

How policy bundles are signed and verified on every sensor, how to stage a change, roll back to an earlier version or switch the organization to monitor-only in an emergency, and what each failure state means.

View Markdown

Every policy change you save compiles a new, numbered policy version. This page covers how that version reaches your sensors safely: signed by the service, verified on the endpoint, staged to part of the fleet first if you choose, and reversible.

Open Policies → Distribution to see and change everything described here. Every action on that panel is written to the audit log with the person who took it, and the reason when one is required.


1. Signed policy bundles

When signing is configured, the service signs each policy bundle with an Ed25519 key before sending it to a sensor.

  • What is signed. The bundle content, the organization it belongs to, its version number, the requester's role context, the distribution state (rollout channel, emergency monitor-only mode, whether signatures are required), the signing key ID, and a validity window (issued, expires).
  • How. The payload is serialized as canonical JSON (RFC 8785), and the signature covers those exact bytes in a DSSE envelope.
  • Where it is checked. The browser extension, the desktop agent, the MCP agent and the desktop app each verify the signature against public keys built into that release. A key that arrives with a bundle is never trusted. The AI gateway reads policy straight from the database, so it checks the signature stored with each policy version instead.
  • When it is checked. Before a bundle is used, before it is written to the local cache, and again every time the cache is loaded. Editing the cached bundle on disk, or in browser storage, makes it fail verification, and the sensor refuses it.

Compatibility mode, then required

Sensors accept unsigned bundles until your organization requires signatures. This lets new sensor releases reach your fleet before the service starts signing, without any interruption.

When you choose Require signatures, the next signed bundle each sensor verifies tells it so. From then on that sensor refuses unsigned bundles permanently, even if a later response claims otherwise. Only a correctly signed bundle can lift the requirement. This prevents a downgrade attack in which someone strips the signature from a bundle.

Device management can also require signatures from the very first bundle:

SensorSetting
Browser extensionManaged storage policy requirePolicySignatures: true
Desktop agent, MCP agent, desktop appEnvironment variable ORTHALON_REQUIRE_POLICY_SIGNATURES=true

Only with a pinned key. Set these only on sensor releases that have the signing key built in. A release with no built-in key cannot verify anything, so with the setting on it refuses every bundle and keeps enforcing whatever it already has.

Limit. Signing protects policy in transit and in the local cache against tampering by anything that is not already a local administrator. A local administrator can replace the sensor itself. Pair signing with managed installation, as described in Sensors and keys.


2. Staged rollout

A staged rollout holds most of the fleet on the current version while your next changes reach only a cohort.

  1. Choose Start staged rollout and set a percentage, named groups, or both. The version current at that moment becomes the held version.
  2. Edit your policies as usual. Each change compiles a new version, and only the cohort receives it.
  3. Watch the panel. It shows how many sensors are in the cohort and how many already report the staged version, computed from what sensors report when they fetch policy.
  4. Raise the percentage, add groups, then Promote to everyone. Alternatively, Abort and restore (see below).

How the cohort is chosen. Each sensor's membership is fixed by a hash of its sensor ID and a per-organization salt. Raising the percentage only ever adds sensors, so the 10% cohort is contained in the 25% cohort.

Named groups. You assign groups (rings such as pilot or it-staff) per sensor in the sensor list on the same panel. A sensor in a named group is in the cohort regardless of the percentage.

Lowering the percentage or removing a group does not take the staged version away from a sensor that already has it. Sensors refuse to go back to a lower version number. Those sensors stay on the staged channel until you promote or abort.

The gateway follows the rollout per gateway key.


3. Rolling back

Choose Roll back to this next to any retained version, and give a reason. Orthalon publishes a new version whose content is identical to the version you chose.

  • Sensors only move forward in version numbers, so a rollback is always a new number.
  • A rollback goes to every sensor and ends any staged rollout. Abort and restore is a rollback to the held version.
  • Orthalon retains the 25 most recent versions, plus the held version of an active rollout.
  • Your next policy edit compiles from the current policy definitions again. To keep the rolled-back behaviour, also revert the policy that caused the problem.

4. Emergency monitor-only mode

Switch to monitor-only downgrades every enforcing decision across the organization to monitor-and-log for a fixed time, between 5 minutes and 7 days. A reason is required. Sensors keep recording what policy would have done, but block, redact, approval, coaching, blocked applications and download file-type blocks all stop enforcing.

  • The window is judged on the sensor's own clock. Enforcement resumes on time even on a device that cannot reach the service.
  • Ending it early. Choose Resume enforcement now to end the window before it expires.
  • Older sensor releases that predate signing receive a bundle in which every rule is monitor-only and nothing is blocked. They resume enforcing at their next policy refresh after the window ends.

5. How quickly changes arrive

Rollbacks, rollout changes, monitor-only mode and the signature requirement are announced on the same realtime channel that announces policy edits. Online sensors with a realtime connection typically fetch the change within seconds.

Without a realtime connection:

  • Browser extension. Refreshes every 30 minutes, and within 5 minutes of activity.
  • Desktop agent. Reconciles every 30 minutes by default (ORTHALON_POLICY_REFRESH_SECONDS), and within 5 minutes of activity.

Use the Versions card to watch how much of the fleet reports the version it should have.


6. Failure states

Each sensor reports the integrity of the policy it is enforcing on every policy request. The Distribution panel shows the state per sensor.

When an update is refused, the sensor keeps enforcing its last good bundle and reports why. A refusal is never silent.

StateMeaningWhat to do
Verified signatureThe bundle in force carries a valid signature from a built-in key.Nothing.
UnsignedCompatibility mode: the service is not signing, or the sensor release has no matching built-in key.Configure signing; update the sensor.
Stale (signature expired)The cached bundle's signature window ended while the device was offline. The bundle is still enforced, because discarding the only policy would leave the device unprotected.Reconnect the device; the next fetch renews it.
Refused: invalid signatureAn update, or the cache, did not verify. It was altered, or it was signed by a key this release does not trust.Investigate tampering or interception on that device. If you just rotated keys, check that the sensor release pins the new key.
Refused: unsignedSignatures are required and an unsigned bundle arrived.Check that the service's signing key is configured and valid.
Refused: expiredAn update arrived with an expired signature, which suggests a replayed old response or a badly wrong device clock.Check the device clock and the network path.
Refused: invalid bundleWrong organization, a lower version than the one in force (rollback), replayed distribution state, or a malformed bundle.Check the device's enrollment.
Not reportedThe sensor release predates integrity reporting.Update the sensor.

On the endpoint, these states also appear in the extension's sync error (invalidPolicy with the state) and in the desktop agent's control status (policy.integrity).


7. Operating signing (administrators of the deployment)

Environment variables

VariableWherePurposeDefault
POLICY_BUNDLE_SIGNING_KEYSingest, web and the background jobs (Trigger.dev)JSON array of { "keyId", "privateKey" }, with the PKCS#8 PEM (newlines as \n) or base64 DER. Every listed key signs.unset: bundles are served unsigned
POLICY_SIGNATURES_REQUIREDingest, web, gatewaytrue requires signatures for every organizationfalse
POLICY_BUNDLE_SIGNATURE_TTL_DAYSingestValidity window of a delivered signature, 2–9014

Generating a production key

Generate the key on an offline or otherwise trusted machine, and store the private key only in your secret manager.

openssl genpkey -algorithm ED25519 -out policy-signing-2026-10.pem
# Public key to pin in client builds (base64url of the raw 32 bytes):
openssl pkey -in policy-signing-2026-10.pem -pubout -outform DER | tail -c 32 \
  | base64 | tr '+/' '-_' | tr -d '='

Never paste the private key into a ticket, chat or log.

Enabling signatures in production, in order

  1. Update the database schema (it adds the distribution table and columns) before deploying the web app and ingest. Policy delivery reads the new table on every request.
  2. Pin the key. Generate the key. Add { "keyId": "orthalon-policy-2026-10", "publicKey": "<base64url>" } to packages/policy-engine/src/trust-anchors.ts and apps/desktop-app/src-tauri/policy-trust-anchors.json. A test keeps the two files identical.
  3. Ship pinned clients. Release the extension, desktop app (sidecar and MCP agent) and gateway with the key pinned. Wait until the Integrity column shows the new releases reporting across the fleet. Sensors on older releases show Not reported.
  4. Start signing. Set POLICY_BUNDLE_SIGNING_KEYS on ingest, web and the background jobs, then redeploy. Sensors move from Unsigned to Verified signature. New policy versions are also signed when they are stored.
  5. Watch for refusals for a few days.
  6. Require signatures for one pilot organization from the panel. Doing so first signs that organization's stored versions that predate signing (trust on first sign: a version whose signature no longer matches is left refused, not re-signed).
  7. Require them for everyone. Either do it per organization, or run pnpm --filter @repo/api exec tsx scripts/sign-policy-versions.ts once against the production database with the signing keys set, and then set POLICY_SIGNATURES_REQUIRED=true.

Rotating the signing key

Rotation follows the same principle as TUF key rotation: clients trust the new key before the server relies on it.

  1. Pin the new key. Generate it, pin it next to the old one in both trust-anchor files, and ship the releases.
  2. Sign with both keys. Once the new releases are widespread, list both keys in POLICY_BUNDLE_SIGNING_KEYS. Every bundle then carries both signatures, and old and new releases each verify the one they trust.
  3. Retire the old key from the server. Remove the old key from POLICY_BUNDLE_SIGNING_KEYS. Then run scripts/sign-policy-versions.ts once, so stored versions signed only by the old key are signed again with the new one. Otherwise the gateway and ingest stop serving those versions once the old key leaves client builds.
  4. Retire the old key from clients. In a later release, remove the old public key, or give it a notAfter date so bundles issued after that date are no longer accepted on its signature.

If a key is compromised, ship a release that removes it from the trust anchors as quickly as possible. Then sign only with the replacement. Sensors that still require signatures and do not yet have the new key will keep enforcing their last good policy, and report Refused: invalid signature until they are updated.