Skip to content

feat: enforce GAT downstream port and rewrite to upstream port on CONNECT#363

Open
clement0010 wants to merge 14 commits into
masterfrom
fix/verify-connect-downstream-port
Open

feat: enforce GAT downstream port and rewrite to upstream port on CONNECT#363
clement0010 wants to merge 14 commits into
masterfrom
fix/verify-connect-downstream-port

Conversation

@clement0010

@clement0010 clement0010 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Related Tickets & Documents

Changes

The controller issues per-resource ports in the GAT under resource.gateway_metadata, and this PR makes the Gateway honor both:

  • Downstream port (downstream.port, client-facing): the CONNECT destination port must equal the token's downstream port, otherwise the connection is refused with 400.
  • Upstream port (upstream.port, backend-facing): after the downstream check passes, the destination port is rewritten to the upstream port before forwarding to the backend.

Both are enforced at the single shared point in ParseConnect, before protocol multiplexing, so they cover all resource types (Kubernetes, SSH, WebApp). Port ranges are validated in GATClaims.Validate, so a malformed or missing port claim is rejected when the token is parsed.

The CONNECT handler previously matched only the destination hostname from
the GAT and discarded the port, so a token scoped to host:22 could be
replayed to reach any other port on the same host (SEC-215).

Model the downstream port from the GAT
(resource.gateway_metadata.downstream.port) and require the CONNECT
destination port to equal it. Fail closed: reject with 403 when the port
mismatches or the downstream port claim is absent. Enforced generically in
ParseConnect, before protocol multiplexing, so it covers all resource types.

Upstream port and port rewriting on forward remain out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNRnYZx1qdvnSdEWKUuYtf
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.76923% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.26%. Comparing base (105f62b) to head (d8e3172).
⚠️ Report is 3 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
internal/connect/connect.go 75.00% 4 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #363      +/-   ##
==========================================
+ Coverage   86.06%   86.26%   +0.19%     
==========================================
  Files          40       40              
  Lines        2828     2853      +25     
==========================================
+ Hits         2434     2461      +27     
+ Misses        269      265       -4     
- Partials      125      127       +2     
Flag Coverage Δ
integration 54.73% <26.92%> (-0.17%) ⬇️
unit 78.68% <80.76%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/token/gat_claims.go 100.00% <100.00%> (ø)
internal/connect/connect.go 93.45% <75.00%> (-4.27%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Gateway’s GAT token model to carry a client-facing downstream port and enforces that incoming CONNECT requests target that exact downstream port, rejecting mismatches.

Changes:

  • Extend token.Resource to include gateway_metadata.downstream.port in GAT claims.
  • Enforce CONNECT destination port equality with the token’s downstream port in MessageValidator.ParseConnect.
  • Update tests and test fakes to populate the new downstream port metadata and cover rejection cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
test/fake/client.go Populates GAT request payload with gateway_metadata.downstream.port derived from the resource port.
internal/token/gat_claims.go Adds GatewayMetadata/Downstream to the token claim model under resource.gateway_metadata.
internal/connect/connect.go Parses CONNECT host/port and rejects requests when the port doesn’t match token downstream port.
internal/connect/connect_test.go Updates GAT claim fixtures and adds negative tests for port mismatch / missing downstream port.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/connect/connect.go Outdated
Address review feedback: a non-numeric CONNECT port is a malformed request,
not an authorization failure. Split the parse step from the comparison so an
unparseable port returns 400 with the parse error in the message (HTTPError
only renders Code + Message, so the error must be in the message), and reserve
403 for a genuine port mismatch. Add a test covering the non-numeric port path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNRnYZx1qdvnSdEWKUuYtf
Previously the downstream port was only checked for presence (non-zero) in
ParseConnect. A negative or out-of-range value (>65535) would pass and later
fail during proxying. Validate the port range [1,65535] when the token is
parsed, so a malformed port is rejected up front. The presence check in
ParseConnect is now redundant and removed; the port-match check remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYz2gKUb3vHW4xzKzAfvEY
clement0010 and others added 4 commits July 1, 2026 23:32
The controller issues a backend-facing upstream port in the GAT under
resource.gateway_metadata.upstream.port. After verifying the CONNECT destination
matches the downstream port, the Gateway rewrites the destination port to the
upstream port before forwarding to the backend.

Applied at the single shared point in ParseConnect, so it covers all resource
types (Kubernetes, SSH, WebApp), which all dial the backend via the validated
address. The upstream port range [1,65535] is validated in GATClaims.Validate,
so a malformed port is rejected when the token is parsed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYz2gKUb3vHW4xzKzAfvEY
The fake client previously used a single port for the CONNECT destination and
both GAT ports, so the port rewrite was a no-op in integration tests. Split
them: the client targets a logical downstream port (WebApp 80, SSH 22,
Kubernetes 443) while the backend listens on the real upstream port, so the
Gateway's rewrite is exercised end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYz2gKUb3vHW4xzKzAfvEY
@clement0010 clement0010 changed the title fix: verify CONNECT destination port matches GAT downstream port feat: enforce GAT downstream port and rewrite to upstream port on CONNECT Jul 2, 2026
@clement0010 clement0010 force-pushed the fix/verify-connect-downstream-port branch from 9264f5e to 568b024 Compare July 2, 2026 05:40
clement0010 and others added 3 commits July 2, 2026 14:00
The downstream port is a 1:1 mapping from resource type (Kubernetes 443,
SSH 22, WebApp 80), so both NewClient callers defined the same const block
and threaded the port through as a parameter. Move the mapping into the
fake client and derive the port from resourceType, failing fast on an
unknown type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w4FfxqxLjQSHuii3bBCds
Drop omitzero from resource.gateway_metadata: the claim is mandatory, so a
zero value should serialize explicitly and fail port validation at parse
rather than silently disappear from the payload. Format the invalid-port
error as "field": value, and tighten the Validate tests: the
missing-downstream case now zeroes the whole metadata and the
missing-upstream case provides only a valid downstream port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w4FfxqxLjQSHuii3bBCds
Move downstream port verification and the upstream port rewrite out of
ParseConnect into rewriteAddress, giving the port-mapping policy a single
seam for future changes (e.g. validating against the upstream port once
the CONNECT destination arrives already translated). Align the
port-mismatch status with the destination address mismatch: both now
return 400.

Cover rewriteAddress with a standalone table-driven test and keep one
ParseConnect test asserting the address is rewritten. Drop the
missing-port ParseConnect tests, which duplicated
TestGATTokenClaims_Validate coverage in the token package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w4FfxqxLjQSHuii3bBCds

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread internal/token/gat_claims.go
Comment thread internal/connect/connect.go
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