devboxes

Give agents variables and secrets

To do it yourself:

In the web app, open Settings → Secrets, pick the whole Organization or a Project, select Add, and choose An API key or token, or A Postgres database for a database. The form asks for what secrets.set takes below. A Redis-compatible store records that your Organization wants one; devboxes doesn't connect Redis stores yet.

Agents see your variables and secrets as environment variables. Set one for a single Project:

secret.json
{
  "organizationId": "<organization id>",
  "projectId": "<project id>",
  "key": "NPM_TOKEN",
  "kind": "secret",
  "value": "<the secret>",
  "hosts": ["registry.npmjs.org"]
}
devboxes invoke secrets.set --input @secret.json

Delete secret.json afterwards. Leave out projectId to set the entry for the whole Organization. Then attach it to Projects with secrets.scope. A Project's own entry takes the place of an Organization entry under the same name. secrets.list gives each entry's receivedBy: the Projects whose tasks get it.

A secret names in hosts the hosts it may be sent to, such as api.github.com. devboxes sends a secret only over HTTPS on port 443, so a host takes no port. Name each host exactly; devboxes refuses wildcards such as *.example.com and IP addresses. A managed task can reach a secret's hosts even when its Organization doesn't allow them. swapIn names the parts of a request that may carry the secret: header by default, which covers every request header, HTTP Basic credentials included. Add path, query or body only when the API takes its key there, for example path for a Telegram bot token. A host can keep and show what a request carries in those parts, a header included, for example in a message it posts.

When devboxes can't protect a secret, set "unprotected": true instead of hosts. Tasks get its real value, so the agent can read it and send it anywhere, and that risk is yours.

On a devboxes machine, a task gets a stand-in for a secret with hosts, never the value itself. Its environment variable holds __devboxes_<KEY>__, and devboxes swaps in the value only in HTTPS requests to the secret's hosts, in the parts that swapIn names.

In a response from those hosts, devboxes puts the stand-in back wherever the value appears the way devboxes sent it: as is, URL-encoded, JSON-escaped, or inside HTTP Basic credentials. That covers the headers, the body, and WebSocket messages. A value that a host splits across separate WebSocket messages, or encodes another way, reaches the agent. devboxes refuses a compressed response from those hosts, and closes a WebSocket connection at a message it can't read whole.

Tools reach those hosts through the proxy that devboxes sets in HTTPS_PROXY. A tool that ignores the proxy sends the stand-in, and the request fails to authenticate. Through the proxy, a task reaches public addresses only, on ports 443 and 80. A task on your own machine gets the real secret: the value of a secret with hosts, and a database's URL with its login, the read-only login when the work may only read. secrets.set, secrets.list and secrets.inspect warn about each unprotected secret.

Databases

devboxes connects Postgres databases. Set the database's URL as a secret with "database": true instead of hosts:

database.json
{
  "organizationId": "<organization id>",
  "projectId": "<project id>",
  "key": "ORDERS_DB",
  "kind": "secret",
  "value": "postgresql://app:<password>@db.example.com:5432/orders?sslmode=verify-full",
  "database": true
}

The URL starts with postgres:// or postgresql://; devboxes refuses another scheme with DATABASE_SCHEME_UNSUPPORTED. It names the host exactly, never an IP address, and a port other than 0, 53, 80 and 443, 5432 when it names none. It holds the user, of at most 63 bytes, the password, of at most 65530 bytes with each double quote counted as two, and one database, whose name is at most 63 bytes, holds no single quote (') and isn't pgbouncer.

Its query may hold these parameters, and devboxes refuses any other with DATABASE_PARAMETER_UNSUPPORTED, naming it:

ParameterWhat devboxes does with it
sslmoderequire, verify-ca or verify-full, verify-full when the URL names none. devboxes connects to the database with this TLS mode. verify-ca and verify-full check the database's certificate against public certificate authorities only, so use require for a database whose certificate a private authority signed.
application_name, options, connect_timeoutThe task's URL carries them, so the task's client uses them as it would with the database. None may hold a control character.
channel_bindingprefer or disable. devboxes' connection to your database can't bind the channel, so it refuses channel_binding=require: remove it from the URL.

On a devboxes machine, a task gets a URL to a database machine of its own, which only that task reaches, with a stand-in password, for example postgresql://app:__devboxes_ORDERS_DB__@devboxes-taskdb-<task id>.devboxes-runners.svc.cluster.local:15432/orders?sslmode=prefer. Each database gets its own port, from 15432 onward in the order of their names, and reaches only the database the URL names. The database machine runs beside the task, with room for five databases, so devboxes refuses work on its machines that would receive more with DATABASE_LIMIT. Work on your own Runner has no such limit. devboxes connects each of the task's connections to the database with your login, over TLS, as a connection of its own, so prepared statements, SET and LISTEN work as they do on a direct connection. secrets.list shows a database's host, port, user, database and parameters, never a password.

On your own machine, a task gets the database's URL with your login, or the read-only login when the work may only read, and with the URL's sslmode, verify-full when it names none. With verify-ca or verify-full, the task's client checks the database's certificate itself, so the machine needs the certificate of the authority that signed it, for libpq in ~/.postgresql/root.crt.

A managed task doesn't get your login, but it can come to hold a working one. It reaches your database only through its database machine, which alone reaches the database's host and port. Through its URL it can still set the login's password, for example with ALTER ROLE CURRENT_USER PASSWORD, and send that password out through a host it reaches, so a database that accepts connections from anywhere then accepts it. devboxes' stored login stops working.

A client may set a setting when it connects, directly or as -c name=value in options or PGOPTIONS, for any setting that PostgreSQL 17 lets every user SET, such as statement_timeout or search_path; devboxes sets it on the database. A connection that sets any other setting when it connects is refused, with the setting's name; set such a setting with SET after connecting. devboxes doesn't set a value longer than about 125 bytes, so set a longer one with SET too. devboxes ignores extra_float_digits, which Java's PostgreSQL driver sends and which changes nothing on PostgreSQL 12 or later.

To let tasks only read a database, give it a login with read grants only, for example a user in the pg_read_all_data role on PostgreSQL 15 or later. PostgreSQL enforces read-only only through such a login. On PostgreSQL 14 or earlier, and in a database upgraded from it, every role may create tables in schema public, so such a user can still write to tables it creates there until the database's owner runs REVOKE CREATE ON SCHEMA public FROM PUBLIC. Such a login can still create large objects and change its own password: PostgreSQL has no privilege that removes either. Pass its user and password, within the same limits as the URL's, as readOnlyUser and readOnlyPassword with the URL, and set "readOnly": true: tasks then get only that login. A secrets.set without readOnlyPassword removes the database's read-only login. A database's readOnly, which only owners and admins set, is the boundary devboxes enforces for every task.

A Blueprint version whose graph sets databaseReadOnly also gives its tasks only the read-only login of every database. Any member can commit a Blueprint version, with or without it, so it narrows what one Blueprint's work may do and enforces nothing beyond that. devboxes refuses work that may only read a database without a read-only login with DATABASE_READ_ONLY_LOGIN_MISSING.

Use "kind": "secret" for tokens and passwords. devboxes stores secrets encrypted and never shows their values. Use "kind": "var" for values that anyone in the Organization may read.

The agent can read every unprotected secret of its Project, and the stand-ins of the others. A value that a task on your own machine writes to its tool cache, its checkpoint or the repository can reach later tasks, on devboxes machines too. Give a Project only the secrets its tasks need.

A change to a secret's value applies to tasks that start afterwards. Work keeps the hosts and swapIn its secrets had when it started, and the host, port and read-only setting of its databases: if you bind a secret to hosts, change its hosts or swapIn (reordering them changes nothing), mark it unprotected, change a database's host, port or read-only setting, remove the read-only login that work uses, or delete the secret while work waits or runs, devboxes refuses that work's next task, and you start the work again.

Terms of ServicePrivacy Policy

On this page