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

Aperta
#98 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

  • #100 di @tomergee — chiusa senza merge

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
22/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
go, grpc, kubernetes
Ambito
api, security

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.
Lingua principale
Go
Stelle
71
Fork
17
Merge medio
21h 36m
PR unite (30g)
19

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di agent-substrate/env

Tutte le issue di agent-substrate/env

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.