Skip to content

A Bot's shell shares a network with postgres, and the credentials are in the compose file #208

Description

@beardthelion

docker-compose.yml declares no networks: stanza, so every service joins one default project
network with DNS by service name. One of those services is the container a Bot's shell runs in, and
another is postgres, whose username and password are on lines 6 and 7 of the same file.

agent-computer/src/shell.ts:262 spawns /bin/bash -c <command>. computer_run_command is a
governed verb (gateway.ts:864) and the shipped default policy is {deny: [], allow: ["true"]}
(policy-store.ts:52), so a fresh deployment permits it. The prompt driving it is either the Bot's
operator or content on a page the Bot was asked to read.

The module states the invariant this breaks in its own words, at shell.ts:18-20:

ISOLATION IS THE CONTAINER'S JOB. A shell can reach whatever the container can reach.

Observed

Compose resolves the topology itself, so this is its answer rather than a reading of the file. Every
service, agent-computer and postgres included:

  agent-computer:
    networks:
      default: null
  postgres:
    networks:
      default: null
networks:
  default:
    name: openbot_default

From a container on openbot_default, with nothing but the committed credentials and the service
name:

$ PGPASSWORD=openbot psql -h postgres -U openbot -d openbot -tAc "select current_user, version();"
openbot|PostgreSQL 17.11 ...

It is superuser, not an application role

POSTGRES_USER bootstraps the instance owner, so rolsuper is t. That matters most for the audit
trail, which is protected by a trigger rather than by permissions. Against the real schema:

=== delete WITH the append-only trigger in place ===
ERROR:  Audit events are append-only
CONTEXT:  PL/pgSQL function prevent_audit_event_mutation() line 19 at RAISE
 still_there | 1

=== drop the protection as this role ===
DROP TRIGGER

=== delete again ===
DELETE 1
 after_tamper | 0

The trigger does its job and the role removes it. So the append-only property does not hold against
anything that reaches the database on this network, and the exposure is not "a Bot can read the
trail", it is "a Bot can edit or erase the record of what Bots did". The policy store and the
agent and profile tables are on the same footing.

Where it applies

Both arrangements, for different reasons.

In the shared-computer setup, the compose agent-computer service is the one scripts/start.sh:165
starts on every run.

In the per-Bot setup that docker-compose.yml:136-138 recommends for real deployments,
COMPUTER_NETWORK is set and supervisor/src/docker.ts:356 attaches every per-Bot computer to that
same network with NetworkMode.

The per-Bot path without COMPUTER_NETWORK is the one that already does this properly, and it is
what makes the gap look like an omission rather than a decision: supervisor/src/docker.ts:327-370
gives each computer no NetworkMode, so it lands on the default bridge and Docker's isolation rules
keep it off the compose network, plus CapDrop: ["ALL"], no-new-privileges and per-Bot volumes,
with a comment for each. The compose agent-computer service gets none of that, and
agent-computer/Dockerfile has no USER, unlike the all-in-one image which chowns to pwuser at
Dockerfile:195.

KEY_ENCRYPTION_KEY is deliberately kept out of the shell environment, so encrypted credential rows
stay encrypted. Everything else in the database is in the clear.

Not proposing a shape

Splitting the computer onto a network that does not carry the data plane is the obvious direction,
but it touches COMPUTER_NETWORK, the supervisor needing to reach the computers it creates, and
SPIRE, and those constraints are yours rather than mine. Happy to implement whichever shape you
want.

Verification notes

Run against a compose postgres brought up from this file, with the real migrations applied. The
reachability and superuser results are from a container attached to openbot_default;
agent-computer's membership of that network is from docker compose config rather than from
building the Playwright image, which is the one link here that is traced and not executed.

Separately, and not the subject of this issue: bringing the stack up failed with
failed to bind host port 0.0.0.0:5432, which is the postgres port being published on every host
interface. That was raised as a follow-up on #194 and again on #195 and does not appear to have been
filed. Same file, different problem, and worth fixing in the same pass if you take this one.

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