Set up browser posture

Turn on browser posture with the kya-os CLI, serve the beacon from your own domain, verify the signed token on your server, and check the result on a live page

Browser posture gives your server a signed copy of Checkpoint's verdict on a browser session, so a protected request can carry it. This page takes the CLI path from opt-in to a verified token on a live page.

A verified token shows that Checkpoint issued it for this page and this session, and it carries Checkpoint's verdict. It doesn't identify the visitor, and a server that finds no token or a refused one should treat the request as having no posture, never as permission.

Prerequisites

  • kya-os 2.3.1 or later, signed in with kya-os login. Posture calls use your management sign-in, and enabling posture and changing origins need write access to the project. See the CLI reference.
  • @kya-os/checkpoint-beacon 1.4.3 or later on the pages. The CLI installs the release it verified. Below 1.4.3 it installs the beacon without posture wiring and prints a note; a run at 1.4.3 or later upgrades the install in place.
  • Pages served over HTTPS. The beacon doesn't store the token on other pages.
  • A server or edge you control that can read two request headers and verify an ES256 signature.

Turn on browser posture

From your app's directory, add the beacon and turn posture on in one command:

kya-os setup --surface beacon --posture --origins https://www.example.com,https://shop.example.com

kya-os detect install takes the same three flags: --posture, --origins, and --proxy. See Browser posture for the table.

With a terminal and no --yes, the CLI asks two questions instead (with --posture and no --origins, only the second, also when posture is already on). Turn on browser posture? defaults to No. Other page origins allowed to carry posture is prefilled with the project's current list, an invalid entry is refused with the reason and asked again, and a blank answer removes all additional origins. Without a terminal, nothing is asked: posture turns on only when you pass --posture.

Checkpoint issues a token only to pages on the project's domain and the origins you list. --origins is a comma-separated list, and it replaces the project's additional origins: an origin you leave out is removed, and the CLI reports what it added and removed. A bare host such as shop.example.com becomes https://shop.example.com, and a path or query is dropped. Wildcards, the apex rule, and the limit of 20 are in Verifying the posture token.

On setup, --origin (singular) is the Checkpoint address you sign in to. Page origins are --origins (plural).

After it turns posture on, the CLI checks the key without asking you. It fetches the project's keys URL and compares the key id and public key with what the project reports. The result is one line, key: confirmed at the keys endpoint or key: not confirmed, with a note that says why, and keyConfirmed in --json output. The reference has the retry rules.

A dry run (--dry-run) turns nothing on. If posture is already on and you pass neither flag, the CLI says so and prints the key id. Without a sign-in, detect install --posture stops before changing anything and asks you to run kya-os login.

Tokens start flowing within 5 minutes; Verifying the posture token explains the delay.

Check what the CLI wrote

--surface beacon checks the release before it installs it. It reads the latest @kya-os/checkpoint-beacon from the npm registry, checks the tarball against the sha512 integrity the registry lists, and computes the script's sha384 integrity hash from the tarball's beacon.min.js. It then fetches the same file from jsDelivr and compares the bytes. If either check fails, nothing is installed.

What lands in your repository depends on the stack. The reference has the full table; the parts that matter for posture are:

  • A pinned beacon. A script tag gets a versioned jsDelivr URL with integrity and crossorigin="anonymous". An npm install is exact-pinned to the verified version.
  • Self-hosted files. For an HTML page with a static directory, the CLI writes beacon.worker.js from the verified tarball into _checkpoint/<version>/ at any beacon version. For a package install at beacon 1.4.3 and later, it writes the worker and beacon-lazy-signals.min.js into _checkpoint/<version>/ under your static directory (public/ for Next.js). It sets workerUrl, and lazySignalsUrl where it wrote the chunk, to those paths. A page that enforces a strict Content-Security-Policy can then load them from its own origin.
  • A helper for the two posture headers. The generated code exports checkpointPostureHeaders(), or window.checkpointPostureHeaders() in an HTML page. It returns KYA-Posture-Session always and KYA-Posture while the latest token hasn't expired.

Use the helper on requests to a protected API on another host, where the beacon's host-only cookies don't travel:

import { checkpointPostureHeaders } from './checkpoint-detect';

await fetch('https://api.example.com/generate', {
  method: 'POST',
  headers: { ...checkpointPostureHeaders() },
});

For a cross-origin request, add both header names to the API's Access-Control-Allow-Headers: KYA-Posture, KYA-Posture-Session. The CLI prints that line when it turns posture on.

Files the CLI wrote itself are recognized on the next run by comparing their whole text with what it generates, so a second run updates them in place. A block or component you've edited, or a beacon install the CLI didn't write, is left alone with a message saying why.

Serve the beacon through your own domain

A page with a strict Content-Security-Policy may not allow connect-src https://kya.vouched.id or a script from a CDN. --proxy moves both onto your origin under a path you choose:

kya-os detect install --surface beacon --posture --proxy /_cp

The path takes letters, digits, _, -, and /, up to 64 characters including the leading slash, with no trailing slash. The beacon's endpoint becomes /_cp/api/v1/beacon, which your domain forwards to https://kya.vouched.id/api/v1/beacon. The forwarding itself uses the mechanism in Forward edge evidence: your edge sets X-API-Key and KYA-Forwarded-IP on the proxied request, and Checkpoint scores it with the visitor's address instead of your proxy's.

The key the proxy uses is minted with the proxy purpose, which carries only the forward permission. It can authenticate forwarded evidence and nothing else, and it's shown once. Minting needs a project owner or admin. When you're signed in, the CLI mints it, stores it as CHECKPOINT_PROXY_KEY in a gitignored env file, and never prints it. Signed out, it tells you to sign in and re-run, or to set CHECKPOINT_PROXY_KEY yourself.

Next.js gets a helper, checkpoint-beacon-proxy.ts, and one entry file written into your app: proxy.ts on Next.js 16 and later, middleware.ts before that, under src/ when the app uses src/app or src/pages. For nginx, Cloudflare, and Akamai, the CLI prints the configuration for you to add and applies nothing. The reference lists each target. Checkpoint hasn't verified the printed Akamai rule against a live property, so test it in staging.

Every generated proxy removes the visitor's Cookie and Authorization headers before it sets the key and the visitor's IP. The Next.js proxy and the Cloudflare Worker also remove every KYA-Forwarded-* header the visitor sent. The nginx block and the Akamai rule clear the JA4, challenge, and challenge-id headers by name, so delete any other KYA-Forwarded-* header yourself. An existing entry file that the CLI didn't write is never overwritten; the CLI prints the lines to compose into it instead. On Vercel and other hosts, .env.local isn't deployed, so set CHECKPOINT_PROXY_KEY in the host's environment too.

With the proxy in place, connect-src needs only 'self', and script-src needs only 'self' when the script is self-hosted. The CLI prints the exact directives for your install, and the reference lists what each one needs.

A project with no static directory still loads the script from jsDelivr, and the CLI says so. It always lists base-uri 'none' and trusted-types checkpoint-beacon; add them only if the page uses a nonce-based policy or enforces Trusted Types. It only claims the collapsed policy when every step landed; a skipped or failed step counts as not in place.

Verify the token on your server

Your server reads the KYA-Posture and KYA-Posture-Session headers, or the matching cookies when the request stays on the same host. Which one wins, the cookie names, and the token format are in Verifying the posture token.

The server SDKs verify for you. They need the project's UUID (the lowercase UUID, not the friendly id), the key, and the page origin. Get the key id and public JWK from the Next.js, Express, or ASP.NET Core block the CLI prints when it turns posture on (--posture on a project that's already on prints it again; the Java block carries the keys URL only, and no block prints without a usable page origin), from posture.state.keyId and posture.state.publicJwk in --json output, or from the Installations card in step 6. The examples read them from environment variables. When one server answers for several origins, list the extra ones next to the primary:

middleware.ts
export default withCheckpoint({
  projectId: '<lowercase project UUID>',
  browserPosture: {
    origin: 'https://www.shop.example',
    origins: ['https://checkout.shop.example', 'https://*.shop.example'],
    keyId: process.env.CHECKPOINT_POSTURE_KEY_ID!,
    publicKey: JSON.parse(process.env.CHECKPOINT_POSTURE_PUBLIC_JWK!),
  },
});

origin stays the primary and is required. origins is additional to it, not a replacement, so a config without origins behaves as before. Each list takes the same entries the CLI accepts, up to 20. The TypeScript verifier refuses wildcards over shared suffixes, as the CLI does; the Java and .NET verifiers don't check, so list only hosts you own. Java and .NET also refuse a primary origin that isn't canonical: https, a lowercase host, no default port, and no path.

The SDK verifiers compare the token's own signed origin claim with origin and origins, then check that aud names that same origin. They don't use the request's Host or Origin header for this, and they never take the key from the token: you supply it, either pinned or, in Java, fetched from the keys URL. The gateway works differently and needs no configuration: it matches the request's own origin against the project's domain and additional origins, then checks the token against the match.

PackageMinimum version
@kya-os/checkpoint-shared (used by @kya-os/checkpoint-express and @kya-os/checkpoint-nextjs)1.5.2
Java ai.knowthat.checkpoint0.2.2
.NET KyaOs.Checkpoint1.11.9
@kya-os/checkpoint-wasm-runtime (Next.js and Express postureOutcome reporting)1.10.1

A refused token doesn't stop the request. The request proceeds as if it carried no posture, and a refused token never grants an allow or overrides a block. The only trace is telemetry: the SDK sends postureOutcome with status of verified, absent, or rejected, and a reason on every rejected, to POST /api/v1/log-detection.

ReasonWhat it means
malformedNot a well-formed token, or its header is unusable.
unknown-keyThe token's kid isn't the key you configured.
bad-signatureThe signature doesn't verify against your key.
invalid-claimsA claim is missing or the wrong shape, the issuer is wrong, or the lifetime is over 120 seconds.
project-mismatchproject_id isn't the project you configured.
origin-mismatchThe token's origin is neither origin nor one of origins.
audience-mismatchaud doesn't name that origin.
expiredThe token's lifetime has ended.
not-yet-validiat or nbf is in the future.
binding-mismatchbinding isn't the SHA-256 of the session id you were sent.
session-missingA token arrived with no session id.

A run of origin-mismatch, audience-mismatch, unknown-key, or bad-signature while the beacon is issuing tokens usually means a configuration gap: an origin missing from origins, or a key that changed. Turning posture off and on changes the key id, which Verifying the posture token covers.

Check the page with detect doctor

kya-os detect doctor fetches a live page and reports whether the beacon and its posture path can work there:

kya-os detect doctor --url https://www.example.com

It reads the page's HTML, then probes the files and headers the beacon needs, and reads up to 20 same-origin scripts only when the page has no beacon.min.js tag. Each check reports ok, info, warn, or fail, and the command exits 1 if any check fails. The reference lists every check; the ones that catch most posture problems are:

  • Beacon files. The script, the lazy-signals chunk, and the worker each answer 200 and are served as JavaScript, and the script's bytes match the integrity hash on its tag.
  • Content-Security-Policy. script-src, worker-src, and connect-src allow what the page loads and the beacon's endpoint. A page that requires Trusted Types must allow the checkpoint-beacon policy that beacon 1.4.3 and later create.
  • Proxy preflight. A proxied endpoint on another origin answers a browser's OPTIONS request.
  • Posture, when you're signed in. The page's origin is allowed to carry posture, the keys endpoint serves the project's key, and the page's beacon uses the project you compared.

An example of a failing run:

Checked https://staging.example.com/
  [ok  ] page: https://staging.example.com/ answered 200
  [ok  ] beacon script integrity: the served bytes match the page's integrity hash
  [FAIL] posture origin: https://staging.example.com is not among the allowed origins (https://www.example.com, https://shop.example.com)
         fix: Run: kya-os detect install --surface beacon --posture --project <project uuid> --origins 'https://shop.example.com,https://staging.example.com' (--origins replaces the whole list, so this keeps the current additional origins)
1 problem(s) found.

The doctor doesn't check that your page forwards KYA-Posture headers to your API or that the API allows them in CORS. Test that with a real request.

Watch it in Installations

Open Installations in the dashboard for the project, at /dashboard/<org>/<project>/installations. The Browser posture card shows, top to bottom:

  • State. An Enabled or Off tag. When it's off, the card offers Enable browser posture and nothing else.
  • Key id, Primary origin, and Keys URL. The primary origin comes from the project's Site URL, and the card says to set one if it's empty. The key id and keys URL have copy buttons.
  • Additional site origins. The same list --origins sets, with the same validation and a limit of 20. Save origins saves without touching the rest of the project.
  • Public key. The JWK for your server's configuration, with a copy button.
  • Posture health. How the last 24 hours of beacon responses went, by status and page origin.

Posture health shows the share of responses that were issued a token, then a row per status and origin. Each status has a label: Issued, Origin not allowed, Site URL not set, Not enabled, Signer unavailable, and State unavailable. An Origin not allowed row means a page on that origin asked for a token and was refused. Add the origin under Additional site origins only if you own it: anyone can send a request that claims any origin.

The counts are approximate. Checkpoint counts responses as they happen instead of reading stored detections, and it buffers repeats briefly, so a burst can undercount. The origin on each row is what the browser reported, and it isn't verified.

Each hour keeps 50 named origins for each status and sums the rest as other origins, not listed. A flood of made-up origins can't hide your totals, though it can push a rarely seen real origin into that row. The first occurrence of an origin in each short window is written straight away, so your own first page load shows up without waiting.

A Server-side verification section lists Verified, No token sent, and Rejected counts, with the reason for each rejection, once your servers report postureOutcome.

To turn posture off, use Turn off browser posture and confirm. Pages stop receiving tokens and the signing key is removed. Checkpoint refuses while human step-up (Turnstile) is configured and says so; Verifying the posture token explains why. Anyone who can read the project sees the health view. Enabling, turning off, and editing origins need write access.

Next steps