LLM 身份验证代理是一个基于 Envoy 的代理,运行在您的环境中,位于 LangSmith 和您的上游 LLM 提供商或网关(例如 OpenAI、Anthropic 或内部 LLM 网关如 LiteLLM)之间。LangSmith 使用短期有效的 JWT(JSON Web 令牌)对每个请求进行签名。该代理验证 JWT,可选择注入提供商凭证或转换请求和响应体,然后将请求转发到上游。它适用于 SaaS 和 自托管 的 LangSmith 客户。
在以下情况下使用 LLM 身份验证代理:
- 对来自您自己的提供商网关的 Playground 或 LLM 作为评判者的评估 请求进行身份验证。
- 注入特定于提供商的 API 密钥或身份验证头,而无需将其暴露给最终用户。
- 转换请求或响应体(例如,在 OpenAI 格式和自定义网关格式之间进行转换)。
工作原理
来自 LangSmith 的每个请求都会经过代理中的以下步骤:
- 验证 JWT(签名、签发者、受众)
- 调用您的
ext_authz 服务,该服务接收已验证的 JWT 并返回要作为头注入的提供商凭证
- 可选择调用您的
ext_proc 转换器,它可以重写请求和响应体(例如,在 OpenAI 格式和自定义网关格式之间转换)
- 使用自定义头(静态或动态)将请求转发到上游提供商
ext_authz 服务和转换器都是客户部署的组件,与代理一起在您的环境中运行。根据您的 使用场景,可以启用其中一个或两个。
先决条件
- LangSmith 企业版计划(SaaS 或自托管版本 0.13.33+)
- 带有 Helm 3 的 Kubernetes 集群
- Envoy v1.37 或更高版本(Helm 图表默认使用
envoyproxy/envoy:v1.37-latest)
- 您的上游 LLM 提供商或网关的 URL(代理将转发请求的目标)
身份验证代理目前支持 Playground、Evals 和 Fleet 功能。
1. 配置 JWT 签名(仅限自托管 LangSmith)
如果使用 LangSmith SaaS,请跳过此步骤。JWT 签名已配置完成。
使用 step CLI(或您偏好的内部流程)生成 Ed25519 密钥对。Ed25519 是 LangSmith 用于签署 JWT 的签名算法。私钥用于签署每个请求;身份验证代理仅使用公钥验证签名。
TMPDIR_KEYS="$(mktemp -d)"
step crypto keypair "$TMPDIR_KEYS/pub.pem" "$TMPDIR_KEYS/priv.pem" \
--kty OKP --crv Ed25519 --no-password --insecure
PRIV_JWK=$(step crypto key format --jwk < "$TMPDIR_KEYS/priv.pem")
SIGNING_JWKS=$(echo "$PRIV_JWK" | jq -c '{keys: [. + {use: "sig", alg: "EdDSA"}]}')
echo "$SIGNING_JWKS"
将 JWKS 存储在 Kubernetes 密钥中:
kubectl create secret generic langsmith-signing-jwks \
--namespace <namespace> \
--from-literal=LANGSMITH_SIGNING_JWKS="$SIGNING_JWKS"
JWKS(JSON Web 密钥集)是用于发布加密密钥的标准 JSON 格式。LANGSMITH_SIGNING_JWKS 包含 Ed25519 私钥,并作为 Kubernetes 密钥存储。它永远不会被暴露。LangSmith 会自动提取相应的公钥,并将其提供给 /.well-known/jwks.json 端点。身份验证代理获取此公共端点以验证 JWT 签名,而无需私钥。
在您的 LangSmith values.yaml 中引用该密钥:
platformBackend:
deployment:
extraEnv:
- name: LLM_AUTH_PROXY_ISSUER
value: "langsmith" # 必须与身份验证代理图表中的 jwtIssuer 匹配
- secretRef:
name: langsmith-signing-jwks
LLM_AUTH_PROXY_ISSUER 设置已签名 JWT 中的 iss 声明。使用 langsmith 以匹配 SaaS 默认值,或使用类似 langsmith:self-hosted:<short_identifier> 的自定义标识符来区分您的安装。该值必须与 步骤 4 中身份验证代理图表的 jwtIssuer 匹配。
2. 为您的组织启用 LLM 身份验证代理
选项 A:为特定组织启用:在 LangSmith UI 中,导航至 设置 页面,复制左上角 组织 旁边的组织 ID。针对您的 LangSmith PostgreSQL 数据库运行以下命令:UPDATE organizations
SET config = config || '{"can_use_llm_auth_proxy": true}'
WHERE id = '<organization_id>';
选项 B:为安装中的所有组织启用:在您的 LangSmith values.yaml 中添加以下内容到 commonEnv:commonEnv:
DEFAULT_ORG_FEATURE_CAN_USE_LLM_AUTH_PROXY: "true"
联系 LangChain 支持 为您的组织启用 LLM 身份验证代理。
3. 在 LangSmith 中配置组织设置
在 LangSmith UI 中,导航至 设置 > 常规,配置以下内容:
- JWT 受众: 代理将验证的
aud 声明值(例如,my-audience)。这必须与 步骤 4 中身份验证代理图表的 jwtAudiences 匹配。
- 启用 LLM 身份验证代理: 为您的组织打开此开关。
4. 安装身份验证代理 Helm 图表
添加 LangChain Helm 仓库:
helm repo add langchain https://langchain-ai.github.io/helm/
helm repo update
创建一个包含上游 URL 和 JWT 验证设置的 values.yaml。JWKS 配置有两种选择:
jwksUri(推荐): 指向您的 LangSmith 实例的 /.well-known/jwks.json 端点。Envoy 会自动获取并缓存公钥,支持无缝密钥轮换。
jwksJson(内联): 将 JWKS JSON 直接粘贴到 values.yaml 中。用于测试或身份验证代理无法出站网络访问 LangSmith 的离线环境。密钥轮换需要更新图表。仅包含公钥组件;省略 d 字段(私钥)。
如果同时设置两者,jwksUri 优先。
authProxy:
upstream: "https://gateway.example.com"
jwtIssuer: "langsmith" # 必须与 LangSmith values.yaml 中的 LLM_AUTH_PROXY_ISSUER 匹配
jwtAudiences:
- "my-audience" # 必须与 LangSmith 中的组织设置匹配
# 选项 A:远程 JWKS(推荐用于生产环境)
# Envoy 从 LangSmith 的 /.well-known/jwks.json 获取并缓存公钥。
jwksUri: "https://langsmith.example.com/.well-known/jwks.json" # 自托管
# jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json" # SaaS
jwksCacheDurationSeconds: 300
# 选项 B:内联 JWKS(仅限测试或离线环境)
# 省略 "d" 字段(私钥);仅包含公钥组件。
# jwksJson: '{"keys": [{"kty": "OKP", "crv": "Ed25519", "x": "<base64url-public-key>", "use": "sig", "alg": "EdDSA"}]}'
安装图表:
helm install langsmith-auth-proxy langchain/langsmith-auth-proxy \
--namespace <your-namespace> \
-f values.yaml
编写 ext_authz 服务
当您需要添加、删除或编辑身份验证头时,例如根据 JWT 中的身份注入提供商 API 密钥,请使用 ext_authz。您的服务接收已验证的 JWT 和可选的请求体,并返回要向上游注入的头。这使用了 Envoy 的 HTTP ext_authz 过滤器(非 gRPC)。
在 values.yaml 中启用它:
authProxy:
extAuthz:
enabled: true
serviceUrl: "http://my-auth-service:8080"
timeout: "10s"
工作原理
在转发每个请求之前,Envoy 使用与原始请求相同的 HTTP 方法,在 <serviceUrl>/check<original_path> 调用您的服务。您的服务在 x-langsmith-llm-auth 头中接收已验证的 JWT。
您的服务返回一个普通的 HTTP 响应:
2xx: 允许请求。任何与 allowedUpstreamHeaders 模式(默认:authorization 和 x-*)匹配的头都将被注入到上游请求中。要在转发前剥离 JWT,请在响应中包含 x-envoy-auth-headers-to-remove: x-langsmith-llm-auth。
- 非
2xx: 拒绝请求。状态码和任何与 allowedClientHeaders 模式(默认:www-authenticate 和 x-*)匹配的头将返回给客户端。
部署选项
您的 ext_authz 服务可以通过两种方式运行:
- Sidecar: 在与代理相同的 Pod 中运行该服务。在
values.yaml 中的 authProxy.deployment.sidecars 下添加容器,并在 authProxy.deployment.volumes 下添加任何所需的卷。使用 localhost URL,例如 http://localhost:10002。
- 独立部署: 独立部署服务,并将
extAuthz.serviceUrl 指向它。使用集群内 DNS 名称,例如 http://my-auth-service.my-namespace.svc.cluster.local:8080,或者如果服务有自己的入口,则使用外部 HTTPS URL。
示例部署
下面的例子是一个最小的 Python ext_authz 服务,它执行 OAuth2 客户端凭证令牌交换。在每个请求中,它返回一个带有新鲜访问令牌的缓存 Authorization 头,并在其过期前从配置的令牌端点刷新。完整示例请参见图表仓库中的 e2e/oauth/。
"""ext_authz 服务,执行 OAuth2 客户端凭证令牌交换。
作为 sidecar(或独立服务)与主身份验证代理组件一起运行。
在每个 ext_authz 检查请求中,它返回一个缓存的 OAuth 访问令牌,
并在过期前从配置的令牌端点刷新。
环境变量:
OAUTH_TOKEN_URL – 令牌端点(例如 https://login.example.com/oauth/token)
OAUTH_CLIENT_ID – 用于凭证授权的客户端 ID
OAUTH_CLIENT_SECRET– 用于凭证授权的客户端密钥
OAUTH_SCOPE – (可选)请求的范围,空格分隔
LISTEN_PORT – (可选)监听端口,默认 10002
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import os
import sys
import threading
import time
import urllib.request
import urllib.parse
# ---------------------------------------------------------------------------
# 配置
# ---------------------------------------------------------------------------
TOKEN_URL = os.environ["OAUTH_TOKEN_URL"]
CLIENT_ID = os.environ["OAUTH_CLIENT_ID"]
CLIENT_SECRET = os.environ["OAUTH_CLIENT_SECRET"]
SCOPE = os.environ.get("OAUTH_SCOPE", "")
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "10002"))
# 在实际过期前提前多少秒刷新令牌。
EXPIRY_BUFFER_SECONDS = 30
# ---------------------------------------------------------------------------
# 令牌缓存(线程安全)
# ---------------------------------------------------------------------------
_lock = threading.Lock()
_cached_token: str | None = None
_token_expiry: float = 0 # 纪元秒
def _fetch_token() -> tuple[str, float]:
"""执行 client_credentials 授权并返回 (access_token, expiry_epoch)。"""
data = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
**({"scope": SCOPE} if SCOPE else {}),
}).encode()
req = urllib.request.Request(
TOKEN_URL,
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
body = json.loads(resp.read())
access_token = body["access_token"]
expires_in = int(body.get("expires_in", 3600))
expiry = time.time() + expires_in - EXPIRY_BUFFER_SECONDS
return access_token, expiry
def get_token() -> str:
"""返回有效的访问令牌,必要时刷新。"""
global _cached_token, _token_expiry
with _lock:
if _cached_token and time.time() < _token_expiry:
return _cached_token
# 在锁外获取,以便其他请求不会因 I/O 而阻塞。
token, expiry = _fetch_token()
with _lock:
_cached_token = token
_token_expiry = expiry
print(f"刷新了 OAuth 令牌(将在 {int(expiry - time.time())} 秒后过期)", flush=True)
return token
# ---------------------------------------------------------------------------
# ext_authz HTTP 处理器
# ---------------------------------------------------------------------------
class Handler(BaseHTTPRequestHandler):
def do_any(self):
try:
token = get_token()
except Exception as exc:
print(f"OAuth 令牌获取失败:{exc}", flush=True)
self.send_response(500)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"OAuth 令牌交换失败")
return
self.send_response(200)
# 根据需要替换头名称 - 此头将被转发到上游 LLM 提供商/网关。
self.send_header("Authorization", f"Bearer {token}")
self.end_headers()
# 处理 Envoy 可能为 ext_authz 检查发送的每种方法。
do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = do_HEAD = do_OPTIONS = do_any
def log_message(self, format, *args):
# 更安静的日志 — 仅打印错误。
pass
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", LISTEN_PORT), Handler)
print(f"ext-authz-oauth 正在监听 :{LISTEN_PORT}", flush=True)
print(f" token_url={TOKEN_URL} client_id=<已编辑>", flush=True)
server.serve_forever()
有关 extAuthz 参数的完整列表,请参阅 Helm 图表 README。
编写 ext_proc 转换器
当您需要重写请求或响应体时,例如在 OpenAI 格式和自定义网关格式之间转换,或向请求负载注入额外字段,请使用 ext_proc。这使用了 Envoy 的 ext_proc 过滤器。
与 ext_authz(HTTP)不同,ext_proc 使用双向 gRPC 流。Envoy 为每个处理阶段(请求头、请求体、响应头、响应体)向您的转换器服务发送一条消息,您的服务回复每个阶段的变更。您的转换器必须实现 envoy.service.ext_proc.v3.ExternalProcessor gRPC 服务。有关示例 Go 实现,请参见图表仓库中的 e2e/transformer/。
何时使用 ext_proc 与 ext_authz
| 功能 | ext_authz | ext_proc |
|---|
| 修改请求头 | 是 | 是 |
| 修改响应头 | 否 | 是 |
| 修改请求体 | 否 | 是 |
| 修改响应体 | 否 | 是 |
| 协议 | HTTP | gRPC |
如果只需要注入身份验证头(例如 API 密钥),请使用 ext_authz。如果需要重写体,请使用 ext_proc。两者可以同时启用。
在 values.yaml 中启用 ext_proc:
authProxy:
transformer:
enabled: true
serviceUrl: "grpc://my-transformer:50051"
timeout: "10s"
failureModeAllow: false
processingMode:
requestHeaderMode: "SEND"
requestBodyMode: "BUFFERED"
responseHeaderMode: "SKIP"
responseBodyMode: "NONE"
设置 failureModeAllow: true 以在转换器不可用时允许请求通过。默认值(false)会拒绝请求。
处理模式
通过 processingMode 控制将哪些阶段发送给您的转换器。仅启用您需要的阶段,因为禁用未使用的阶段可减少延迟。
| 字段 | 选项 | 描述 |
|---|
requestHeaderMode | SEND, SKIP, DEFAULT | 是否转发请求头。 |
responseHeaderMode | SEND, SKIP, DEFAULT | 是否转发响应头。 |
requestBodyMode | NONE, BUFFERED, STREAMED, BUFFERED_PARTIAL | 如何发送请求体。 |
responseBodyMode | NONE, BUFFERED, STREAMED, BUFFERED_PARTIAL | 如何发送响应体。 |
requestTrailerMode | SEND, SKIP | 是否转发请求尾。 |
responseTrailerMode | SEND, SKIP | 是否转发响应尾。 |
- 使用
BUFFERED 进行请求体重写:在发送前缓冲整个体,对于 JSON 重写最简单。
- 使用
STREAMED 进行流式 LLM 响应体重写:在块到达时发送,延迟更低但实现更复杂。
- 使用
NONE 完全跳过某个阶段。
在修改体时,您的 ext_proc 服务还必须通过 HeaderMutation 更新 content-length 头以匹配新的体大小。如果 content-length 与修改后的体不匹配,Envoy 将拒绝响应。
请求流程
启用了 ext_proc 进行头注入和体重写的示例:
curl -H "X-LangSmith-LLM-Auth: <JWT>" -d '{"model":"gpt-4",...}'
-> Envoy(:10000)
-> 内置 Envoy JWT 过滤器(验证签名、iss、aud)
-> `ext_proc` 过滤器 -> transformer:50051 (gRPC)
<- 阶段 1: request_headers -> 修改头(注入 Authorization)
<- 阶段 2: request_body -> 修改体(重写 JSON)+ 更新 content-length
-> 上游 LLM 提供商或网关
示例部署
下面的示例将一个最小的 Go 转换器部署为 Kubernetes Deployment。它从请求头读取 JWT,注入 Authorization 头,并将请求体从 OpenAI 格式重写为自定义格式。
transformer-configmap.yaml
transformer-deployment.yaml
对于生产环境,请预构建容器镜像,而不是在初始化容器中编译。有关多阶段构建示例,请参阅 Helm 图表仓库中的 e2e/transformer/Dockerfile。
附加配置
HTTP 代理
Envoy 不遵循 HTTP_PROXY、HTTPS_PROXY 或 NO_PROXY 环境变量。请显式配置 HTTP 代理:
authProxy:
httpProxy:
enabled: true
host: "proxy.example.com"
port: 3128
noProxy:
- "internal.corp"
- ".internal.corp"
其他选项
有关入口、自动缩放、资源限制和其他配置选项,请参阅 Helm 图表 README。
为了生产环境的可靠性,请将 authProxy.autoscaling.hpa.minReplicas 设置为至少 3。
完整配置示例
authProxy:
upstream: "https://gateway.example.com" # 您的 LLM 网关或提供商
jwtIssuer: "langsmith" # 必须与 LangSmith 上的 LLM_AUTH_PROXY_ISSUER 匹配
jwtAudiences:
- "my-audience" # 必须与 LangSmith 中的组织设置匹配
# 选项 A:远程 JWKS(推荐用于生产环境)
# Envoy 从 LangSmith 的 /.well-known/jwks.json 端点获取并缓存公钥。
jwksUri: "https://langsmith.example.com/.well-known/jwks.json" # 自托管
# jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json" # SaaS
jwksCacheDurationSeconds: 300 # Envoy 缓存 JWKS 的时长(默认 5 分钟)
# 选项 B:内联 JWKS(仅限测试或离线环境)
# jwksJson: '{"keys": [...]}'
# ext_authz:仅头部的身份验证逻辑(仅在需要时包含)
# 使用此功能来注入、删除或修改授权头。
# 您的服务在 /check 收到一个 HTTP 请求,其中包含已验证的 JWT,
# 位于 x-langsmith-llm-auth 头中,并响应要向上游注入的头。
extAuthz:
enabled: true
serviceUrl: "http://localhost:10002" # sidecar URL
# serviceUrl: "http://ext-authz.<namespace>.svc.cluster.local:10002" # 独立部署
sendBody: false # 设置为 true 以包含请求体
# transformer:请求/响应体转换(仅在需要时包含)
# 当您需要重写请求或响应体时(例如 OpenAI -> 自定义格式)使用此功能。
# 可以与 ext_authz 同时启用。
transformer:
enabled: true
serviceUrl: "grpc://transformer.<namespace>.svc.cluster.local:50051"
timeout: "10s"
failureModeAllow: false # 如果转换器不可用则拒绝
processingMode:
requestHeaderMode: "SEND" # 转发请求头(读取 JWT,注入身份验证)
responseHeaderMode: "SKIP" # 跳过响应头
requestBodyMode: "BUFFERED" # 缓冲完整体以进行 JSON 重写
responseBodyMode: "NONE" # 跳过响应体
requestTrailerMode: "SKIP"
responseTrailerMode: "SKIP"
JWT 声明参考
LangSmith 使用 Ed25519 (EdDSA) 签署 JWT。公钥在 /.well-known/jwks.json 提供,并由代理自动获取。身份验证代理使用这些公钥验证签名。
| 声明 | 描述 |
|---|
iat, exp, jti, nbf | 标准 JWT 声明(签发时间、过期时间、JWT ID、生效时间) |
iss | 签发者。SaaS 为 langsmith;自托管通过 LLM_AUTH_PROXY_ISSUER 设置 |
aud | 受众。匹配 LangSmith 组织设置中的 JWT 受众 |
sub | 行为者标识符(用户 ID、评估器 ID、助手 ID 或 API 密钥 ID) |
actor_type | 取值:user、evaluator、agent-builder、api_key 之一 |
workspace_id | 工作区 ID |
organization_id | 组织 ID |
request_id | 请求关联 ID |
ls_user_id | LangSmith 用户 ID(仅当 actor_type 为 user 时存在) |
JWT 在 x-langsmith-llm-auth 请求头中传递给您的 ext_authz 或转换器服务。
常见问题解答
支持。通过 values.yaml 中的 httpProxy 部分配置 HTTP 代理。详见 HTTP 代理。
支持,通过 customCa 支持自定义 CA 证书,通过 mtls 支持双向 TLS。
单个身份验证代理能否路由到多个上游 LLM 网关?
不能。身份验证代理只有一个 upstream 字段。
可以。多个组织可以通过其在 LangSmith 中的模型配置指向同一个身份验证代理实例。
LangSmith 到身份验证代理的连接能否使用 HTTP 而不是 HTTPS?
可以,但我们建议将身份验证代理放在专用入口之后,以便通信使用 HTTPS。要允许 HTTP,请在您的 LangSmith values.yaml 中的 commonEnv 和 playground.deployment.extraEnv 中添加 LLM_AUTH_PROXY_ACCEPT_HTTP。
Helm 图表参考
有关可配置值的完整列表,请参阅 Helm 图表 README。