Skip to content

capsule and verify — sending a proof

A capsule is one file that holds one finding: the request, the identities the check names, the declared expectation, the markers, and optionally what you observed. You attach it to a report. The person who receives it runs veridor verify on it against their own instance and gets one of three answers: CONFIRMED, NOT_REPRODUCED or INCONCLUSIVE.

A capsule carries no credential that Veridor's rule recognises. It names the environment variables the verifier must set from their own deployment.

When to use it

When the target is not yours: you found an authorization bug in someone else's software and need the maintainer to reproduce it. It is also a way to hand one finding to a colleague without handing over your configuration.

For a target you own, veridor run is the tool. A capsule holds one check, and verify gives one verdict.

veridor capsule flags

Flag Default Does
--config PATH required the configuration the check is in
--check ID required the id of the check to prove
--from-run PATH none a run JSON whose finding for this check becomes the capsule's claim, so the reader sees what you observed
--image REF none the container image you used, ideally digest-pinned. Recorded verbatim
--output PATH stdout where to write the capsule. It may not be the configuration or the --from-run file

Write one

The configuration must reference credentials through ${ENV_VAR}. The shipped fixtures/veridor.demo.json writes its demo tokens literally, so it is refused:

veridor capsule --config fixtures/veridor.demo.json --check invoice-tenant-isolation --output proof.json
veridor: identity 'invoice_owner' carries a literal Authorization value, and a capsule is meant to be attached to a disclosure and read by someone else. Move it to an environment variable first, for example "Authorization": "Bearer ${VERIDOR_INVOICE_OWNER_TOKEN}", and re-run. Nothing has been written.

Exit code 2, and no file. There is no flag to override it. See a literal credential is refused for what the rule recognises.

For the rest of this page, veridor.capsule-demo.json is a copy of the demo configuration with each Authorization value moved to a variable (Bearer ${VERIDOR_OWNER_TOKEN}, ${VERIDOR_OTHER_TOKEN}, ${VERIDOR_ADMIN_TOKEN}) and target.base_url set to http://127.0.0.1:18805, where the demo target ran with --vulnerable. First a run, to have a finding to quote:

export VERIDOR_OWNER_TOKEN=owner-token VERIDOR_OTHER_TOKEN=outsider-token VERIDOR_ADMIN_TOKEN=admin-token
veridor run --config veridor.capsule-demo.json --json run.json
…
Veridor run 20261110T043547Z-dab5f6: 5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
Target: http://127.0.0.1:18805
…

Then the capsule:

veridor capsule --config veridor.capsule-demo.json --check invoice-tenant-isolation --from-run run.json --output proof.json
Wrote a capsule for check 'invoice-tenant-isolation' to proof.json
  It carries NO credential. Whoever verifies it must set: VERIDOR_OTHER_TOKEN, VERIDOR_OWNER_TOKEN
  They run: veridor verify proof.json --base-url https://their-instance

Exit code 0. The file, abridged:

{
  "kind": "veridor.capsule",
  "schema_version": 1,
  "created_at": "2026-11-10T04:35:53Z",
  "claim": {
    "check_id": "invoice-tenant-isolation",
    "finding_id": "0ec4c5cbd79deaed6e9f",
    "identity": "other_tenant",
    "verdict": "vulnerable",
    "evidence_grade": "confirmed",
    "statement": "Identity 'other_tenant' received HTTP 200 (declared: one of [404]) and …",
    "observed_in_run": "20261110T043547Z-dab5f6"
  },
  "scope": "Veridor evaluates the expectations you declared. …",
  "target": {"base_url": "http://127.0.0.1:18805", "timeout_seconds": 3},
  "identities": {
    "anonymous": {"headers": {}},
    "invoice_owner": {"headers": {"Authorization": "Bearer ${VERIDOR_OWNER_TOKEN}"},
                      "marker": "vdr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1"},
    "other_tenant": {"headers": {"Authorization": "Bearer ${VERIDOR_OTHER_TOKEN}"}, …}
  },
  "check": {"id": "invoice-tenant-isolation", "request": {…}, "expect": {…}},
  "requires_environment": ["VERIDOR_OTHER_TOKEN", "VERIDOR_OWNER_TOKEN"],
  "how_to_verify": […]
}

What it carries and what it leaves out:

  • Only the identities the check names. administrator is in the configuration and not in this check, so it is not in the capsule.
  • The configuration as written, before ${ENV_VAR} substitution. The whole finished document is scanned for literal credentials, not only the identity headers. Recognition is by name and position, so a secret under a name the rule does not know is not caught. Read the capsule before you send it.
  • A denied identity's marker_probe, when it has one. It is what lets a write-method capsule reach NOT_REPRODUCED (below).
  • VERIDOR_* names only. verify reads no other variable, so capsule renames anything else and says so:
Wrote a capsule for check 'invoice-tenant-isolation' to renamed-proof.json
  It carries NO credential. Whoever verifies it must set: VERIDOR_INVOICE_OWNER_OWNER_TOKEN, VERIDOR_OTHER_TOKEN
  Renamed for the verifier, who can supply only VERIDOR_* variables: OWNER_TOKEN -> VERIDOR_INVOICE_OWNER_OWNER_TOKEN
  They run: veridor verify renamed-proof.json --base-url https://their-instance

That was the same configuration with invoice_owner sending Bearer ${OWNER_TOKEN}.

Without --output, the capsule is printed on stdout and a rename note goes to stderr, so the output can be redirected to a file intact.

veridor verify flags

verify is an alias for run. Given a capsule path as its argument, it runs the one check in it and accepts only these options:

Flag Does
--base-url URL the instance you are verifying. Required unless the capsule's target is this machine's loopback and the check only reads
--output-dir DIR run history directory (default .veridor)
--read-only refuse the capsule before sending if it has any POST, PUT, PATCH or DELETE
--json PATH write the run document
--sarif PATH write SARIF
--no-prove-liveness skip the liveness control. The verdict then says it was off

Any other run flag is refused, not ignored:

veridor verify proof.json --fail-on any
veridor: verify <capsule> does not apply --fail-on; refused rather than ignored. A capsule run has its own exit codes (1 CONFIRMED, 0 NOT_REPRODUCED, 2 INCONCLUSIVE or refused) and takes only --base-url, --output-dir, --read-only, --json, --sarif and --no-prove-liveness. Remove the rest and re-run; to run a configuration, use `veridor run --config FILE`. Nothing has been sent.

Verify one

Against the vulnerable demo target:

export VERIDOR_OWNER_TOKEN=owner-token VERIDOR_OTHER_TOKEN=outsider-token
veridor verify proof.json --base-url http://127.0.0.1:18805
veridor: verifying capsule check 'invoice-tenant-isolation' against http://127.0.0.1:18805 (from --base-url). It reads VERIDOR_OTHER_TOKEN, VERIDOR_OWNER_TOKEN from your environment and sends the values there.
veridor: liveness controls -- 1 extra request(s) carrying a credential Veridor fabricated, …
Veridor capsule — CONFIRMED

  check:    invoice-tenant-isolation  (Invoice tenant isolation)
  verified: http://127.0.0.1:18805
  claim:    finding 0ec4c5cbd79deaed6e9f graded confirmed by the reporter

  The reporter's original statement:
    Identity 'other_tenant' received HTTP 200 (declared: one of [404]) and the response carried 'invoice_owner''s marker: …

  CONFIRMED: the declared expectation failed here as well.

  Scope: Veridor evaluates the expectations you declared. A finding means observed behaviour differed from your declaration; it does not establish that the declaration is correct.

Exit code 1. The first stderr line names every variable verify read and where it sent the values.

Against the same target restarted without --vulnerable:

veridor verify proof.json --base-url http://127.0.0.1:18805 --json verify.json
Veridor capsule — NOT_REPRODUCED
…
  NOT_REPRODUCED: the declared expectation held on this target. That is not proof the boundary is enforced -- only that this one declaration was met here.
…

Exit code 0, and verify.json holds the run document.

With an other_tenant token the target does not recognise (VERIDOR_OTHER_TOKEN=expired-token-0000), against the vulnerable target:

Veridor capsule — INCONCLUSIVE
…
  INCONCLUSIVE: the claim was not measured on this target: 'other_tenant' received HTTP 404, and so did a credential Veridor fabricated, so that denial does not show this target recognised the identity's credential.
…

Exit code 2. And with --base-url http://127.0.0.1:1, where nothing listens:

Veridor capsule — INCONCLUSIVE

  check:    invoice-tenant-isolation  (Invoice tenant isolation)
  verified: http://127.0.0.1:1
  reported: http://127.0.0.1:18805  (the reporter's target, not this one)
…
  INCONCLUSIVE: Request failed: [Errno 61] Connection refused Also inconclusive here: the probes as anonymous, other_tenant.
…

Exit code 2.

The three verdicts

Verdict Means Exit
CONFIRMED an identity got an answer its declaration rules out, here as well 1
NOT_REPRODUCED the declaration held here, and that was measured 0
INCONCLUSIVE the claim was not measured here; the reason says what was missing 2

A refusal before anything is sent also exits 2. 1 is the answer a maintainer is looking for, not a tool failure.

NOT_REPRODUCED needs a measurement behind it. Every identity the check names was answered with a status the capsule declares, every graded marker was observed where it had to be, and the liveness control found no denial that a fabricated credential drew as well. A capsule with a POST, PUT, PATCH or DELETE sends no fabricated credential, so there a denied identity counts as live only through its own marker_probe. Without one, the verdict is INCONCLUSIVE. Give the denied identity a marker_probe before you write a write-method capsule.

CONFIRMED needs less. When the evidence is suspected (a status, with no marker observed), anything that answers every request with 200, such as a proxy or a captive portal, also produces CONFIRMED. Before acting on one, look for the requests in the target's own access log. See veridor verify on a capsule.

The capsule is untrusted input

It came from someone else and is expanded from your environment. Before any request, verify refuses with exit 2, among others:

  • a ${NAME} that is not VERIDOR_*;
  • a target that is not this machine's loopback, unless you pass --base-url:
veridor verify remote-proof.json
veridor: this capsule's target is 'https://staging.example.com', which is not this machine's loopback. A capsule names the REPORTER's target, and `veridor verify` sends nothing to an address the capsule chose. Pass --base-url with the instance YOU are verifying, e.g. --base-url https://your-instance. Nothing has been sent.
  • any POST, PUT, PATCH or DELETE without --base-url, loopback included, and any of them at all under --read-only;
  • a Host header anywhere in the capsule, a request path that would leave the target, and terminal control characters in the fields printed beside the verdict.

The full list is on safety posture.

Exit codes

Command 0 1 2
veridor capsule written not used refused (a literal credential, an unknown check, an output path that would overwrite an input, a file that does not load)
veridor verify FILE NOT_REPRODUCED CONFIRMED INCONCLUSIVE, or refused before sending

70, 130 and 143 mean the same as for every command.

What it does not do

  • A capsule does not say the declaration was right. CONFIRMED means the declared expectation failed there too.
  • capsule does not send a request. It reads files.
  • verify does not use the reporter's credentials or send anything to the reporter's address unless that address is your own loopback and the check only reads.
  • verify does not plant markers. If the reporter's markers are not in your instance's data, the owner's marker cannot be observed and evidence is at most suspected.
  • A capsule holds one check. It does not carry sequences or the rest of the configuration.