ate-env-api accepts any caller: no authentication on the gRPC or MCP surface, while it holds a privileged ateapi identity
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
- #100 @tomergee による — マージされずにクローズ
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 22/100
調査の方向性
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 の本文から書いたものです。
説明
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-apiis 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 currentmain(cmd/ate-env-api/main.go,internal/apiservice,internal/mcp).- Deployed on Substrate
v0.4.0-gke.0viaate-env manifest;ate-env-apifronted by an internal load balancer so a Deployment in a second GKE cluster can drive environments.
Steps to reproduce
- Deploy with
ate-env manifest | kubectl apply -f -. - From any pod in the cluster (no ServiceAccount, no token, no TLS):
All succeed. AddingH='-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}'-H 'authorization: Bearer garbage'changes nothing (the header is never read). - Code:
cmd/ate-env-api/main.go—grpcServer := grpc.NewServer()(noUnaryInterceptor/StreamInterceptor),mux.Handle("/", grpcServer)andmux.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 showsport-forwardand 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
- Static bearer, opt-in:
--auth-token-file <path>; when set, a gRPC unary+stream interceptor and an HTTP middleware requireauthorization: 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=). - Kubernetes-native, opt-in:
--auth-mode=tokenreview[:audience]— validate the bearer as a ServiceAccount token viaTokenReview, so callers use projected tokens exactly asate-env-apiitself does toward ateapi; optionallySubjectAccessReviewon a virtual resource for per-namespace allow-lists. - Secure default: when no auth mode is configured, warn loudly at startup and/or default
--listento127.0.0.1:7777so port-forward keeps working and nothing else does; document the exposure in the README next to theport-forwardline. - 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 を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
agent-substrate/env のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 42/100
agent-substrate/env#123 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 74/100
agent-substrate/env#121 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 42/100
agent-substrate/env#119 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 55/100
agent-substrate/env#118 ·
メンテナーはふだん 1 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 57/100
agent-substrate/env#117 ·
メンテナーはふだん 1 日以内に返信
agent-substrate/env の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 80/100
rishikanthc/Scriberr#506 ·
-
[Bug] rollingUpdatePartition panics when live StatefulSet replicas sit far below the desired countオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
sgl-project/rbg#547 ·
メンテナーはふだん 1 日以内に返信
-
area: frontend priority: normal
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
area/validator
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
TestAutonomousStop_ReportsObservedState fails under host load: 1s wall-clock ceiling vs 250ms cycleオープンtype:bug
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
nightgauge/nightgauge#2909 ·
メンテナーはふだん 1 日以内に返信