Skip to content

A Bot's shell has passwordless owner access to the database in the all-in-one image #226

Description

@davidmckayv

What is wrong

The all-in-one image runs the database and the Bot's computer as two s6 services in the same
container, and the database is created with trust authentication:

docker/s6/scripts/postgres-init.sh

initdb -D "$DATA" -A trust -U openbot
pg_ctl -D "$DATA" -o "-c listen_addresses=127.0.0.1" -w start

-A trust means any local connection is accepted as whichever role it asks to be, with no password.
-U openbot makes that role the instance owner. The script explains itself this way:

Bound to loopback and trust-auth on purpose: the only client is the process beside it, inside this
container, and a password would be a secret with nobody to keep it from. Publishing 5432 from this
container would change that, which is why nothing here does.

The premise is what is wrong. The process beside it is not only the API server. docker/s6/s6-rc.d
also starts computer, which is the service that runs /bin/bash -c on a Bot's behalf, and
agent-computer/src/shell.ts states the constraint plainly:

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

Loopback inside the container is exactly where the Bot's shell is. So in this image a Bot's shell
connects to the cluster as the instance owner without a password: the audit trail, the policy store,
the credential vault and the agent tables. Nothing needs to be published for that, and no secret has
to leak, because there is no secret.

Reaching that shell needs no exploit. computer_run_command is a governed verb and the shipped
default policy is {deny: [], allow: ["true"]}, so a fresh deployment permits it, whether driven by
the Bot's operator or by content on a page the Bot was asked to read.

The append-only trail is not a second line of defence here. The trigger refuses a delete, and the
role that owns it can drop the trigger.

This is the same class of finding as the Compose one, in the deployment shape that change does not
touch, and worse: there the credentials at least had to be read out of a file.

What should be true

The cluster should require authentication that the Bot's shell does not have, so that being inside
the container is not the same as being the database's owner. That means, roughly:

  • scram-sha-256 rather than trust, with a password generated at first init and stored where only
    the API service can read it, not in the image and not in a world-readable path;
  • the API connecting with that password, which means passing it between s6 services rather than
    relying on being the only process that can reach the socket;
  • ideally the API using a role that is not the instance owner, so the append-only trigger is
    something its own connection cannot drop.

Worth deciding at the same time: whether computer and postgres belong in one image at all. The
Compose topology now says they do not, and the all-in-one exists for a different reason (one
container to run), so the answer may be that the all-in-one keeps them together and makes auth carry
the weight the container boundary carries elsewhere. Either way the two should not disagree silently.

How to check it is fixed

From the Bot's shell inside the all-in-one image, connecting to the local cluster as openbot
should be refused for want of a password, and the API should still start and migrate. A test that
asserts the shell can reach the socket and still cannot authenticate is the one worth keeping, since
the socket stays reachable by design.

Found while reviewing #218, which closes the equivalent gap in docker-compose.yml.

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