Skip to content

sciontool port-forward tunnel does not send the transport (IAP) token, so the websocket upgrade is refused behind IAP #1930

Description

@ameer00

Description

When the Hub runs behind Identity-Aware Proxy with transport auth (SCION_TRANSPORT_MODE=iap), an agent's port-forward tunnel never connects. The tunnel websocket is dialed with only the agent token and no transport token, so IAP rejects the upgrade before the request reaches the Hub. The agent then retries every 5 seconds for its whole lifetime and logs an error on every attempt.

The agent's other Hub traffic works in the same setup: heartbeat and status updates send the transport token and succeed, and terminal attach (scion attach) works too. Only the tunnel is affected, so port forwarding and web previews of agent-served ports do not work behind IAP.

Steps to Reproduce

  1. Deploy a hosted Hub behind IAP with server.auth.transport mode iap (for example on Cloud Run), with the embedded runtime broker dispatching agents to the kubernetes runtime.
  2. Create any agent. It receives SCION_TRANSPORT_MODE=iap and SCION_TRANSPORT_TOKEN, and IAP accepts the token's audience: heartbeats and status updates reach the Hub.
  3. Read the agent's log.

Expected Behavior

The port-forward tunnel (/api/v1/agents/{id}/ports/tunnel) connects through IAP like the agent's other Hub calls, and exposed ports can be opened through the Hub.

Actual Behavior

The agent log repeats about every 5 seconds, indefinitely:

[sciontool] ERROR: Port-forward tunnel disconnected: websocket: bad handshake

The Hub never logs the tunnel request, because IAP refuses the upgrade first. Heartbeat, status and terminal attach keep working.

Root Cause

pkg/sciontool/portforward/tunnel.go, Manager.runOnce (lines 82-84 at v0.3.0-preview.3; unchanged on main):

header := http.Header{}
header.Set("X-Scion-Agent-Token", m.client.AuthToken())
conn, _, err := websocket.DefaultDialer.DialContext(ctx, endpoint, header)

The dial carries only the agent token. The sciontool Hub client adds the transport token to its HTTP calls through the round-tripper that configureOIDCTransport installs in pkg/sciontool/hub/oidc.go (transportauth.Wrap(..., transportauth.HeaderAuthorization), lines 55 and 75). A websocket dial with websocket.DefaultDialer does not go through that round-tripper, so the token is never attached.

The other websocket dialers already handle this with transportauth.ApplyHeaders:

Suggested Fix

  1. Let pkg/sciontool/hub.Client apply its transport token to a header. For example, add an ApplyTransportHeaders(h http.Header) error method that calls transportauth.ApplyHeaders with the client's oidcSource and header mode, and does nothing when oidcSource is nil.
  2. Call it in runOnce before the dial:
header := http.Header{}
header.Set("X-Scion-Agent-Token", m.client.AuthToken())
if err := m.client.ApplyTransportHeaders(header); err != nil {
    return err
}
conn, _, err := websocket.DefaultDialer.DialContext(ctx, endpoint, header)

The client already refreshes injected transport tokens (adjustRefreshForTransportTokens), so each reconnect picks up a valid token.

Test idea: a tunnel test against a Hub stub that requires Authorization: Bearer <transport token> on the upgrade and returns 401 without it. Today that test reproduces the failure.

Nice to have: back off for longer after repeated 401/403 handshake failures (reconnectDelay is a fixed 5 s today), so that a misconfigured deployment does not log an error every 5 seconds forever.

Related

Environment

  • Scion version: v0.3.0-preview.3; the code above is unchanged on main as of 2026-09-25
  • OS/Platform: Linux (amd64); Hub on Cloud Run behind IAP, agents on GKE
  • Runtime: Kubernetes
  • Harness: Claude

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions