Skip to the content.

RFC 0015: Public Read-Only Verification Endpoints

Summary

Open read-only verification endpoints to anonymous callers for projects in attested mode, so a relying party can verify what’s running without holding the daemon’s admin token.

Problem

Every /_api/* request currently passes through _check_auth in proxy/ingress.py (line 391, called from _handle_api at line 403). That includes endpoints whose entire purpose is to let an external party verify what code is running:

Today all four return 401 missing token to an anonymous caller. The proxy/verify.py CLI documents this by accepting --token — but a relying party does not have the daemon admin’s token, and asking for one defeats the point of attestation.

This blocks the iconic relying-party demo on the project’s front page and breaks the trust chain laid out in RFC 0001.

Files to Modify

Implementation

  1. In _handle_api, do not gate every path through _check_auth. Instead, classify each path before the auth check:
    • Public, always-readable: none yet.
    • Public, attested-only: GET /_api/projects/<name>, GET /_api/projects/<name>/audit, GET /_api/attest/<name>, GET /_api/verification/<name>. Resolve the project; if it exists and mode == "attested", serve. If it does not exist or is in dev mode, return 404 not found (do not leak existence of dev projects).
    • Authenticated: all mutating endpoints (POST, DELETE), plus list endpoints (GET /_api/projects, GET /_api/routes, GET /_api/audit) which would otherwise enumerate dev projects.
  2. Public endpoints that take no project name (e.g., a future GET /_api/instance for the daemon’s own attestation) should also be public. Do not add this in this RFC — keep scope to the per-project endpoints.
  3. Document the public/private split in a docstring on _handle_api so the rule is reviewable in one place.
  4. Do not change the _check_auth implementation itself. Only change which endpoints route through it.

Testing & Validation Requirements

Report Requirements

Out of Scope

Revisions

An RFC that opens a trust boundary is a live document: what is learned about that boundary afterwards belongs here, in the thing the next implementer reads, not only in the commit that fixed it.

2026-07-21 — this RFC specified a secret leak, and did so for three months

What happened. Making these endpoints public was implemented as a routing change. The payloads they already returned were not revisited. Those payloads were asdict(project), which includes project.env. From 2026-04-25 every attested project published its secrets to anyone who asked — BRIDGE_SECRET, OPENVPN_USER/PASS, OVPN_CONFIG_BASE64, ZAI_API_KEY, OAuth client secrets, neko passwords. Fixed 2026-07-21 in 156a82a6; the payload is now a projection, not a serialization.

Why this RFC is the right place to record it. The same defect was correctly diagnosed on 2026-06-18 (1bb89b7f) on a sibling public route — root cause stated exactly, in a commit message. That finding never reached this document. Eight days later 3c09e7e9 added the HTML renderer and faithfully implemented what this RFC still asked for: the full payload. The spec outlived the review, so the review did not count.

The rule this yields. An RFC that changes an endpoint’s AUDIENCE owes a specification of what crosses the boundary, not only who may cross it. Reachability tests verify that the door opens; they are silent on what is behind it. On a boundary deliberately opened, content review is the only control left — and it was the one nobody wrote down.

Still open (tracked separately, not fixed by 156a82a6): _api_status guards the same serialization behind a caller-supplied public: bool flag, so safety is opt-in; _api_aggregate_status serializes env on an authenticated route. Both are the same pattern, and the sweep after 2026-06-18 is the sweep that should have caught this one.