以编程方式使用文档

LLM 身份验证代理让您的组织能够对来自 LangSmith 的所有模型调用强制执行自己的身份验证流程,从而提供商凭据永远不会暴露给最终用户,并且每个请求都可以追溯到特定的操作者。

LLM 身份验证代理是一个基于 Envoy组件,运行在您的环境中,位于 LangSmith 和您的上游 LLM 提供商或网关(如 OpenAI、Anthropic 或内部 LLM 网关如 LiteLLM)之间。LangSmith 使用短期 JWT(JSON Web Token)对每个请求进行签名。代理验证 JWT,可选地注入提供商凭据或转换请求和响应正文,然后将请求转发到上游。它适用于 SaaSself-hosted LangSmith 客户。

在以下情况下使用 LLM 身份验证代理:

  • - 身份验证 Playground or LLM 即法官评估 针对您自己的提供商网关的请求。
  • - 注入提供商特定的 API 密钥或身份验证标头,而不向最终用户暴露它们。
  • - 转换请求或响应正文(例如,在 OpenAI 格式和自定义网关格式之间转换)。

对于 OAuth2 client_credentials 具体来说, 模型配置上的 OAuth 客户端凭据 是一种按配置自助服务替代方案,工作区管理员可以在不启动身份验证代理的情况下进行设置。路由在配置级别是互斥的——启用 OAuth 的配置不会通过身份验证代理。

工作原理

来自 LangSmith 的每个请求都通过代理中的以下步骤:

  1. 验证 JWT(签名、颁发者、受众)
  2. 调用您的 ext_authz 服务,该服务接收已验证的 JWT 并返回提供商凭据以作为标头注入
  3. 可选地调用您的 ext_proc 转换器,该转换器可以重写请求和响应正文(例如,在 OpenAI 格式和自定义网关格式之间转换)
  4. 使用自定义标头(静态或动态)将请求转发到上游提供商

ext_authz 服务 转换器都是客户部署的组件,与代理一起在您的环境中运行。可以启用其中一个或两个.

架构图显示 LangSmith 向自托管身份验证代理颁发签名 JWT,该代理验证 JWT、应用客户定义的身份验证,然后将请求转发到上游模型提供商。 架构图,展示 LangSmith 向自托管认证代理颁发签名 JWT,认证代理验证 JWT、应用客户定义的认证,并将请求转发到上游模型提供商。

前置条件

  • - LangSmith Enterprise 计划(SaaS 或自托管,版本 0.13.33+)
  • - 配备 Helm 3 的 Kubernetes 集群
  • - Envoy v1.37 或更高版本(Helm chart 默认使用 envoyproxy/envoy:v1.37-latest)
  • - 上游 LLM 提供商或网关的 URL(代理将请求转发到的目标)

1. 配置 JWT 签名(仅适用于自托管 LangSmith)

LangSmith SaaS 跳过此步骤。JWT 签名已配置。

生成 Ed25519 密钥对 使用 step CLI (或您偏好的内部流程)。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 --no-password --insecure < "$TMPDIR_KEYS/priv.pem")
SIGNING_JWKS=$(echo "$PRIV_JWK" | jq -c '{keys: [. + {use: "sig", alg: "EdDSA"}]}')
echo "$SIGNING_JWKS"

将 JWKS 存储在 Kubernetes Secret 中:

kubectl create secret generic langsmith-signing-jwks \
  --namespace <namespace> \
  --from-literal=LANGSMITH_SIGNING_JWKS="$SIGNING_JWKS"

JWKS(JSON Web Key Set)是一种标准的 JSON 格式,用于发布加密密钥。 LANGSMITH_SIGNING_JWKS 包含 Ed25519 私钥,并存储为 Kubernetes Secret。绝不公开。LangSmith 自动提取相应的公钥并在其提供服务。 /.well-known/jwks.json。认证代理获取此公共端点来验证 JWT 签名,无需私钥。

**在您的中引用密钥 LangSmith values.yaml:**

platformBackend:
  deployment:
    extraEnv:
      - name: LLM_AUTH_PROXY_ISSUER
        value: "langsmith"        # must match jwtIssuer in the auth proxy chart
      - secretRef:
          name: langsmith-signing-jwks

LLM_AUTH_PROXY_ISSUER 设置 iss 声明在已签名的 JWT 中。使用 langsmith 来匹配 SaaS 默认值,或使用自定义标识符如 langsmith:self-hosted:<short_identifier> 来区分您的安装。该值必须匹配 jwtIssuer 在 auth proxy chart 中的 第 4 步).

2. 为您的组织启用 LLM Auth Proxy

Self-hosted

选项 A: 为特定组织启用:

在 LangSmith UI 中,导航到 **设置** 页面,复制左上角的组织 ID,旁边是 **组织**.

针对您的 LangSmith PostgreSQL 数据库运行以下命令:

UPDATE organizations
SET config = config || '{"can_use_llm_auth_proxy": true}'
WHERE id = '<organization_id>';

选项 B: 为安装中的所有组织启用:

将以下内容添加到 commonEnv 在您的 LangSmith values.yaml:

commonEnv:
  DEFAULT_ORG_FEATURE_CAN_USE_LLM_AUTH_PROXY: "true"

SaaS

通过以下方式联系技术支持 支持门户 为您的组织启用 LLM Auth Proxy。

3. 在 LangSmith 中配置组织设置

在 LangSmith UI 中,导航到 **设置** > **常规**,配置以下内容:

1. **JWT 受众:** 的 aud 代理将验证的声明值(例如, example-audience)。此值必须与 jwtAudiences 中 auth proxy chart 的 第 4 步. 2. **启用 LLM auth proxy:** 为您的组织开启切换开关。 3. **允许的 URL:** 控制代理允许将 JWT 转发到哪些目标 URL。这可以防止将凭据转发到意外的主机。选择以下三个选项之一: - **允许所有** (默认):允许将 JWT 转发到任何上游 URL。相当于无限制。 - **阻止所有:** 阻止将 JWT 转发到所有 URL。 - **Custom:** 指定允许的 URL 模式明确列表。空字符串和裸 * 不被接受。当 LLM auth proxy 切换开关关闭时,该控制被禁用。

LangSmith 中的 LLM Auth Proxy 设置,显示启用 LLM auth proxy 复选框、JWT 受众字段和允许的 URL 单选按钮,其中选择了允许所有。 LangSmith 中的 LLM Auth Proxy 设置,显示启用 LLM auth proxy 复选框、JWT 受众字段和允许的 URL 单选按钮,其中选择了允许所有。

4. 安装 auth proxy Helm chart

添加 LangChain Helm 仓库:

helm repo add langchain https://langchain-ai.github.io/helm/
helm repo update

创建一个 values.yaml ,其中包含上游 URL 和 JWT 验证设置。JWKS 配置有两个选项:

  • - **jwksUri (推荐):** 指向您的 LangSmith 实例的 /.well-known/jwks.json 端点。Envoy 自动获取并缓存公钥,支持无缝密钥轮换。
  • - **jwksJson (内联):** 将 JWKS JSON 直接粘贴到 values.yaml。将此用于测试或气隙环境,其中 auth proxy 无法访问 LangSmith 的出站网络。需要更新 chart 来轮换密钥。只包含公钥组件;省略 d 字段(私钥)。

如果两者都已设置,则 jwksUri 优先。

authProxy:
  upstream: "https://gateway.example.com"
  jwtIssuer: "langsmith" # must match LLM_AUTH_PROXY_ISSUER in LangSmith values.yaml
  jwtAudiences:
    - "example-audience" # must match the org setting in LangSmith

  # Option A: remote JWKS (recommended for production)
  # Envoy fetches and caches public keys from LangSmith's /.well-known/jwks.json.
  jwksUri: "https://langsmith.example.com/.well-known/jwks.json"       # self-hosted
  # jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json"   # SaaS
  jwksCacheDurationSeconds: 300

  # Option B: inline JWKS (testing or air-gapped environments only)
  # Omit the "d" field (private key); include public key components only.
  # jwksJson: '{"keys": [{"kty": "OKP", "crv": "Ed25519", "x": "<base64url-public-key>", "use": "sig", "alg": "EdDSA"}]}'

安装 chart:

helm install langsmith-auth-proxy langchain/langsmith-auth-proxy \
  --namespace <your-namespace> \
  -f values.yaml

编写一个 ext_authz 服务

当您需要添加、删除或编辑授权标头时使用 ext_authz ,例如根据 JWT 中的身份注入提供者 API 密钥。您的服务接收经过验证的 JWT 和可选的请求正文,并返回要注入上游的标头。这使用 Envoy 的 HTTP ext_authz 过滤器 (非 gRPC)。

在以下位置启用 values.yaml:

authProxy:
  extAuthz:
    enabled: true
    serviceUrl: "http://my-auth-service:8080"
    timeout: "10s"

工作原理

在转发每个请求之前,Envoy 会调用你的服务,地址为 <serviceUrl>/check<original_path> 使用与原始请求相同的 HTTP 方法。你的服务会在 x-langsmith-llm-auth header.

你的服务返回一个纯 HTTP 响应:

  • - **2xx:** 允许该请求。任何匹配的标头 allowedUpstreamHeaders 模式(默认值: authorizationx-*)会被注入到上游请求中。要在转发前剥离 JWT,请在响应中包含 x-envoy-auth-headers-to-remove: x-langsmith-llm-auth
  • - **Non-2xx:** 拒绝该请求。状态码和任何匹配的标头 allowedClientHeaders 模式(默认值: www-authenticatex-*)会被返回给客户端。

部署选项

你的 ext_authz 服务可以以下两种方式运行:

  • Sidecar: 在与代理相同的 pod 中运行服务。将容器添加到 authProxy.deployment.sidecars 下,并在 authProxy.deployment.volumes in values.yaml下添加任何必需的卷。使用 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-oauth.py

"""ext_authz service that performs an OAuth2 client-credentials token exchange.

Runs as a sidecar (or standalone service) alongside the main auth-proxy component.
On each ext_authz check request it returns a cached OAuth access token,
refreshing it from the configured token endpoint when expired.

Environment variables:
  OAUTH_TOKEN_URL    – Token endpoint (e.g. https://login.example.com/oauth/token)
  OAUTH_CLIENT_ID    – Client ID for the credentials grant
  OAUTH_CLIENT_SECRET– Client secret for the credentials grant
  OAUTH_SCOPE        – (optional) Space-separated scopes to request
  LISTEN_PORT        – (optional) Port to listen on, default 10002
"""

from http.server import HTTPServer, BaseHTTPRequestHandler








# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
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"))

# Refresh the token this many seconds before it actually expires.
EXPIRY_BUFFER_SECONDS = 30

# ---------------------------------------------------------------------------
# Token cache (thread-safe)
# ---------------------------------------------------------------------------
_lock = threading.Lock()
_cached_token: str | None = None
_token_expiry: float = 0  # epoch seconds


def _fetch_token() -> tuple[str, float]:
    """Perform a client_credentials grant and return (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:
    """Return a valid access token, refreshing if necessary."""
    global _cached_token, _token_expiry
    with _lock:
        if _cached_token and time.time() < _token_expiry:
            return _cached_token
    # Fetch outside the lock so other requests aren't blocked on I/O.
    token, expiry = _fetch_token()
    with _lock:
        _cached_token = token
        _token_expiry = expiry
    print(f"Refreshed OAuth token (expires in {int(expiry - time.time())}s)", flush=True)
    return token


# ---------------------------------------------------------------------------
# ext_authz HTTP handler
# ---------------------------------------------------------------------------
class Handler(BaseHTTPRequestHandler):
    def do_any(self):
        try:
            token = get_token()
        except Exception as exc:
            print(f"OAuth token fetch failed: {exc}", flush=True)
            self.send_response(500)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"OAuth token exchange failed")
            return

        self.send_response(200)
        # Replace the header name as needed - this header will be forwarded to the upstream LLM provider / gateway.
        self.send_header("Authorization", f"Bearer {token}")
        self.end_headers()

    # Handle every method Envoy might send for ext_authz checks.
    do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = do_HEAD = do_OPTIONS = do_any

    def log_message(self, format, *args):
        # Quieter logs — only print errors.
        pass


if __name__ == "__main__":
    server = HTTPServer(("0.0.0.0", LISTEN_PORT), Handler)
    print(f"ext-authz-oauth listening on :{LISTEN_PORT}", flush=True)
    print(f"  token_url={TOKEN_URL} client_id=<redacted>", flush=True)
    server.serve_forever()

有关完整的 extAuthz 参数列表,请参阅 Helm chart README.

编写一个 ext_proc 转换器

当你需要重写请求或响应正文时使用,例如在不同格式之间转换或在请求负载中注入额外字段时。这利用了 Envoy 的 ext_proc filter ext_proc 过滤器.

ext_authz (HTTP)不同, ext_proc 使用双向 gRPC 流。Envoy 在每个处理阶段(请求标头、请求正文、响应标头、响应正文)向你的转换器服务发送一条消息,你的服务则回复每个阶段的修改。你的转换器必须实现 envoy.service.ext_proc.v3.ExternalProcessor gRPC 服务。参见 e2e/transformer/ 中的示例 Go 实现。

何时使用 ext_proc vs ext_authz

功能ext_authzext_proc
修改请求标头
修改响应标头
修改请求正文
修改响应正文
协议HTTPgRPC

使用 ext_authz 如果您只需要注入认证头部,例如用于 API 密钥。请使用 ext_proc 如果您需要重写请求体。两者可以同时启用。

启用 ext_proc in values.yaml:

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。只启用您需要的阶段,因为禁用未使用的阶段可以减少延迟。

字段选项描述
requestHeaderModeSEND, SKIP, DEFAULT是否转发请求头。
responseHeaderModeSEND, SKIP, DEFAULT是否转发响应头。
requestBodyModeNONE, BUFFERED, STREAMED, BUFFERED_PARTIAL如何发送请求体。
responseBodyModeNONE, BUFFERED, STREAMED, BUFFERED_PARTIAL如何发送响应体。
requestTrailerModeSEND, SKIP是否转发请求尾部。
responseTrailerModeSEND, SKIP是否转发响应尾部。
  • - 使用 BUFFERED 进行请求体重写:在发送前缓冲完整请求体,最适合 JSON 重写。
  • - 使用 STREAMED 进行流式 LLM 响应体重写:在块到达时立即发送,延迟更低但实现更复杂。
  • - 使用 NONE 完全跳过某个阶段。

请求流程

启用 ext_proc 进行头部注入和请求体重写的示例:

curl -H "X-LangSmith-LLM-Auth: " -d '{"model":"gpt-4",...}'
  -> Envoy(:10000)
  -> built-in Envoy JWT filter (validate sig, iss, aud)
  -> `ext_proc` filter -> transformer:50051 (gRPC)
    <- phase 1: request_headers -> mutate headers (inject Authorization)
    <- phase 2: request_body   -> mutate body (rewrite JSON) + update content-length
  -> upstream LLM provider or gateway

示例部署

以下示例将一个最小的 Go 转换器部署为 Kubernetes Deployment。它从请求头中读取 JWT,注入一个 Authorization 头部,并将请求体从 OpenAI 格式重写为自定义格式。

transformer-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: transformer-source
data:
  main.go: |
    package main

        "encoding/json"
        "fmt"
        "io"
        "log"
        "net"
        "strings"

        core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
        ext_proc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
        "google.golang.org/grpc"
    )

    type server struct {
        ext_proc.UnimplementedExternalProcessorServer
    }

    func (s *server) Process(stream ext_proc.ExternalProcessor_ProcessServer) error {
        for {
            req, err := stream.Recv()
            if err == io.EOF {
                return nil
            }
            if err != nil {
                return err
            }

            var resp *ext_proc.ProcessingResponse
            switch v := req.Request.(type) {
            case *ext_proc.ProcessingRequest_RequestHeaders:
                resp = handleRequestHeaders(v.RequestHeaders)
            case *ext_proc.ProcessingRequest_RequestBody:
                resp = handleRequestBody(v.RequestBody)
            default:
                resp = &ext_proc.ProcessingResponse{}
            }

            if err := stream.Send(resp); err != nil {
                return err
            }
        }
    }

    func handleRequestHeaders(headers *ext_proc.HttpHeaders) *ext_proc.ProcessingResponse {
        var jwtValue string
        for _, h := range headers.Headers.Headers {
            if strings.EqualFold(h.Key, "x-langsmith-llm-auth") {
                if len(h.RawValue) > 0 {
                    jwtValue = string(h.RawValue)
                } else {
                    jwtValue = h.Value
                }
                break
            }
        }

        resp := &ext_proc.ProcessingResponse{
            Response: &ext_proc.ProcessingResponse_RequestHeaders{
                RequestHeaders: &ext_proc.HeadersResponse{},
            },
        }

        if jwtValue != "" {
            // TODO: Replace with your auth logic, e.g. exchange JWT for a
            // provider-specific token, call a secrets manager, etc.
            providerKey := "Bearer your-provider-key"

            headerResp := resp.GetRequestHeaders()
            headerResp.Response = &ext_proc.CommonResponse{
                HeaderMutation: &ext_proc.HeaderMutation{
                    SetHeaders: []*core.HeaderValueOption{
                        {
                            Header: &core.HeaderValue{
                                Key:      "Authorization",
                                RawValue: []byte(providerKey),
                            },
                        },
                    },
                },
            }
        }
        return resp
    }

    func handleRequestBody(body *ext_proc.HttpBody) *ext_proc.ProcessingResponse {
        resp := &ext_proc.ProcessingResponse{
            Response: &ext_proc.ProcessingResponse_RequestBody{
                RequestBody: &ext_proc.BodyResponse{},
            },
        }

        var original map[string]interface{}
        if err := json.Unmarshal(body.Body, &original); err != nil {
            log.Printf("Body parse failed, passing through: %v", err)
            return resp
        }

        // TODO: Replace with your transformation logic.
        // This example wraps the OpenAI-format body in a custom envelope.
        transformed := map[string]interface{}{
            "custom_model":    original["model"],
            "custom_messages": original["messages"],
            "metadata":        map[string]string{"source": "langsmith"},
        }

        newBody, err := json.Marshal(transformed)
        if err != nil {
            log.Printf("Body marshal failed, passing through: %v", err)
            return resp
        }

        // IMPORTANT: update content-length to match the new body size.
        bodyResp := resp.GetRequestBody()
        bodyResp.Response = &ext_proc.CommonResponse{
            Status: ext_proc.CommonResponse_CONTINUE_AND_REPLACE,
            HeaderMutation: &ext_proc.HeaderMutation{
                SetHeaders: []*core.HeaderValueOption{
                    {
                        Header: &core.HeaderValue{
                            Key:      "content-length",
                            RawValue: []byte(fmt.Sprintf("%d", len(newBody))),
                        },
                    },
                },
            },
            BodyMutation: &ext_proc.BodyMutation{
                Mutation: &ext_proc.BodyMutation_Body{
                    Body: newBody,
                },
            },
        }
        return resp
    }

    func main() {
        lis, err := net.Listen("tcp", ":50051")
        if err != nil {
            log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
        ext_proc.RegisterExternalProcessorServer(s, &server{})
        log.Println("transformer listening on :50051")
        if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v", err)
        }
    }
  go.mod: |
    module transformer

    go 1.23

    require (
        github.com/envoyproxy/go-control-plane/envoy v1.32.4
        google.golang.org/grpc v1.72.1
    )

transformer-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: transformer
  labels:
    app: transformer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: transformer
  template:
    metadata:
      labels:
        app: transformer
    spec:
      initContainers:
        - name: build
          image: golang:1.23
          command: ["sh", "-c"]
          args:
            - |
              cp /src/main.go /src/go.mod /build/ &&
              cd /build &&
              go mod tidy &&
              CGO_ENABLED=0 go build -o /build/transformer ./main.go
          volumeMounts:
            - name: source
              mountPath: /src
              readOnly: true
            - name: binary
              mountPath: /build
      containers:
        - name: transformer
          image: gcr.io/distroless/static-debian12:nonroot
          command: ["/app/transformer"]
          ports:
            - containerPort: 50051
          volumeMounts:
            - name: binary
              mountPath: /app
              readOnly: true
      volumes:
        - name: source
          configMap:
            name: transformer-source
        - name: binary
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: transformer
  labels:
    app: transformer
spec:
  selector:
    app: transformer
  ports:
    - port: 50051
      targetPort: 50051
      protocol: TCP

其他配置

HTTP 代理

Envoy 不遵循 HTTP_PROXY, HTTPS_PROXY, or NO_PROXY 环境变量。请显式配置 HTTP 代理:

authProxy:
  httpProxy:
    enabled: true
    host: "proxy.example.com"
    port: 3128
    noProxy:
      - "internal.corp"
      - ".internal.corp"

无公共入口部署

当认证代理没有公共入口且只能通过内部 Kubernetes 网络访问时,必须配置 LangSmith 服务允许对私有 IP 地址的出站请求。如果没有这些设置,内置的 SSRF 保护会阻止对私有 IP 的请求。

将以下环境变量添加到您的 LangSmith values.yaml:

  • - **SSRF_ALLOW_K8S_INTERNAL** — 在所有发起 LLM 调用的服务上都需要。在 commonEnv 中为支持该配置的服务添加,或添加到每个服务各自的 extraEnv 对于不支持的服务 commonEnv.
  • - **SSRF_ALLOW_PRIVATE_IPS_PLAYGROUND** — 在此服务上必需 playground 仅限服务。请将此添加到 playground.deployment.extraEnv.
# Allow all LLM-calling services to reach the auth proxy on private IPs
commonEnv:
  SSRF_ALLOW_K8S_INTERNAL: "true"

# Allow the playground service to reach the auth proxy on private IPs
playground:
  deployment:
    extraEnv:
      - name: SSRF_ALLOW_K8S_INTERNAL
        value: "true"
      - name: SSRF_ALLOW_PRIVATE_IPS_PLAYGROUND
        value: "true"

If commonEnv 不适用于您部署中的所有必需服务,请设置 SSRF_ALLOW_K8S_INTERNAL 通过以下方式单独设置 extraEnv 在每个调用 LLM 的服务上。

其他选项

有关入口、自动扩展、资源限制和其他配置选项,请参阅 Helm chart README.

完整配置示例

authProxy:
  upstream: "https://gateway.example.com"   # your LLM gateway or provider
  jwtIssuer: "langsmith"                    # must match LLM_AUTH_PROXY_ISSUER on LangSmith
  jwtAudiences:
    - "example-audience"                    # must match org setting in LangSmith

  # Option A: remote JWKS (recommended for production)
  # Envoy fetches and caches public keys from LangSmith's /.well-known/jwks.json endpoint.
  jwksUri: "https://langsmith.example.com/.well-known/jwks.json"   # self-hosted
  # jwksUri: "https://api.smith.langchain.com/.well-known/jwks.json"  # SaaS
  jwksCacheDurationSeconds: 300             # how long Envoy caches the JWKS (default 5 min)

  # Option B: inline JWKS (testing or air-gapped environments only)
  # jwksJson: '{"keys": [...]}'

  # ext_authz: header-only auth logic (include only if needed)
  # Use this to inject, remove, or modify authorization headers.
  # Your service receives an HTTP request at /check with the validated JWT
  # in the x-langsmith-llm-auth header and responds with headers to inject upstream.
  extAuthz:
    enabled: true
    serviceUrl: "http://localhost:10002"    # sidecar URL
    # serviceUrl: "http://ext-authz.<namespace>.svc.cluster.local:10002"  # separate deployment
    sendBody: false                         # set true to include request body

  # transformer: request/response body transformation (include only if needed)
  # Use this when you need to rewrite request or response bodies (e.g. OpenAI -> custom format).
  # Can be enabled alongside ext_authz.
  transformer:
    enabled: true
    serviceUrl: "grpc://transformer.<namespace>.svc.cluster.local:50051"
    timeout: "10s"
    failureModeAllow: false                 # reject if transformer is unavailable
    processingMode:
      requestHeaderMode: "SEND"             # forward request headers (read JWT, inject auth)
      responseHeaderMode: "SKIP"            # skip response headers
      requestBodyMode: "BUFFERED"           # buffer full body for JSON rewriting
      responseBodyMode: "NONE"              # skip response body
      requestTrailerMode: "SKIP"
      responseTrailerMode: "SKIP"

JWT 声明参考

LangSmith 使用以下方式签署 JWT **Ed25519 (EdDSA)**。公钥在以下位置提供 /.well-known/jwks.json ,代理自动获取。认证代理使用这些公钥验证签名。

声明描述
iat, exp, jti, nbf标准 JWT 声明(签发时间、过期时间、JWT ID、生效时间)
iss签发者。 langsmith 对于 SaaS;通过以下方式设置 LLM_AUTH_PROXY_ISSUER 对于自托管
aud受众。与 LangSmith 组织设置中的 JWT 受众匹配
sub参与者标识符(用户 ID、评估器 ID、助手 ID 或 API 密钥 ID)
actor_type以下之一: user, evaluator, agent-builder, api_key
workspace_id工作区 ID
workspace_name工作区名称
organization_id组织 ID
organization_name组织名称
request_id请求关联 ID
ls_user_idLangSmith 用户 ID(仅当 actor_type is user)

JWT 被传递给您的 ext_authz 或转换器服务位于 x-langsmith-llm-auth 请求头中。

FAQ

Does the auth proxy support corporate proxies?

是的。通过 httpProxy 部分配置 values.yaml。请参阅 HTTP 代理 了解更多详情。

Does the auth proxy support custom certificates?

是的,通过 customCa 用于自定义 CA 证书,以及 mtls 用于双向 TLS。

Can a single auth proxy route to multiple upstream LLM gateways?

否。认证代理只有一个 upstream field.

Can the auth proxy serve multiple organizations?

是的。多个组织可以通过 LangSmith 中的模型配置指向同一个认证代理实例。

Can the LangSmith to auth proxy connection use HTTP instead of HTTPS?

是的,但仅在自托管中,我们通常建议将认证代理放置在专用入口后面以使用 HTTPS 通信。要允许 HTTP,请添加 LLM_AUTH_PROXY_ACCEPT_HTTP to commonEnvplayground.deployment.extraEnv 在您的 LangSmith values.yaml. 要启用到认证代理的 HTTP 流量以支持 Chat and Insights,请在各自的 extraEnv sections: config.polly.agent.extraEnv (对于 Chat,之前称为 Polly)和 config.insights.agent.extraEnv.

Does the auth proxy work without a public ingress?

是的。当认证代理仅可通过内部 Kubernetes 网络访问(无公共入口)时,添加 SSRF_ALLOW_K8S_INTERNAL 向所有发起 LLM 调用和两者的服务 SSRF_ALLOW_K8S_INTERNALSSRF_ALLOW_PRIVATE_IPS_PLAYGROUNDplayground 服务。请参阅 无公网入口的部署 了解配置详情。

When should I use the LLM auth proxy versus OAuth client credentials on a model configuration?

当认证需要 OAuth2 之外的自定义逻辑时,请使用 LLM 身份验证代理 client_credentials。例如,将 LangSmith JWT 交换为特定于提供商的令牌、注入 GCP 或 AWS 身份,或重写请求和响应正文。请使用 模型配置上的 OAuth 客户端凭据 当每个工作区或团队需要对自己的 OAuth2 进行自助服务控制时 client_credentials 针对自定义网关时。两者可以在同一组织内共存;路由按配置进行。

Helm chart 参考

有关所有可配置值的完整列表,请参阅 Helm chart README.