feat(serve): split public and admin console listeners#89
Open
dviejokfs wants to merge 10 commits into
Open
Conversation
Adds a two-listener model for the console API so admin/management routes
can bind to a private interface (loopback, VPN, etc.) while public ingest
endpoints stay reachable from the open internet.
Each plugin now reports its public-facing routes through the existing
configure_public_routes hook; configure_routes is reserved for the admin
surface. PluginManager::build_split_application returns the two routers
separately and the serve command spawns one axum::serve per listener
when TEMPS_CONSOLE_ADMIN_ADDRESS is set, falling back to a single
merged listener for backwards compatibility.
Plugin route splits in this commit:
- analytics-events: /_temps/event ingest is public; dashboard stays admin
- analytics-session-replay: /_temps/session-replay/* public, queries admin
- analytics-performance: /_temps/speed{,/update} public, metrics admin
- error-tracking: Sentry/OTLP ingest + sentry-cli compat public; alerts,
DSN mgmt, source-map mgmt, error-group queries stay admin
- email-tracking: tracking pixel/click/SES webhook public; event queries admin
- ai-gateway: /ai/v1/{chat/completions,embeddings,models} public (API-key
authed at the handler); usage/pricing/provider keys stay admin
Agent-facing routes wired in serve/console.rs (node register/heartbeat,
edge route sync) move to the public listener since workers connect from
arbitrary networks with bearer tokens.
Defense-in-depth admin gate (serve/admin_gate.rs) adds optional
CIDR + Host-header allowlists on top of the network-layer binding:
- TEMPS_ADMIN_ALLOWED_IPS - comma-separated IPs/CIDRs
- TEMPS_ADMIN_ALLOWED_HOSTS - comma-separated Host values
- TEMPS_ADMIN_TRUST_FORWARDED_FOR - honor XFF only from loopback peers
Denials return 404 (not 403) so probes can't fingerprint the surface.
SwaggerUi and the embedded SPA now mount on the admin listener only.
Tests:
- 10 admin_gate unit tests (CIDR/host matchers, XFF anti-spoof)
- 2 PluginManager::build_split_application tests verifying admin-only
and public-only routes land on the correct router
- Add visitor facet aggregation endpoints (country, region, city, channel, referrer, event, browser, OS, device, language, UTM*) with ClickHouse + Postgres backends and matching SDK. - Add FacetCombobox UI and wire VisitorsList to the facet API. - Add environment subdomain rename handler/service with audit logging and slug normalization. - Regenerate web SDK against /api/api-docs/openapi.json (correct admin- listener path after the listener split).
Adds a dedicated /docs/admin-listener guide covering when to enable the split, the per-plugin route classification (public ingest vs admin), the four configuration env vars, deployment recipes (SSH tunnel, Tailscale/WireGuard, reverse proxy, IP allowlist), a testing checklist, and the threat model. Updates the environment-variables reference table with the four new vars (TEMPS_CONSOLE_ADMIN_ADDRESS, TEMPS_ADMIN_ALLOWED_IPS, TEMPS_ADMIN_ALLOWED_HOSTS, TEMPS_ADMIN_TRUST_FORWARDED_FOR) and adds a cross-link from the security architecture page.
Visitor facets: - Drop event-row dimensions (event/browser/os/device/language/utm_*) from VisitorSegmentFilters, VisitorFacets, OpenAPI, and the VisitorsList UI. Only country/region/city/channel/referrer remain. - Fan the 5 visitor-row queries out with tokio::try_join! so wall- clock is bounded by the slowest (~15 ms) instead of summed. - Drop facet_event_dimension and the EXISTS-against-events subquery in get_visitors. No more events hypertable touches in this code path — facets endpoint goes from ~1 s to ~15-20 ms at 150 k events. Environment subdomain: - Build env URLs from environments.subdomain (the canonical source) instead of reconstructing from project_slug + env_slug. Renames via update_environment_subdomain now propagate to listing/detail. - Expose subdomain on EnvironmentResponse and the settings UI.
Adds error-level structured logging at each fallible step of the GitHub App installation token mint flow (private key parse, JWT creation, client build, installation fetch, access_tokens URL parse, GitHub access_tokens POST). Each log includes installation_id and app_id so a failure can be traced back to the specific installation without re-deriving it from the request. Most useful for diagnosing the "GitHub rejected access_tokens" path, which silently fails today when the requested repo isn't selected on the installation or the App lacks a requested permission.
…ogging Unreleased entries: - Added: public/admin console listener split with optional CIDR/Host admin gate (TEMPS_CONSOLE_ADMIN_ADDRESS + TEMPS_ADMIN_ALLOWED_*). - Fixed: contextual error logging on GitHub App scoped token mint failures so installation/permission misconfigs are traceable.
GitHub's POST /app/installations/{id}/access_tokens rejects `owner/repo`
in the `repositories` array — the owner is fixed by the installation, so
the field expects bare repo names. We were sending `kfsoftware/foo` and
getting 422 even when the App had access.
Pass `repo` alone to for_repo_read/for_repo_write, update their docs, and
flip the regression test from "uses full name" to "uses bare repo name"
so this can't regress.
Resolves the Dependabot alerts that can be cleared without source-level refactors: - openssl 0.10.78 → 0.10.79 (GHSA-xp3w-r5p5-63rr high, GHSA-xv59-967r-8726 medium) - astral-tokio-tar 0.6.0 → 0.6.1 (GHSA-xx64-wwv2-hcqq low, GHSA-fp55-jw48-c537 medium) - examples/nextjs/basic next 16.2.3 → 16.2.6 (13 Next.js advisories) - examples/node/vercel-ai-tracing @opentelemetry/sdk-node ^0.57.0 → ^0.217.0 (GHSA-q7rr-3cgh-j5r3 high) Out of scope (require source migrations, tracked separately): - rmcp 0.6 → 1.4 (major API change in temps-mcp) - hickory-proto 0.25 → 0.26 (major bump in temps-dns-resolver) - protobuf 2.28 / remove_dir_all 0.5 (locked by pingora and nixpacks transitives)
GitHub's POST /app/installations/{id}/access_tokens silently strips the
entire `permissions` block if any requested key isn't in the App's
*declared* permission set. `metadata` is granted implicitly to every
installation token but is not a declarable permission, so listing it
explicitly triggers the strip — the minted token comes back with empty
permissions and surfaces as `push:false, pull:false` on every repo, even
when the App has `contents:read+write` granted.
Drop `metadata` from for_repo_read/for_repo_write. Tests now assert it
is absent so this can't regress. Symptom this fixes: `git push` from
workspace sandboxes 403'ing with "Write access to repository not granted"
even on private repos where the App has Read & write on Contents.
Public ingest endpoints (e.g. /api/_temps/session-replay/init) failed at runtime with "Missing request extension RequestMetadata" because build_split_application applied middleware only to the admin router. The auth middleware doubled as the RequestMetadata builder, so the public router got neither. Split the responsibilities: - temps-core::RequestMetadataMiddleware now owns metadata injection (Observability priority, apply_to_public=true). - PluginMiddleware gained apply_to_public; build_split_application partitions middleware and runs shared middleware on both routers. - AuthPlugin registers both middlewares; AuthMiddleware drops its inline metadata block. Regression tests in temps-core lock in the wiring contract: shared middleware reaches the public router, admin-only middleware does not. Also bundles unrelated CI hardening: bounded tokio::time::timeout wrappers around the MongoDB + Redis docker-tests that hung GitHub run 25806816492 (PR #89) for 90 min.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lets operators bind admin/management routes to a private interface while public ingest endpoints stay reachable from the internet. Single-listener mode remains the default — set
TEMPS_CONSOLE_ADMIN_ADDRESSto opt in./ai/v1/*), multi-node register/heartbeat/route-sync.TEMPS_ADMIN_ALLOWED_IPS(CIDR allowlist),TEMPS_ADMIN_ALLOWED_HOSTS(Host-header allowlist), andTEMPS_ADMIN_TRUST_FORWARDED_FOR(honor XFF only from loopback peers). Denials return404, not403, so probes can't fingerprint the surface.configure_routes(admin) /configure_public_routes(public) hooks. Six plugins updated.What's in this PR
64149c1a02b18d7e9adf47541ee7a458eac,c0a9dc8Configuration
Full reference: docs/howto/admin-listener — covers SSH tunnel, Tailscale, reverse proxy, and office IP allowlist recipes plus the threat model.
Test plan
cargo check --bin temps— clean across 56 cratescargo test --bin temps admin_gate— 10 admin_gate unit tests pass (CIDR/host matchers, XFF anti-spoof)cargo test --lib -p temps-core split_application— 2 split-router tests pass (admin-only routes 404 on public, public-only routes 404 on admin)TEMPS_CONSOLE_ADMIN_ADDRESS=127.0.0.1:8081; verifycurl http://127.0.0.1:8080/api/auth/loginreturns 404 andcurl http://127.0.0.1:8081/api/auth/loginreturns 401TEMPS_ADMIN_ALLOWED_IPS=127.0.0.1/32and admin bound to0.0.0.0:8081, verify a request from a non-loopback IP returns 404TEMPS_ADMIN_ALLOWED_HOSTS=admin.local, verifycurl -H 'Host: evil.example'returns 404Threat model
Defends against drive-by scanning of
/api/auth/login, credential-stuffing attacks that don't know the admin surface exists, and CORS-misconfiguration mistakes that would otherwise expose admin APIs. Does not replace authentication — keepTEMPS_AUTH_SECRETstrong and treat the admin URL as a secret regardless.Webhooks (GitHub, Stripe, SES) stay on the public listener — they verify their own signatures and must be reachable from arbitrary IPs.