ate-env-api accepts any caller: no authentication on the gRPC or MCP surface, while it holds a privileged ateapi identity

オープン
#98 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

  • #100 @tomergee による — マージされずにクローズ

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
22/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
go, grpc, kubernetes
領域
api, security

調査の方向性

Start in cmd/ate-env-api/main.go, where grpc.NewServer() and the http.ServeMux are built without interceptors or middleware, then check internal/apiservice and internal/mcp for the request paths. Read the README around the port-forward line to see how access is documented. Done means the maintainers have picked an auth mode, and requests without valid credentials are rejected on both the gRPC and MCP surfaces while /healthz stays open; the choice between a static bearer and TokenReview is a design decision to settle first.

索引モデルが issue の本文から書いたものです。

説明

bug enhancement

Summary

ate-env-api serves the EnvironmentService, ProcessService and FileSystemService gRPC APIs and the /v1alpha/envs/{id}/mcp HTTP endpoint on a plaintext h2c listener (cmd/ate-env-api/main.go: grpc.NewServer() with no interceptors, http.ServeMux with no middleware, SetUnencryptedHTTP2(true)). There is no authentication or authorization of the caller anywhere in the request path: no bearer check, no mTLS, no Kubernetes TokenReview/SubjectAccessReview, no per-env ownership. The only credential in the binary is outbound (--ateapi-token-file, a projected ServiceAccount token with the ateapi audience), so the process acts on the control plane with its own privileged identity on behalf of whoever can open a TCP connection to it.

In practice: anything that can reach svc/ate-env-api:7777 can create and delete environments in the default atespace, start processes inside any environment it can name as the guest's user (root on Substrate v0.4.0, which ignores the image USER; StartProcess has no uid restriction), and read or write any file in any environment's filesystem. The environment id (sent as x-env-id / x-env-atespace metadata) is the only thing a caller needs; ids are operator-chosen names, and anyone with read access to Actors in the cluster can list them.

This is fine for the documented path (kubectl port-forward, README line 143) and becomes a real exposure as soon as the Service is reached any other way — which is the first thing a non-trivial deployment does (we needed it from a second cluster via an internal load balancer; a NeMo-Gym trainer or an agent framework will need it from its own pods).

Impact

  • Who hits it: any deployment where ate-env-api is reachable from more than the operator's laptop: cross-cluster callers, multi-tenant clusters, any namespace without a NetworkPolicy that blocks the port.
  • What is exposed: full control of every environment behind the server — arbitrary command execution as root in the sandboxes, file read/write (which includes whatever credentials the workload keeps on disk), environment create/delete — using the server's own ateapi token, so ateapi's authentication (substrate#2636) does not help: ate-env-api is the authenticated party.
  • Severity: High; P1. Not an exploit of a bug, but a missing control on a privileged proxy, and nothing in the manifests or README says so.

Environment

  • agent-substrate/env @ 1c08a7a (2026-10-14). Same shape on current main (cmd/ate-env-api/main.go, internal/apiservice, internal/mcp).
  • Deployed on Substrate v0.4.0-gke.0 via ate-env manifest; ate-env-api fronted by an internal load balancer so a Deployment in a second GKE cluster can drive environments.

Steps to reproduce

  1. Deploy with ate-env manifest | kubectl apply -f -.
  2. From any pod in the cluster (no ServiceAccount, no token, no TLS):
    H='-H x-env-id:<id> -H x-env-atespace:ate-env'
    grpcurl -plaintext $H ate-env-api.ate-env.svc:7777 ateenv.v1alpha.EnvironmentService/GetEnvironment
    grpcurl -plaintext $H -d '{"command":["id"]}' ate-env-api.ate-env.svc:7777 ateenv.v1alpha.ProcessService/StartProcess
    # -> a Process; StreamProcessOutput shows uid=0(root) on v0.4.0
    grpcurl -plaintext $H -d '{"path":"/etc/shadow"}' ate-env-api.ate-env.svc:7777 ateenv.v1alpha.FileSystemService/ReadFile
    curl -X POST ate-env-api.ate-env.svc:7777/v1alpha/envs/<id>/mcp -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
    
    All succeed. Adding -H 'authorization: Bearer garbage' changes nothing (the header is never read).
  3. Code: cmd/ate-env-api/main.go — grpcServer := grpc.NewServer() (no UnaryInterceptor/StreamInterceptor), mux.Handle("/", grpcServer) and mux.Handle("/v1alpha/envs/{id}/mcp", mcp.NewHandler(client)) with no wrapper; grep -rn authorization cmd/ate-env-api internal/apiservice internal/mcp → no matches.

Expected vs actual

  • Expected: a privileged proxy either authenticates its callers or refuses to listen on anything but loopback by default, and the docs say which.
  • Actual: listens on 0.0.0.0:7777, plaintext, accepts every request; README shows port-forward and never mentions that the port is unauthenticated.

Workaround (what we run)

An nginx Deployment in front of ate-env-api on the load balancer: if ($http_authorization != "Bearer <token>") { return 401; }, h2c proxy to ate-env-api:7777, client_max_body_size 0 (large WriteFile streams), token from a Kubernetes Secret; plus a NetworkPolicy so only the gate can reach ate-env-api directly. This works but every integrator has to rediscover it, and it is what surfaced the client-side streaming-auth bug (see the companion issue on the Python client).

Suggested fix

  1. Static bearer, opt-in: --auth-token-file <path>; when set, a gRPC unary+stream interceptor and an HTTP middleware require authorization: Bearer <token> (constant-time compare) on everything except /healthz. ~40 lines; the manifest can mount a Secret; the Python/Go clients already send the header (auth_token=).
  2. Kubernetes-native, opt-in: --auth-mode=tokenreview[:audience] — validate the bearer as a ServiceAccount token via TokenReview, so callers use projected tokens exactly as ate-env-api itself does toward ateapi; optionally SubjectAccessReview on a virtual resource for per-namespace allow-lists.
  3. Secure default: when no auth mode is configured, warn loudly at startup and/or default --listen to 127.0.0.1:7777 so port-forward keeps working and nothing else does; document the exposure in the README next to the port-forward line.
  4. Longer term: per-environment ownership (creator identity recorded on CreateEnvironment, enforced on process/file calls) so one token cannot reach every env.
主要言語
Go
スター
71
フォーク
17
平均マージ
21時間 36分
マージ済み PR(30日)
19

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

agent-substrate/env のほかの issue

agent-substrate/env の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。