WebSocket Authentication
A WebSocket connection begins as an HTTP upgrade request. Apache APISIX can run an authentication plugin on that handshake and reject the request before the upstream returns 101 Switching Protocols. After the upgrade succeeds, APISIX proxies WebSocket frames; HTTP authentication plugins are not re-run for each frame.
This tutorial uses key-auth to demonstrate handshake authentication for a non-browser client that can set a custom header.
WebSocket authentication boundaries#
Design the connection lifecycle before choosing a credential:
- Standard browser WebSocket APIs cannot set an arbitrary
apikeyorAuthorizationheader. Do not put a long-lived secret in the URL to work around that limitation. Use an application-specific short-lived ticket, a protected session established over HTTPS, or another browser-compatible design whose replay and authorization behavior you have defined. - Browser CORS policy does not automatically authorize a WebSocket connection. Validate the
Originat a trusted component when your application relies on an origin allowlist. - A successful handshake authenticates the connection at that moment. If a credential expires or is revoked, the application must define whether to close, reauthenticate, or otherwise reauthorize an existing long-lived connection.
- The upstream service still owns per-message and resource-level authorization. It must not trust identity headers supplied directly by the client; the gateway should strip or overwrite them at the trust boundary.
Prerequisites#
Before you begin, ensure that you have:
- Apache APISIX installed and the Admin API reachable from an operator environment. The
tls.verifysetting used below requires an APISIX-Runtime build. - A WebSocket upstream. This example uses Postman's public echo service at
wss://ws.postman-echo.com/raw; use a controlled test server if an external service is unsuitable for your environment. - A WebSocket client that can set custom headers, such as
websocat.
Create a WebSocket route#
Reuse the resolved Admin API secret from the environment that starts APISIX:
admin_key="${ADMIN_KEY:?ADMIN_KEY is not set}"
If a local test configuration contains the actual key rather than an environment or secret reference, you can read that literal value with admin_key="$(yq -r '.deployment.admin.admin_key[0].key' conf/config.yaml)". yq does not resolve ${{VARIABLE}} templates or external secrets.
Create a Route that enables WebSocket proxying and authenticates the upgrade request:
curl "http://127.0.0.1:9180/apisix/admin/routes/websocket-auth" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/raw",
"methods": ["GET"],
"enable_websocket": true,
"plugins": {
"key-auth": {
"hide_credentials": true
}
},
"upstream": {
"type": "roundrobin",
"scheme": "https",
"pass_host": "node",
"tls": {
"verify": true
},
"nodes": {
"ws.postman-echo.com:443": 1
}
}
}'
enable_websocket enables the protocol upgrade on the Route. The upstream
scheme: https corresponds to wss:// for the proxied WebSocket connection.
pass_host: node sends the node hostname as the upstream Host and TLS server
name instead of forwarding the client's 127.0.0.1 host.
On an APISIX-Runtime build, tls.verify: true verifies the upstream certificate
using the CA certificates in upstream.tls.ca_certs, or the
ssl_trusted_certificate configured in conf/config.yaml when the Upstream does
not provide its own CA certificates. Keep verification enabled for authenticated
upstream TLS, and supply the appropriate private CA when the WebSocket service
does not use a publicly trusted certificate. See the
Upstream section of the Admin API for the current TLS
fields. A standard APISIX build without the required runtime modules cannot apply
these verification fields; use a controlled proxy or service-mesh hop that
verifies the upstream identity instead of sending sensitive traffic over an
unauthenticated upstream TLS connection.
Create a Consumer credential#
Create a Consumer and a separate key-auth credential:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"username": "jack"
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers/jack/credentials" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"id": "websocket-key",
"plugins": {
"key-auth": {
"key": "this_is_the_key"
}
}
}'
Use a generated secret delivered through your deployment's secret-management process in production. The fixed value above is only a local example.
Test the handshake#
Connecting without the configured header should fail with 401 Unauthorized:
websocat -v ws://127.0.0.1:9080/raw
Connect again with the key in the configured header:
websocat -v -H='apikey: this_is_the_key' ws://127.0.0.1:9080/raw
After the server returns 101 Switching Protocols, send a text message. The echo service should return the same message. Verify that the upstream does not receive the apikey header, that failed handshakes are logged without the raw credential, and that your connection-termination policy works when a credential is revoked.
The key-auth plugin also supports a query parameter and gives the header higher priority. If both sources are present, the plugin authenticates with the header; hide_credentials removes only that matched header, so the lower-priority query value can still reach the upstream. It also cannot prevent a query credential from first appearing in a URL. If the route must be header-only, reject credential-bearing query parameters at an earlier request-policy layer and test that behavior explicitly.