Read Auth for Private Networks — COMPLETE
Shipped 2026-03-14. Per-path read token enforcement for private networks.
Design
Model: If any token in the store has "read" in its operations AND its path pattern matches the request path, that path requires read auth. If no such token exists, the path is public. No new config — the existing tokens TOML file drives everything.
Performance: Pre-compute readPaths []string at load time by collecting path patterns from tokens with "read" in operations. RequiresReadAuth calls matchesAnyPath(ts.readPaths, reqPath) — no iteration over tokens at request time.
Exempt paths: /health and /.well-known/agent-manifest.md bypass read auth so agents can discover capabilities before authenticating.
Content-addressed fetch: Hash lookups resolve to a real path first, then read auth checks that path — knowing a hash doesn't bypass access control.
What Shipped
server/internal/auth/auth.go
readPaths []string field on TokenStore, pre-computed at load time
collectReadPaths() — extracts path patterns from tokens with "read" op
RequiresReadAuth(path) — checks if any read token covers the path
- Directory path normalization — checks both
/private and /private/ against patterns so trailing-slash omission can't bypass /** globs
server/internal/handler/handler.go
authorizeRead(w, req) helper — checks token store, exempts well-known manifest path, calls RequiresReadAuth, then Authorize if needed
- Integrated into
handleFetch, handleList, handleVersions
handleList and handleVersions signatures changed from (w, path string) to (w, req protocol.Request) to carry auth metadata
- Hash-based fetch checks read auth on the resolved path after hash lookup
- Versioned path auth —
/doc.md/v2 checks auth on the base path /doc.md
Tests
TestRequiresReadAuth — 9 table-driven cases covering read tokens, publish-only tokens, no tokens, glob patterns, directory paths with/without trailing slash
TestReadAuth — 15 handler tests covering FETCH/LIST/VERSIONS with and without tokens, wrong tokens, public paths, well-known manifest, no token store, hash-based fetch, versioned paths
Edge Cases Caught During Review
- Versioned path bypass:
/doc.md/v2 wouldn't match an exact-path token for /doc.md. Fixed by checking auth on the base path before dispatching to handleFetchVersion.
- Directory path bypass:
LIST /private without trailing slash wouldn't match /private/**. Fixed by having RequiresReadAuth check both the path as-is and with a trailing slash appended.
Backwards Compatibility
Fully backwards compatible:
- No read tokens configured = all reads public (same as before)
- No new config options needed
- No protocol changes
- Existing write auth unchanged
Related documents