Skip to content

fix: exempt loopback from TierAuth so local vault run doesn't get 429'd#97

Merged
dangtony98 merged 2 commits intomainfrom
fix/ratelimit-loopback-exempt
Apr 21, 2026
Merged

fix: exempt loopback from TierAuth so local vault run doesn't get 429'd#97
dangtony98 merged 2 commits intomainfrom
fix/ratelimit-loopback-exempt

Conversation

@dangtony98
Copy link
Copy Markdown
Contributor

Summary

  • vault run fetches /v1/mitm/ca.pem on every invocation and the dashboard re-fetches it on component re-mount — both from 127.0.0.1. Per-IP TierAuth (10 req / 5 min) would trip after a handful of local iterations, and the CLI would fall back to explicit-proxy-only with a confusing server returned 429 log line.
  • Fix: exempt loopback peers from TierAuth at the keyer level (mirrors the existing MITM CONNECT policy in internal/mitm/connect.go). Added ratelimit.IPKeySkipLoopback so the "ip:" prefix + loopback logic lives in one place.
  • Also drop the ipAuth wrapper from /v1/service-catalog, /v1/skills/cli, /v1/skills/http, /v1/mitm/ca.pem. These are immutable static reads with no credentials on the wire — same category as /health and /v1/status, which already rely on the TierGlobal backstop.

Login/register keep their own in-handler IP+email buckets (handle_auth.go:595), so credential-bearing endpoints retain defense-in-depth even from loopback.

Test plan

  • go test ./... green (existing MITM/service-catalog/skill handler tests unchanged)
  • New ipkeyer_test.go covers IPv4 127.0.0.0/8, IPv6 ::1, and non-loopback keys
  • Manual: run ./agent-vault vault run -- claude 20+ times in a row with the dashboard open; confirm the routing HTTPS through MITM proxy line appears every time

🤖 Generated with Claude Code

The CA-fetch path (`/v1/mitm/ca.pem`) and other public static reads
were gated by per-IP TierAuth (10 req / 5 min). `vault run` calls the
CA endpoint on every invocation, and the dashboard re-fetches it on
component re-mount — both from 127.0.0.1, so a developer iterating on
`vault run` could easily burn the budget and fall back to the
explicit-proxy-only path with a confusing 429 log line.

- Add `ratelimit.IPKeySkipLoopback` so loopback peers return an empty
  key and the middleware fails open (mirrors the existing MITM CONNECT
  policy in internal/mitm/connect.go).
- Drop the `ipAuth` wrapper from `/v1/service-catalog`, `/v1/skills/*`,
  and `/v1/mitm/ca.pem`. These are immutable static reads with no
  credentials on the wire — TierGlobal is the only useful backstop.

Login/register keep their own in-handler IP+email buckets, so
credential-bearing endpoints retain defense-in-depth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread internal/server/server.go
Comment thread internal/ratelimit/key.go Outdated
Review on #97 surfaced two real issues with the IPKeySkipLoopback
approach:

1. Exempting loopback at the keyer level also silently exempts
   /v1/auth/register, /v1/auth/verify, /v1/auth/resend-verification,
   /v1/auth/forgot-password, and /v1/auth/reset-password. Only
   handleLogin enforces an in-handler IP bucket, so a local caller
   could spam registrations or reset emails unbounded.

2. IPKeySkipLoopback checks the XFF-derived clientIP rather than the
   direct TCP peer. A misconfigured trusted proxy (common nginx
   footgun) lets a remote attacker forge X-Forwarded-For: 127.0.0.1
   and bypass rate limiting entirely.

The route unwrap on the four public static reads (/v1/service-catalog,
/v1/skills/cli, /v1/skills/http, /v1/mitm/ca.pem) already solves the
reported vault run regression — the CA fetch is the only ipAuth route
the CLI hits. Revert the keyer and the ratelimit helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dangtony98
Copy link
Copy Markdown
Contributor Author

Both review findings addressed in 774b734 — dropping the keyer change entirely.

  • Over-broad exemption (server.go:847): correct — handleRegister, handleVerify, handleResendVerification, handleForgotPassword, and handleResetPassword have no in-handler IP bucket, so the keyer-level exemption silently disarmed them for loopback callers. The PR description's defense-in-depth claim was wrong for those routes.
  • XFF spoofing (ratelimit/key.go): correct — checking the XFF-derived clientIP rather than the direct TCP peer means a misconfigured trusted proxy turns the exemption into a remote bypass.

The route unwrap on the four public static reads (/v1/service-catalog, /v1/skills/cli, /v1/skills/http, /v1/mitm/ca.pem) is sufficient on its own: vault run only calls /v1/mitm/ca.pem, and the dashboard only fetches the CA PEM. The keyer change turned out to be unnecessary belt-and-suspenders that introduced both vulnerabilities. Reverted along with IPKeySkipLoopback and the now-orphaned ipkeyer_test.go.

Final diff vs main is 10 insertions / 8 deletions in server.go — just the route-unwrap.

@dangtony98 dangtony98 merged commit 5de88a1 into main Apr 21, 2026
4 checks passed
@dangtony98 dangtony98 deleted the fix/ratelimit-loopback-exempt branch April 21, 2026 04:34
dangtony98 added a commit that referenced this pull request Apr 21, 2026
Resolved conflict in internal/server/server.go: #97 (merged) dropped
the ipAuth wrapper from /v1/service-catalog, /v1/skills/{cli,http},
and /v1/mitm/ca.pem. Kept that change and preserved this branch's
new /v1/vaults/{name}/logs route alongside it.

Also applied De Morgan's to two QF1001 staticcheck findings in the
new TestListRequestLogsTailOrdering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dangtony98 added a commit that referenced this pull request Apr 21, 2026
## Summary

- New per-vault request log captures every proxy and transparent-MITM
request (actor, matched service, credential keys injected, status,
latency), surfaced via a Logs tab in the dashboard and `GET
/v1/vaults/{name}/logs`.
- MITM and explicit-proxy paths share a single `requestlog.Sink`, so
both ingress paths feed the same pipeline with consistent fields.
- Background retention worker trims by age and per-vault row cap; knobs
exposed as env vars and CLI flags.

## Changes

**Store**
- Migration
[039_request_logs.sql](internal/store/migrations/039_request_logs.sql)
adds the `request_logs` table + two indexes.
- CRUD in [internal/store/sqlite.go](internal/store/sqlite.go); methods
added to the `Store` interface.

**Pipeline**
- New [internal/requestlog/](internal/requestlog/) package: `Sink`
interface, SQLite-backed implementation, retention worker.
- `AttachLogSink` on the Server wires the sink at startup; the
explicit-proxy and MITM handlers fan events into it.

**API + docs**
- `GET /v1/vaults/{name}/logs` (vault-member auth) returns paginated
logs.
- Skill files ([cmd/skill_cli.md](cmd/skill_cli.md),
[cmd/skill_http.md](cmd/skill_http.md)) updated.
- Env-var / CLI-flag reference updated ([.env.example](.env.example),
[docs/reference/cli.mdx](docs/reference/cli.mdx),
[docs/self-hosting/environment-variables.mdx](docs/self-hosting/environment-variables.mdx)).

**UI**
- [web/src/pages/vault/LogsTab.tsx](web/src/pages/vault/LogsTab.tsx) +
[web/src/components/LogsView.tsx](web/src/components/LogsView.tsx)
render the table with filtering and status/latency cells.
- `AllAgentsTab` and `router.tsx` updated to integrate the new layout.

## Test plan

- [x] `go test ./...` green
- [ ] Manual: start the broker, exercise both explicit `/proxy/*` and
MITM HTTPS_PROXY flows, confirm rows land in the Logs tab with correct
actor / status / latency / credential-keys fields
- [ ] Manual: verify retention — set a low per-vault cap via env,
generate > cap rows, confirm trim on next worker tick
- [ ] Manual: verify auth — non-vault-member gets 403 on `GET
/v1/vaults/{name}/logs`

## Notes

- Branched off `main`. Does not depend on #97 (rate-limit loopback fix)
— they touch disjoint hunks.
- Frontend changes ship as source; `make build` will re-embed `web/dist`
into the binary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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