Skip to content

fix(sentinel): real keyed signatures, verify traces before scoring, escape dashboard XSS#13

Merged
imran-siddique merged 1 commit into
mainfrom
fix/sentinel-hardening
Jun 30, 2026
Merged

fix(sentinel): real keyed signatures, verify traces before scoring, escape dashboard XSS#13
imran-siddique merged 1 commit into
mainfrom
fix/sentinel-hardening

Conversation

@imran-siddique

Copy link
Copy Markdown
Contributor

Security hardening for the sentinel integration. Three fail-closed fixes, each configurable via environment variables. No behavior change when the relevant env vars are set; default posture is to reject/refuse rather than fabricate trust.

1. Real keyed incident signatures (fail closed)

Incident reports were "signed" with base64(sha256(payload) + b"signed") — keyless, so any party could recompute it, and /verify recomputed the same public value. Replaced with HMAC-SHA256 over the canonical report JSON using a secret from SENTINEL_SIGNING_KEY.

  • If no key is configured, signing fails closed: the report is returned with "signature_status": "unsigned" and no signature, rather than a value that merely looks signed.
  • /verify now verifies the keyed HMAC with hmac.compare_digest, and returns UNVERIFIABLE when no key is set.

2. Verify TRACE claims before scoring/enforcing (fail closed)

ingest_trace, the /evaluate endpoint, and the CLI fleet path fed unverified input straight into scoring and enforcement. Added a verification gate that checks the Ed25519 signature against a trusted key from TRACE_TRUSTED_JWK (an OKP/Ed25519 public JWK as JSON), using agentrust_trace.verify_record when the package is importable, otherwise a minimal Ed25519 check.

  • Verification is against the configured trusted key, never the key embedded in the record.
  • Unsigned traces, bad signatures, and a missing trusted key are rejected (403 on the endpoint). The only bypass is SENTINEL_ALLOW_UNVERIFIED=1, which logs a loud warning on every use. Default is reject.

3. Escape dashboard DOM-XSS

dashboard.html interpolated posted-trace fields (agent_id, claim.reason, event.description, detection_type, claim_id) into innerHTML and inline onclick handlers unescaped. All trace-derived strings are now HTML-escaped via an esc() helper, untrusted values are carried on data-* attributes, and actions are wired with addEventListener instead of inline onclick.

Tests

Adds sentinel/tests/test_security.py (18 tests, all passing locally on Python 3.12):

  • Keyed signing: round-trip verifies; tampering, wrong key, and no-key-set all fail closed; guard against regressing to the old keyless value.
  • Trace gate: unsigned/wrong-key traces rejected and a properly signed trace accepted, at the module, ingest_trace, and /evaluate endpoint level; SENTINEL_ALLOW_UNVERIFIED=1 opt-out logs a warning.
  • Incident endpoints: export marks unsigned without a key; /verify round-trips VERIFIED, detects TAMPERED, and returns UNVERIFIABLE without a key.
  • Dashboard: source-level assertions that the render path escapes untrusted fields and uses no inline on* handlers for untrusted values.

Both the agentrust_trace-installed path and the minimal-fallback path were exercised and pass. cryptography added to requirements.txt. README and integration.yaml updated to document the env vars and the fail-closed behavior.

🤖 Generated with Claude Code

…scape dashboard XSS

Three fail-closed, env-configurable security fixes for the sentinel integration:

1. Incident signatures are now a real keyed HMAC-SHA256 over the canonical
   report JSON, using a secret from SENTINEL_SIGNING_KEY. The previous value
   was base64(sha256(payload) + b"signed") with no key, which any party could
   recompute. If no key is configured, signing fails closed: the report is
   marked "signature_status": "unsigned" and carries no signature rather than
   an authentic-looking value. /verify uses hmac.compare_digest against the
   keyed HMAC and returns UNVERIFIABLE when no key is set.

2. Incoming TRACE claims are verified before they are scored or enforced.
   ingest_trace, the /evaluate endpoint, and the CLI fleet path now run a
   verification gate that checks the Ed25519 signature against a trusted key
   from TRACE_TRUSTED_JWK (using agentrust_trace.verify_record when available,
   otherwise a minimal Ed25519 check). The trusted key is the configured one,
   never the key embedded in the record. Unsigned or invalid traces are
   rejected unless SENTINEL_ALLOW_UNVERIFIED=1 is set, which logs a loud
   warning on every use. Default is reject.

3. The enforcement dashboard no longer interpolates trace-derived fields
   (agent_id, claim.reason, event.description, detection_type, claim_id) into
   innerHTML or inline on* handlers unescaped. All such values are HTML-escaped
   via an esc() helper, untrusted values are carried on data-* attributes, and
   actions are wired with addEventListener instead of inline onclick.

Adds tests/test_security.py covering keyed signing and tamper/wrong-key/no-key
cases, trace acceptance/rejection at the module, ingest, and endpoint level,
and source-level assertions that the dashboard render path escapes untrusted
fields. Adds cryptography to requirements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@imran-siddique imran-siddique merged commit e11817c into main Jun 30, 2026
3 checks passed
@imran-siddique imran-siddique deleted the fix/sentinel-hardening branch June 30, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant