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.
administratoris 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 reachNOT_REPRODUCED(below). VERIDOR_*names only.verifyreads no other variable, socapsulerenames 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 <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 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 notVERIDOR_*; - a target that is not this machine's loopback, unless you pass
--base-url:
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,PATCHorDELETEwithout--base-url, loopback included, and any of them at all under--read-only; - a
Hostheader 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.
CONFIRMEDmeans the declared expectation failed there too. capsuledoes not send a request. It reads files.verifydoes 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.verifydoes 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 mostsuspected.- A capsule holds one check. It does not carry sequences or the rest of the configuration.