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

Abierto
#98 2 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

  • #100 de @tomergee — cerrado sin fusionar

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
22/100
Tipo de issue
Nueva funcionalidad
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
go, grpc, kubernetes
Área
api, security

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.
Lenguaje dominante
Go
Estrellas
71
Forks
17
Merge medio
21 h 36 min
PR fusionados (30 d)
19

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de agent-substrate/env

Todos los issues de agent-substrate/env

Issues similares

Más issues de Go

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.