headers
Inbound header policy for tunnel traffic.
Everything the gate forwards arrives from the PUBLIC internet through the portr client, so every header on it is attacker-controlled. The headers an app trusts are precisely the ones a caller must not be able to write, because the app cannot tell a header the Databricks front door set from one a browser typed.
Policy shape: strip by default, allow by pattern
Section titled “Policy shape: strip by default, allow by pattern”Enumerating what to remove is a losing game - a deny-list is only correct until
the platform adds a header or a library starts trusting another one - so the
policy is inverted. EVERY x--prefixed request header is dropped from tunnel
traffic unless it matches a configured pattern. That fails CLOSED: a header
nobody thought about is removed rather than trusted.
The allow-list is zero-to-many literals, globs, or /regex/es, compiled by
shared-core’s pattern.toPatternMatcher, and is UNIONED with
DEFAULT_FORWARD_HEADERS so extending it never silently breaks the
built-in surfaces. Configure it with forwardHeaders /
TUNNEL_FORWARD_HEADERS.
Non-x- headers are untouched. Standard ones (content-type, accept,
authorization, cookie, …) are the app’s normal input and the gate has no
business rewriting them.
The headers no pattern can forward
Section titled “The headers no pattern can forward”PROTECTED_HEADERS is stripped BEFORE the allow-list is consulted, so a
permissive pattern (x-*, or a careless *) cannot re-open impersonation:
| Header | What an app does with it | Why spoofing it matters |
|---|---|---|
x-forwarded-access-token |
OBO auth (AppKit asUser) |
Paste any workspace token and every call runs as its owner. The gate’s verified email says nothing about who a pasted credential belongs to. |
x-forwarded-user |
Caller identity | Impersonate another user. The gate sets this itself, from a verified session. |
x-forwarded-email |
Caller identity | Same. |
x-forwarded-preferred-username |
Display name from the IdP | Same. |
x-forwarded-host |
The originally-requested host | Poison absolute URLs the app builds (the classic reset-link attack). |
x-forwarded-proto / -port |
Original scheme / port | Convince the app a plaintext request arrived over TLS. |
x-forwarded-for |
Client IP | Forge the audit trail, and fan out per-IP rate-limit keys (see below). |
x-real-ip |
Client IP | Same. |
x-request-id |
Request correlation UUID | Forge or collide trace ids, making logs unreliable. |
The identity four are AppKit’s OBO contract; the rest are the X-Forwarded-*
set the Databricks Apps reverse proxy documents passing to an app
(https://docs.databricks.com/aws/en/dev-tools/databricks-apps/http-headers),
plus the conventional x-forwarded-proto/-port/x-real-ip an app or one of
its libraries may read even though the table omits them.
Stripping the x-forwarded-* transport trio is safe because http-proxy-3 is
configured with xfwd: true and re-adds them AFTER this policy runs - from the
real socket, not from the caller’s claim. The app therefore sees the honest
(loopback) values instead of whatever the internet asserted. The gate reads the
client IP for rate limiting from the raw inbound headers BEFORE stripping, and
takes the RIGHTMOST x-forwarded-for entry, which is the only one a proxy
appended rather than a client supplied.