diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd611c5..3ab6e5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### A Bot's computer is no longer on the same network as the database + +Compose declared no networks, so every service shared one and reached the others by service name. +One of those services is the container a Bot's shell runs in, and another is PostgreSQL, whose +username and password are in the same file. A shell reaches whatever its container reaches, so a Bot +could open `postgres:5432` and authenticate: the audit trail, the policy store and the agent tables, +from the one container whose job is to run what a Bot asks for. The role Compose creates is the +instance owner, so the trail's append-only trigger was no defence either, being something its owner +can drop. + +PostgreSQL and `migrate`, the only service that reaches it by name, are now on a `data` network of +their own. Everything else stays where it was. Nothing changes for a deployment that runs the API +server on the host, which reaches the database through the published port and never used the shared +network for it. **A deployment that runs the server inside Compose has to join that service to both +networks**, which is the one place the two are meant to meet. + +The published port is now on loopback, as every other port in that file already was. Taking the +database off the Bots' network removes the name, not the address: a container's default gateway is +the host, and a port published on every interface answers there. From inside the computer container, +the gateway on `5432` accepted a connection and began authenticating as `openbot` on `openbot`, with +the password in the same file. **A deployment that reached the database from another machine over +this port has to reach it another way**, which is what publishing it on every interface was doing. + +This does not reach back in time. A deployment that has been running with the two on one network +should assume a Bot could have read or written the database, and look at the trail with that in +mind. + ### Name the private addresses an agent may live at Refusing `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` in production closed a hole and took something with diff --git a/docker-compose.yml b/docker-compose.yml index a832ffc0..e2e7cfbc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,30 @@ services: postgres: image: pgvector/pgvector:pg17 + # Off the network the Bots are on, because a Bot's computer is on it and a Bot has a shell. + # + # With no `networks:` anywhere, Compose puts every service on one network with DNS by service + # name, so `agent-computer` could open `postgres:5432` and the credentials three lines below are + # in this file. That is the audit trail, the policy store and the agent tables, reachable from + # the one container whose whole job is to run what a Bot asks for. `agent-computer/src/shell.ts` + # says it plainly: isolation is the container's job, a shell can reach whatever the container + # can reach. + # + # `migrate` is the only service that reaches this by name, so the two of them are all that + # belongs here. Everything else that needs the database is outside this file: the API server + # runs on the host in local development and connects through the published port below. environment: POSTGRES_DB: openbot POSTGRES_USER: openbot POSTGRES_PASSWORD: openbot ports: - - "${POSTGRES_PORT:-5432}:5432" + # Loopback, like every other port in this file, and for the reason the network split above + # exists. Published on every interface, this is reachable from the Bot's computer at the + # container's default gateway, which is the host: taking `postgres` off that network removes + # the name and leaves the address. Proven from inside `agent-computer`, where the gateway on + # :5432 answered and began authenticating as `openbot` on `openbot`, with the password in + # this file. The server on the host still reaches it, because that is what loopback is. + - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: @@ -14,6 +32,8 @@ services: interval: 5s timeout: 5s retries: 10 + networks: + - data migrate: build: @@ -26,6 +46,8 @@ services: postgres: condition: service_healthy restart: "no" + networks: + - data # The Bot's computer is long-lived so browser sessions remain signed in across turns. agent-computer: @@ -233,6 +255,16 @@ services: timeout: 5s retries: 5 +networks: + # The database and the one service that reaches it by name. Nothing a Bot can reach is on it. + # + # A deployment that runs the API server inside Compose rather than on the host puts that service + # on both this and `default`, which is the one place the two are meant to meet. + data: + # Everything else, which is where `default` already put it. Named here only so that adding a + # service without a `networks:` key keeps landing beside the Bots rather than beside the database. + default: + volumes: postgres-data: agent-workspace: diff --git a/docs/architecture.md b/docs/architecture.md index 9a0759b7..e8dc5a5c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,6 +69,8 @@ startup. `agent-computer` requires `COMPUTER_TOKEN` and permits only `/health` without it. Docker Compose binds it to `127.0.0.1:4100`. +Compose puts it on a different network from PostgreSQL. A Bot has a shell, and a shell reaches whatever its container reaches, so the database is on a `data` network carrying only itself and `migrate`, and everything else is on `default`. A deployment that runs the API server inside Compose rather than on the host joins it to both, which is the one place the two meet. + With `COMPUTER_SUPERVISOR_URL`, each Bot gets its own computer container, workspace volume, and browser profile. Without it, all Bots share `AGENT_COMPUTER_URL`. A command on the computer inherits PATH, locale and terminal names, and the proxy variables, not the rest of the process environment. Userinfo is stripped from a proxy URL. `COMPUTER_SHELL_ENV` names anything else a deployment wants passed.