Skip to content

fix(proxy): require public tunnel for all service previews - #1758

Open
zombee0 wants to merge 10 commits into
boxlite-ai:mainfrom
zombee0:codex/tunnel-preview-gate
Open

zombee0 wants to merge 10 commits into
boxlite-ai:mainfrom
zombee0:codex/tunnel-preview-gate

Conversation

@zombee0

@zombee0 zombee0 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Require an active public tunnel for every guest service preview.

Design doc: https://linear.app/polygala/issue/POL-695/persist-per-port-access-declarations-for-httpwebsocket-and-connect

Summary

A preview URL or credential previously could reach an undeclared guest port. The proxy now resolves the box and numeric port, checks the API per-port declaration before runner access, and denies missing declarations. API errors fail closed; terminal port 22222 keeps its authenticated route. The API caches access verdicts for 3 seconds.

Docs: apps/proxy/README.md request paths and docs/concepts/networking.md service access.

Verification

make test:apps: proxy tests passed; the full matrix failed on Postgres file descriptors and Dashboard localStorage setup. The before-fix reproducer run is pending.

zombee0 and others added 10 commits September 28, 2026 22:47
Direct public hosts and CONNECT requests previously accepted any guest port. Resolve the current tunnel declaration through the API before either path reaches the runner, including requests over pooled HTTP connections. Preserve signed and terminal access.
The proxy asks /preview/{boxId}/tunnels/{port} on every public preview
request, and each call queried Postgres. Cache both verdicts for 3 s
under preview:tunnel:{boxId}:{port}, the same window as the other
preview checks (preview:public, preview:token), so revocation or making
a box private takes effect within 3 s.

declarePublic clears the key after its write, so a freshly declared
port is not held behind a cached refusal.

Co-authored-by: Cursor <cursoragent@cursor.com>
The initial tunnel gate changes proxy and API behavior only. Keep the Dashboard copy on its established contract while the remaining proxy paths are completed in the next layer.
parseHost returned the raw port label, so "022222" slipped past
TERMINAL_PORT string comparisons: on the HTTP path it skipped terminal
authentication and on the warning page it showed the interstitial.
parseHost now returns a validated uint16 (1-65535) and TERMINAL_PORT is
numeric, so every caller compares one canonical form and the separate
re-parsing in GetProxyTarget and tunnelTarget is gone.

The tunnel access check no longer depends on isDirectHost: any public
box is gated, so a future box ID format cannot skip it. An API failure
now answers 502 on the HTTP path, matching CONNECT.

Co-authored-by: Cursor <cursoragent@cursor.com>
The proxy now gates every guest service preview on a public tunnel, while terminal traffic follows its authenticated route. Clarify URL issuance, access checks, cache behavior, and source references so the docs match the current implementation.
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: 7608e036-7a1f-4ac6-bd89-5ed3dcf6a98b

📥 Commits

Reviewing files that changed from the base of the PR and between 056196e and 211ea84.

📒 Files selected for processing (12)
  • apps/api/src/box/services/tunnel.service.spec.ts
  • apps/api/src/box/services/tunnel.service.ts
  • apps/proxy/README.md
  • apps/proxy/pkg/proxy/get_box_target.go
  • apps/proxy/pkg/proxy/parse_host_test.go
  • apps/proxy/pkg/proxy/proxy.go
  • apps/proxy/pkg/proxy/tunnel.go
  • apps/proxy/pkg/proxy/tunnel_access.go
  • apps/proxy/pkg/proxy/tunnel_access_test.go
  • apps/proxy/pkg/proxy/tunnel_test.go
  • docs/concepts/networking.md
  • docs/contributing/investigations/2026-09-26-preview-tunnel-gate.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The API caches public tunnel access verdicts in Redis for three seconds and clears the affected entry when a public tunnel is declared. The proxy parses ports as numeric values and checks access before forwarding guest-service HTTP and raw CONNECT requests. Denied checks return 404, and access-check errors return 502. The authenticated terminal path remains separate from guest tunnel checks.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Proxy
  participant PreviewTunnelEndpoint as Preview tunnel endpoint
  participant TunnelService
  participant Redis
  participant Database
  Client->>Proxy: Request guest service port
  Proxy->>PreviewTunnelEndpoint: Check access for box and port
  PreviewTunnelEndpoint->>TunnelService: Check public tunnel access
  TunnelService->>Redis: Read cached verdict
  alt Cache miss
    TunnelService->>Database: Run existing access checks
    Database-->>TunnelService: Return access result
    TunnelService->>Redis: Cache result for three seconds
  end
  TunnelService-->>PreviewTunnelEndpoint: Return access result
  PreviewTunnelEndpoint-->>Proxy: Return access status
  Proxy-->>Client: Forward request or return denial
Loading

Suggested reviewers: dorianzheng

Priority: ➖ Normal

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 211ea

The documented tunnel gate has no confirmed outstanding issue in the supplied evidence. The redirect behavior remains unverified but does not, on its own, prevent merging after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 211ea

The new gate substantially narrows access to guest ports, but its decision depends on a remote response and a short-lived cache. A possible redirect path cannot be assessed without production routing details, and access may briefly persist after a status change.

Retained concerns

  • Low · security · inferred: The new access check trusts the final HTTP 200 rather than confirming a response from the intended guarded endpoint. A redirect to an unrelated successful response could defeat the per-port decision; whether production routing permits that redirect is unknown.
  • Low · security · inferred: A cached allow bypasses the current tunnel and box-state predicates for up to three seconds. Cache misses can also write an earlier verdict after a concurrent state change, so the new gate does not provide immediate transition enforcement.
Security review details

Security Blast Radius

  • inferred — The independently reachable guest-port scope is a requested box and nonterminal port through either preview path. A mistaken allow permits traffic to that guest service, not access to the separate terminal route.

Security Findings and Attack Paths

  • inferred — A redirect ending in an unrelated 200 could be mistaken for an access verdict. The configured client has no redirect restriction, but whether deployed routing can produce the necessary redirect is unresolved; this is not a verified bypass.

Trust Boundaries and Controls

  • observed — Attacker-supplied preview targets are resolved to a numeric port before the proxy requests a box-and-port verdict. The direct API route applies authentication and proxy guards, while its cache-miss query checks the exact tunnel and box eligibility.

Resilience and Maintainability Implications

  • inferred — The three-second expiry limits an old allow, but the cache-hit path does not recheck changed state, and its read–query–write sequence does not order concurrent verdicts against state transitions.

Hardening Proposals

  • proposed — Constrain redirect handling for the authorization request, and explicitly define whether revocation and box-state transitions require immediate cache invalidation or the documented short expiry is an acceptable policy window.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 9 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: requiring a public tunnel for all service previews.
Description check ✅ Passed The description includes the required TL;DR, design document, summary, and verification sections. It explains the enforcement change, terminal-port exception, caching, test results, and known full-mat…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 9 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI

Warning

Some tools did not complete. Review the errors below.

🔧 Biome (2.5.12)
apps/api/src/box/services/tunnel.service.ts

File contains syntax errors that prevent linting: Line 22: Decorators are not valid here.; Line 23: Decorators are not valid here.


Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

The PR author must acknowledge the current diff and description before requesting review.

Author review acknowledgment

The PR author acknowledged this commit.

@zombee0: read the current diff and description, then check:

Does the description accurately explain the mechanism shown in the diff?

Use the form best suited to the change. Check drafts too, and repeat this review
after description edits. Post this as a new PR comment:

/reviewed 211ea84aae26b6008f4340e0d7acca3d26e37550

Unacknowledged PRs are converted to draft. After this check passes, click Ready for review when you want reviews.
Only a new, unedited comment from the PR author counts. A new commit requires a new acknowledgment.
This records the author's acknowledgment of the commit, not an automated judgment of the description; maintainer approval is separate.

@zombee0

zombee0 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

/reviewed 211ea84

@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@zombee0
zombee0 marked this pull request as ready for review September 28, 2026 16:01
@zombee0
zombee0 requested a review from a team as a code owner September 28, 2026 16:01

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@@ -0,0 +1,31 @@
## TL;DR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not keep this

This branch has not been deployed

No deployments
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.

2 participants