diff --git a/backend/src/ee/services/proxied-service/proxied-service-enums.ts b/backend/src/ee/services/proxied-service/proxied-service-enums.ts
index f890202c80f..2ecb0854f77 100644
--- a/backend/src/ee/services/proxied-service/proxied-service-enums.ts
+++ b/backend/src/ee/services/proxied-service/proxied-service-enums.ts
@@ -13,5 +13,7 @@ export enum ProxiedServiceSubstitutionSurface {
Header = "header",
Path = "path",
Query = "query",
- Body = "body"
+ Body = "body",
+ // Applies to frames the agent sends after a WebSocket upgrade, not to the upgrade request itself
+ WebSocket = "websocket"
}
diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts
index 33907cde81d..95616426145 100644
--- a/backend/src/lib/api-docs/constants.ts
+++ b/backend/src/lib/api-docs/constants.ts
@@ -1627,7 +1627,7 @@ export const PROXIED_SERVICES = {
placeholderValue:
"For credential substitution: the placeholder value the agent proxy swaps for the real secret value on the wire.",
substitutionSurfaces:
- "For credential substitution: which request surfaces are scanned for the placeholder. Allowed values: 'header', 'path', 'query', 'body'."
+ "For credential substitution: which request surfaces are scanned for the placeholder. Allowed values: 'header', 'path', 'query', 'body', 'websocket'. The 'websocket' surface scans text messages the agent sends after a WebSocket upgrade, rather than the upgrade request itself."
}
} as const;
diff --git a/docs/documentation/platform/agent-proxy/local-agent-proxy.mdx b/docs/documentation/platform/agent-proxy/local-agent-proxy.mdx
index 161f6aeed5f..ee54db39d92 100644
--- a/docs/documentation/platform/agent-proxy/local-agent-proxy.mdx
+++ b/docs/documentation/platform/agent-proxy/local-agent-proxy.mdx
@@ -185,6 +185,8 @@ On Linux, `run` checks the sandbox before starting your agent. If the private ne
Under both enforced modes the agent has exactly one route to the outside, the proxy. Direct connections fail, including connections straight to an IP with no hostname involved. Only HTTP and HTTPS are brokered, so SSH cannot be: use HTTPS with a brokered token, which is how `git` and `gh` work.
+WebSockets go through the proxy like anything else, so an agent that opens a real-time connection is brokered rather than blocked. See [WebSockets](/documentation/platform/agent-proxy/proxied-services#websockets) for which parts of a connection can carry a credential.
+
**DNS is deliberately unavailable inside the sandbox.** The agent does not need a resolver: it hands the hostname to the proxy in a `CONNECT` or an absolute-URI request, and the proxy resolves it on the host, outside the sandbox. Blocking it buys two things. A tool that ignores the proxy variables fails loudly instead of quietly going direct, and DNS lookups cannot be used to smuggle data out. So `Could not resolve host` inside a sandboxed agent is expected, and it means the failing tool is not using the proxy.
Both letter cases of every proxy variable are set (`HTTP_PROXY` and `http_proxy`, and so on), because some tools read only one, and `NO_PROXY` always covers `localhost` and `127.0.0.1`.
diff --git a/docs/documentation/platform/agent-proxy/proxied-services.mdx b/docs/documentation/platform/agent-proxy/proxied-services.mdx
index 43bc946fd94..b2caf30d039 100644
--- a/docs/documentation/platform/agent-proxy/proxied-services.mdx
+++ b/docs/documentation/platform/agent-proxy/proxied-services.mdx
@@ -96,13 +96,37 @@ The placeholder is also delivered for you: both [`agent-proxy connect`](/documen
- The request surfaces the agent proxy scans for the placeholder: `path`, `query`, `header`, and/or `body`. Scoping is the security boundary: the proxy only substitutes in the surfaces you list.
+ The request surfaces the agent proxy scans for the placeholder: `path`, `query`, `header`, `body`, and/or `websocket`. Scoping is the security boundary: the proxy only substitutes in the surfaces you list.
+
+ `websocket` is the one surface that is not part of the request. It scans the messages an agent sends after a [WebSocket upgrade](#websockets), rather than the upgrade request itself.
The secret (from the same folder) whose real value replaces the placeholder.
+## WebSockets
+
+Real-time APIs are brokered too. The upgrade request is an ordinary HTTP request, so [header rewrites](#header-rewrites) and `path`, `query` and `header` substitution apply to it exactly as they do to any other request. That covers most services, because a credential in a WebSocket URL or an `Authorization` header on the handshake is the common pattern.
+
+Some services expect the credential in the first message after the connection opens instead. The `websocket` surface covers that: the agent sends the placeholder in a message, and the Agent Proxy swaps in the real value on the way out.
+
+It also swaps the value back. If a service returns the credential in a message, in an error for example, the Agent Proxy replaces it with the placeholder again before the message reaches your agent, so the real value stays out of the agent either way.
+
+Messages the Agent Proxy cannot read are forwarded unchanged rather than blocked, so a credential inside one of these is **not** substituted:
+
+- Binary messages
+- Messages split across several frames
+- Messages larger than 1 MB
+
+
+ **Compression is turned off on these connections.** The Agent Proxy has to read your agent's messages to substitute in them, so where this surface is in use it does not negotiate `permessage-deflate`. Expect more bandwidth on a high-volume connection. Services without a `websocket` substitution are unaffected.
+
+
+
+ The placeholder has to appear as ordinary text in the message. If your client encodes the credential into a binary payload, put it in the handshake instead.
+
+
## Using secrets from other folders and environments
You cannot point a proxied service at another folder directly, but you can bring an outside value into its folder so the service can use it. There are two ways, both using features that already exist in Infisical:
diff --git a/docs/documentation/platform/agent-proxy/standalone-agent-proxy.mdx b/docs/documentation/platform/agent-proxy/standalone-agent-proxy.mdx
index 240089189c1..ca617656e16 100644
--- a/docs/documentation/platform/agent-proxy/standalone-agent-proxy.mdx
+++ b/docs/documentation/platform/agent-proxy/standalone-agent-proxy.mdx
@@ -167,6 +167,8 @@ When an agent requests a host no proxied service matches, the `--unmatched-host`
- Both HTTPS and plain-HTTP services are brokered. HTTPS traffic arrives as `CONNECT` tunnels; plain `http://` traffic arrives as regular forward-proxy requests (useful for internal services without TLS). An `https://` URL sent as a plain forward-proxy request is rejected, so the Agent Proxy can never be used to downgrade TLS.
- Only HTTP and HTTPS are brokered, so SSH cannot be: use HTTPS with a brokered token instead. A tool that ignores the proxy variables is not brokered either; nothing forces its traffic through the proxy.
+- WebSockets are brokered, both for a credential carried in the upgrade request and, with the `websocket` [substitution surface](/documentation/platform/agent-proxy/proxied-services#websockets), for one carried in the messages the agent sends afterwards. A live WebSocket holds its connection open for its lifetime, so a fleet of long-lived real-time connections counts against the Agent Proxy's concurrent-connection ceiling in a way short HTTP requests do not.
+- **Use `wss://` for any WebSocket that carries a credential.** Plain `ws://` is brokered as well, for internal services that do not offer TLS, but the credential the Agent Proxy applies then travels unencrypted. As with plain-HTTP upstreams, keep those services inside the trusted network (see [Network placement](#network-placement)).
- If the agent identity can already read a secret that one of its proxied services brokers, `connect` refuses to start: the agent would receive that value directly and bypass the Agent Proxy. Pass `--allow-readable-brokered-secrets` to override the guardrail.
## Certificates and TLS interception
diff --git a/frontend/src/components/proxied-services/forms/SurfaceSelect.tsx b/frontend/src/components/proxied-services/forms/SurfaceSelect.tsx
index 57c50bdd1ec..189a8c042b8 100644
--- a/frontend/src/components/proxied-services/forms/SurfaceSelect.tsx
+++ b/frontend/src/components/proxied-services/forms/SurfaceSelect.tsx
@@ -7,7 +7,8 @@ const SURFACE_LABELS: Record = {
[ProxiedServiceSubstitutionSurface.Path]: "Path",
[ProxiedServiceSubstitutionSurface.Query]: "Query",
[ProxiedServiceSubstitutionSurface.Body]: "Body",
- [ProxiedServiceSubstitutionSurface.Header]: "Header"
+ [ProxiedServiceSubstitutionSurface.Header]: "Header",
+ [ProxiedServiceSubstitutionSurface.WebSocket]: "WebSocket Message"
};
type SurfaceOption = {
diff --git a/frontend/src/hooks/api/proxiedServices/enums.ts b/frontend/src/hooks/api/proxiedServices/enums.ts
index 4ef88988224..fda64d19702 100644
--- a/frontend/src/hooks/api/proxiedServices/enums.ts
+++ b/frontend/src/hooks/api/proxiedServices/enums.ts
@@ -12,5 +12,6 @@ export enum ProxiedServiceSubstitutionSurface {
Header = "header",
Path = "path",
Query = "query",
- Body = "body"
+ Body = "body",
+ WebSocket = "websocket"
}