Skip to content

Security Checklist

LoopGlitch26 edited this page Oct 20, 2026 · 1 revision

HTTP security checklist

Every HTTP surface of Tracekit passes this checklist. Each item names the tests that enforce it; tests/test_security_checklist.py fails when a handler is added without being listed below and probed, or when a test named here no longer exists.

Surfaces

Handler Serves Kind
tracekit/observe.py:Handler tracekit view (v2 store, or with view.logs every log of a central deployment at /runs, /api/runs, /api/run and /api/bundle; token, or OIDC login at /login and /callback; an approver's approval pages at /approvals; with why logs, tracekit view --why and tracekit why serve, the why read API /api/why/workspace and /api/why/runs/<id> for the token only, the replay POSTs /api/why/runs/<id>/tests and /api/why/runs/<id>/attribute, and POST /v1/ingest for a bearer token only: an --ingest-token, which reads nothing, else the token) and tracekit observe or tracekit view --ledger/--bundle (v0.2 ledger) browser; read-only but for the approval pages and the why replay POSTs; ingest API
tracekit/transport/http.py:_Handler signer POST /v2/rpc and OTLP/HTTP POST /v1/traces; tracekit issuer serve API
tracekit/gateway.py:_Handler tracekit gateway serve (LLM gateway) API
tracekit/executor.py:_Executor tracekit executor serve (GET /v1/actions, POST /v1/actions/<name>: actions run with credentials the agent never holds) API
tracekit/mcp_gateway.py:_McpGateway tracekit mcp-gateway serve (POST /mcp: MCP over streamable HTTP to the configured upstream servers) API
tracekit/egress.py:_Handler tracekit egress serve (egress observer: CONNECT host:port and absolute-form http:// requests, forwarded to the outside; no TLS interception) API, proxy
tracekit/signer/metrics.py:Handler signer /metrics, /logs/v0 and tlog tiles API, public data
tracekit/otlp.py:H tracekit otel serve (loopback OTLP receiver) API
tracekit/ingest.py:H tracekit ingest serve (remote ingest) API
tracekit/witness_server.py:H tracekit witness serve API
tracekit/public_witness_server.py:H tracekit public-witness serve (C2SP tlog-witness add-checkpoint, open register, aggregate stats) API, open: a log proves its key by signing; limits per address and origin
tracekit/proxy.py:Handler tracekit-proxy (Anthropic proxy) API
tracekit/slack_approvals.py:Handler tracekit approvals slack serve (Slack interactivity callbacks) API, Slack-signed

The browser surface is the only one that serves HTML or holds a session. The viewer's approval pages and its why replay POSTs (which run allow-listed programs and add test results to a run) are the only requests a session can change state with; the why logs are the token's only (an OIDC session gets 403: they belong to no tenant), and their ingest endpoint takes the bearer token only, never the cookie. API surfaces authenticate every request by header or client certificate (bearer token, mTLS, Kubernetes service-account or OIDC token, run token) and never read a cookie, so a browser cannot ride a session on them. The public witness is open by design: it takes no credential, and a log registers by signing a checkpoint with the key it registers (one key per origin, the first). The Slack bridge authenticates each callback by Slack's v0 request signature instead, and answers approvals only through the signer as the Slack user who clicked.

Checklist

# Item Enforced by
1 HTML and attribute sinks: every value interpolated into the page goes through esc() (& < > " ') tests/test_observe_render.py::EscapingStaticCheck::test_terminal_html_escapes_every_interpolation, tests/test_view.py::View::test_hostile_tool_names_are_data_and_the_page_escapes_every_sink
2 JS sinks: run data reaches the page only as JSON from the API (exports: as escaped script data), never as code; no eval, Function, document.write; the approval and run pages set every value as text, never as markup tests/test_security_checklist.py::Viewer::test_hostile_run_data_is_served_as_json_data, tests/test_security_checklist.py::Page::test_approval_page_shows_data_as_text_with_format_characters_escaped, tests/test_security_checklist.py::Page::test_no_url_or_code_sink_takes_data, tests/test_observe_render.py::HostileBundleRendersInert::test_replay_embeds_hostile_strings_only_as_script_data
3 URL sinks: no href, src or action takes data, so a javascript: value stays text; the one href set is the downloaded bundle's object URL tests/test_security_checklist.py::Page::test_no_url_or_code_sink_takes_data
4 Unicode tricks: bidi controls and invisible format characters in tool names, args or tenant names are shown as \uXXXX tests/test_security_checklist.py::Page::test_esc_neutralises_markup_quotes_and_format_characters, tests/test_security_checklist.py::Page::test_approval_page_shows_data_as_text_with_format_characters_escaped
5 Strict CSP: default-src 'none', script-src a fresh nonce per response, no unsafe-inline script, no inline handlers, base-uri and form-action 'none' tests/test_observe_render.py::ObserverHttp::test_csp_uses_a_fresh_nonce_and_no_unsafe_inline_script, tests/test_security_checklist.py::Viewer::test_page_headers, tests/test_security_checklist.py::Approvals::test_page_headers, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
6 frame-ancestors 'none' tests/test_security_checklist.py::Viewer::test_page_headers, tests/test_security_checklist.py::Approvals::test_page_headers, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
7 Referrer-Policy: no-referrer and X-Content-Type-Options: nosniff on every browser-surface answer, errors included tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_security_checklist.py::Viewer::test_page_headers
8 Cookie auth: the session cookie is HttpOnly; SameSite=Strict, Secure over HTTPS, an 8-hour expiry with a MAC under the server's own random key (nothing of the token); an OIDC login's is a random id of a server-side session tests/test_observe_render.py::ObserverHttp::test_token_is_exchanged_for_a_cookie_and_leaves_the_url, tests/test_view.py::View::test_https_serves_with_a_secure_cookie, tests/test_identity_oidc.py::TestViewerLogin::test_auditor_session_sees_its_tenant_only, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
9 API surfaces: bearer, mTLS, service-account or OIDC credentials only, checked on every request; Slack callbacks: a v0 signature over the body, a timestamp within 5 minutes and a signature never seen before tests/test_slack_approvals.py::TestSignature::test_bad_signature_old_timestamp_and_replay_are_refused, tests/test_http_transport.py::TestLimits::test_bearer_token_and_keep_alive, tests/test_identity_oidc.py::TestValidate::test_refusals, tests/test_gateway.py::TestAuth::test_refusals_never_reach_the_upstream, tests/test_ingest.py::EndToEnd::test_http_level_rejections, tests/test_view.py::WhyIngest::test_rejects_bad_token_tamper_and_gaps
10 CSRF: the only state-changing requests served to a cookie session are the approval pages' POSTs, from an approver's OIDC session, each with that session's CSRF token in X-CSRF-Token (a header a cross-site request cannot set) and a JSON body; without approval pages the viewer answers GET only. The why replay POSTs need the token (cookie or bearer), a JSON body (which a cross-site page cannot send without a preflight nobody answers), a same-origin Origin if any, and an expected Host unless the token comes as a bearer header. The signer decides each answer for the person, under the approval rules. OIDC login: authorization code with PKCE, a state the browser's login cookie must match, used once tests/test_security_checklist.py::Viewer::test_no_state_changing_request_is_served_to_a_session, tests/test_security_checklist.py::Approvals::test_posts_need_the_session_csrf_token, tests/test_e13_approvals.py::TestWeb::test_csrf_role_and_bridge_grant, tests/test_identity_oidc.py::TestViewerLogin::test_refusals, tests/test_view.py::WhyPage::test_replay_posts_need_the_token_and_a_same_origin_json_body
11 Tenant scoping on every read: an identity of tenant B cannot read or act on tenant A's runs or approvals; a run token works only for its run, identity and tenant. The viewer's token is one operator credential over its store or logs; an OIDC session (auditor or approver role) reads its tenant's runs only, in every query of the central viewer (list, search, run, bundle, by guessed id too), an operator-admin's every tenant's, read-only, and an approver's sees and answers its tenant's approvals only tests/test_view_central.py::CentralViewer::test_every_endpoint_reads_the_sessions_tenant_only, tests/test_identity_oidc.py::TestViewerLogin::test_operator_admin_reads_every_tenant_and_gets_no_approval_pages, tests/test_identity_oidc.py::TestViewerLogin::test_auditor_session_sees_its_tenant_only, tests/test_e13_approvals.py::TestWeb::test_cross_tenant_approval_is_refused, tests/test_http_transport.py::TestCrossTenant::test_matrix, tests/test_gateway.py::TestAuth::test_refusals_never_reach_the_upstream, tests/test_view.py::View::test_loopback_viewer_prints_a_token_url_and_needs_it
12 No secret or token stays in a URL: the viewer's (and tracekit why serve's) printed ?token= is exchanged once for the cookie and answered with a redirect to a URL without it; only the page path exchanges it tests/test_observe_render.py::ObserverHttp::test_token_is_exchanged_for_a_cookie_and_leaves_the_url, tests/test_view.py::WhyPage::test_the_page_is_the_viewers_and_the_why_api_needs_the_token
13 No secret or token in logs: handlers log no request line; refusals echo no credential tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_remote_tokens.py::TestRemoteSigner::test_refusals_carry_no_log_state
14 Request size limits: an oversized body is refused with 413 before it is read tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_security_checklist.py::Approvals::test_posts_need_the_session_csrf_token, tests/test_http_transport.py::TestLimits::test_oversize_body_is_refused, tests/test_gateway.py::TestExchange::test_bodies_are_capped, tests/test_view.py::WhyPage::test_replay_posts_need_the_token_and_a_same_origin_json_body
15 Rate and connection limits: failed authentications limited per address and in total; per-client rate limit on ingest; registrations per address, checkpoints per origin and origins in total on the public witness; slow clients cut off; the central viewer's pages bounded (50 runs, one search query per log plus a reader of each log with matching runs; 500 records) tests/test_view_central.py::CentralViewer::test_pages_are_bounded_and_filters_apply, tests/test_remote_tokens.py::TestFailedAuth::test_limited_per_address_and_bounded, tests/test_ingest.py::EndToEnd::test_rate_limit, tests/test_public_witness.py::Server::test_limits, tests/test_http_transport.py::TestLimits::test_slow_client_is_cut_off
16 Error bodies without internals: no traceback, source path or credential tests/test_security_checklist.py::Surfaces::test_refusals_carry_no_internals_or_credentials, tests/test_otlp.py::Wire::test_hostile_input_is_refused_cleanly
17 HTTPS required off loopback: the viewer, the signer's http section, the issuer, ingest, the witnesses, tracekit why serve and the Slack bridge refuse plain HTTP beyond loopback (ingest, the witnesses and tracekit why serve take --insecure-http, the Slack bridge insecure_http: true, for TLS terminated in front); the observer (tracekit observe, tracekit view --ledger/--bundle) and the OTLP receiver serve loopback only; metrics need allow_remote: true and serve public data only tests/test_view.py::View::test_beyond_loopback_needs_https, tests/test_view.py::OneViewer::test_v1_ledger_defaults_to_its_home_and_stays_on_loopback, tests/test_http_transport.py::TestConfig::test_tls_required_except_explicit_insecure_loopback, tests/test_security_checklist.py::PlainHttp::test_plain_http_beyond_loopback_is_refused, tests/test_signer_metrics.py::TestListen::test_non_loopback_needs_allow_remote, tests/test_slack_approvals.py::TestConfig::test_secrets_come_from_files_and_beyond_loopback_needs_tls
18 Host check: the browser surface serves only the names it expects, other names only with the token (DNS rebinding) tests/test_observe_render.py::ObserverHttp::test_loopback_without_token_still_refuses_foreign_hosts, tests/test_observe_render.py::ObserverHttp::test_wildcard_bind_accepts_its_own_host_only_with_the_token, tests/test_view.py::WhyPage::test_foreign_host_is_refused_without_the_token
19 Verdicts the server computes are labelled "operator-side view — re-verify with the signed release for evidence", each run's in the central viewer's list and page too, and its bundle is served to re-verify offline; findings and what the model said (tracekit analyze --v2, agent text) are shown beside a run's integrity, never in it tests/test_view_central.py::CentralViewer::test_every_verdict_is_labelled_and_a_failed_run_shows_no_records, tests/test_view_central.py::CentralViewer::test_downloaded_bundle_verifies_offline, tests/test_view.py::View::test_dev_run_is_listed_with_its_counts_and_verified_while_the_signer_holds_the_lock, tests/test_view.py::View::test_tampered_record_shows_only_the_failure, tests/test_view.py::View::test_asked_and_said_show_beside_the_call_and_findings_beside_the_integrity
20 The viewer refuses a run that fails verification: only the failure report is shown tests/test_view_central.py::CentralViewer::test_every_verdict_is_labelled_and_a_failed_run_shows_no_records, tests/test_view.py::View::test_tampered_record_shows_only_the_failure, tests/test_observe_render.py::ObserveBundle::test_tampered_bundle_is_refused
21 Every HTTP handler is listed here and probed tests/test_security_checklist.py::Checklist::test_every_http_handler_is_listed, tests/test_security_checklist.py::Surfaces::test_every_surface_has_probes
22 Signed findings (review_finding on POST /v2/rpc): written only into a findings run, which only an identity in signer.yaml analyzers registers, bound to a run of its tenant, with that run's token; the request schema is closed, so no text of the run (excerpts, paths, reasons) reaches a signed record, and the signer writes the rule's fixed title tests/test_reviews.py::TestReviewFinding::test_refused_outside_a_findings_run_and_for_an_identity_not_in_analyzers, tests/test_reviews.py::TestReviewFinding::test_the_closed_verdict_takes_no_text_of_the_run, tests/test_signer_lifecycle.py::TestSignerOnlyRecords::test_findings_only_from_a_configured_analyzer_bound_to_a_run_of_its_tenant
23 Live constraints (constraint_add on POST /v2/rpc): any client of a run may add one, and it only ever raises a later call of that run to an ask a person answers (TK-CONSTRAINT), never lowering a deny or an ask; the record holds a commitment to the file, never its name; an equal one adds nothing; past 32 file constraints per run a new one is a signed constraint_refused gap, the hook is told, and every later write is held (a kind that names no file is always added); the writer checks at most 256 names per call tests/test_constraints.py::Live::test_a_decision_is_never_lowered, tests/test_constraints.py::Live::test_a_made_up_constraint_only_adds_holds, tests/test_constraints.py::Live::test_the_record_holds_a_commitment_never_the_name, tests/test_constraints.py::Live::test_refusals, tests/test_constraints.py::Live::test_an_equal_constraint_is_the_one_the_run_has, tests/test_constraints.py::Live::test_past_the_cap_a_file_constraint_is_a_signed_gap_and_every_write_is_held, tests/test_constraints.py::Live::test_the_writer_checks_at_most_max_names_files_per_constraint

Adding a surface

Add the handler to SURFACES and Surfaces.PROBES in tests/test_security_checklist.py and to the Surfaces table above, and make it pass every item that applies to its kind.

Home

Get started

How it works

Integrations

Policy and approvals

Deploy and operate

Verify the evidence

Security and privacy

Compliance

Evaluation

Demos

Help

Clone this wiki locally