Evidence and attestation
Signing reports, binding them to an audit trail, and knowing what a report can leak.
On this page
Reports are compliance evidence. They leave the runner, pass through object stores, tickets, and inboxes, and are read months later. This guide covers what binds a report to its contents, how to verify one, and what a report is allowed to carry.
What is attested
Every non-terminal report format carries an attestation.
{
"v": "1.1",
"sha256": "9c1f...",
"audit_log_sha256": "4ab0...",
"run_id": "a263...",
"created": "2026-09-03T10:04:11Z",
"key_id": "3f2a9c118d4e77b0",
"sig": "base64..."
}The SHA-256 is always present, taken over the report body with surrounding whitespace trimmed. Trimming is the single canonicalization rule, so JSON value re-extraction and comment-trailer splitting agree byte for byte.
An Ed25519 signature is added when --signing-key is given. The signed payload
is a fixed byte sequence covering the version, report hash, audit log hash, run
id, and creation time, so everything that gives the report meaning is bound and
a report cannot be re-labeled, re-dated, or attributed to another run without
breaking it.
run_id names the invocation the report covers. It is derived from the
profiles, targets and times of the run rather than generated, so re-rendering a
stored report yields the same id, and every entry of a json report or event of
a wazuh stream carries it. v is the payload layout, major.minor: a minor
appends a line and leaves the earlier layout readable, so an artifact written
before the run id still verifies, carrying 1 as a number rather than a
string.
With --audit-log, that file’s hash is included, so forging results also means
fabricating a consistent command-by-command trail.
The run’s scope, meaning the filter terms and the count of excluded controls,
sits inside the signed body, so a narrowed run cannot be presented as a complete
one without breaking verification. An attestation proves a report has not
changed. It does not by itself make what was written complete.
Embedding per format
The shapes are chosen so verification re-hashes exactly the attested bytes.
| Format | Shape |
|---|---|
json, gitlab |
{"report": <verbatim body>, "attestation": {...}} |
junit, html |
Trailing <!-- refpoint-attestation ... --> comment |
gha |
Trailing ::notice title=refpoint-attestation::... line |
wazuh |
Final NDJSON line with "event_type": "attestation" |
terminal |
Not attested |
NDJSON has no comment syntax, so wazuh carries the attestation as an event of
its own, anchored to the last line rather than searched for — so target output
inside a report cannot supply one.
The JSON wrapper is assembled by hand rather than marshalled, because
re-marshalling a json.RawMessage re-indents it and silently changes the bytes
the hash covers. The JUnit, HTML, and gha shapes stay parseable by existing
tools, since a comment or a notice line is inert to them.
Signing and verifying
refpoint keygen --out refpoint-signingkey id: 3f2a9c118d4e77b0
private key: refpoint-signing.key
public key: refpoint-signing.pubrefpoint run baseline --format json --output result.json \
--audit-log audit.ndjson \
--signing-key refpoint-signing.keyrefpoint verify result.json --pubkey refpoint-signing.pub --audit-log audit.ndjsonsha256: OK (9c1f...)
audit log: OK (audit.ndjson matches 4ab0...)
run: a263...
created: 2026-09-03T10:04:11Z
signature: OK (key 3f2a9c118d4e77b0)Without --pubkey, a signed report is reported as “signature present but NOT
verified” rather than printing OK and implying more than was proven. Passing
--audit-log against a report whose attestation covers no audit log is an
error, not a pass.
The trust model, stated plainly
The hash alone is integrity, not authenticity. Anyone who edits the report can recompute it. It protects against corruption and casual edits.
The signature proves origin and post-signing integrity. The keyholder can always modify and re-sign, and no in-file cryptography prevents that. What the signature buys is that tampering by anyone else is detectable, and tampering by the keyholder is attributable. Verifiers hold only the public key and cannot forge. The private key therefore belongs in the runner’s secret store, not on workstations.
Keyless cosign signatures over the release checksums.txt cover the binaries
themselves. That is a separate mechanism from report attestation.
Re-renderings produced by refpoint report are derivative artifacts and are not
attested. The artifact of record is what run wrote.
The audit trail
refpoint run baseline --targets-file fleet.rptargets --audit-log audit.ndjsonOne NDJSON line is appended per transport operation on every target.
{"time":"2026-09-03T10:00:01Z","target":"admin@web-01","control_id":"ssh-01",
"op":"exec","detail":"sshd -T 2>/dev/null || /usr/sbin/sshd -T 2>/dev/null",
"exit_code":0,"duration_ms":42}| Field | Notes |
|---|---|
time |
Operation start |
target |
Target address |
control_id |
The control the operation ran for, carried on the context |
op |
exec, read_file, read_dir, stat, platform |
detail |
The command line or path |
exit_code |
Present for exec |
error |
Present on failure |
duration_ms |
No stdout or stderr is recorded. Command output routinely contains secrets, the
contents of /etc/shadow or a private key among them. What auditors need is
what was run, where, for which control, and whether it succeeded.
The log is wrapped at the transport boundary, so every current and future transport is covered by one implementation. It is opened append-only at 0600, with the mode set explicitly as well as at open, because a pre-created file keeps its own permissions. A dry run writes nothing, since it executes no commands and the platform probe is connection setup rather than evidence.
The file grows without bound across appends. Rotate it yourself if you keep long-running scan history, remembering that a report’s attestation binds the hash of the file as it stood at the end of that run.
What a report can leak
Reports hold what the target said, captured with whatever privilege the run used.
A command check stores its full stdout as the check’s observed value. That
is deliberate, since a verdict without the output it was based on is not
evidence, but nothing removes secrets from it. A command that prints a secret
puts that secret in the report.
HTTP header assertions record the verdict rather than the value, so a bearer
token or a Set-Cookie is not stored. Body matches store a pass or fail, never
the body.
Reports and audit logs are created 0600 and re-chmodded, because an existing file keeps its previous permissions.
Before publishing a report as a CI artifact, consider what the command checks
in the profile actually print.