agent-sandbox: validate required secrets + existing-secret DSN ref#308
agent-sandbox: validate required secrets + existing-secret DSN ref#308JatinNanda wants to merge 1 commit into
Conversation
1c15011 to
b5817c9
Compare
The agent-sandbox secret story was under-validated and rigid:
- An empty postgres.url silently base64-encoded to nothing
({{ $as.postgres.url | default "" | b64enc }}), so a misconfigured deploy
installed cleanly and the controller/proxy crash-looped at runtime.
- jwtPublicKey / jwtPrivateKey (required for the controller/proxy to boot and
for the backend to sign sandbox tokens) had no guard when absent.
- Postgres could only be supplied as a plaintext DSN; operators could not reuse
an existing password-only secret (e.g. the backend's Postgres password).
The agent-sandbox app consumes a single connection string (no split-field code
path), so the chart now offers four ways to supply it, validated at install:
1. postgres.url -- plaintext DSN.
2. postgres.host (+ user + database) -- the chart assembles
postgres://user@host:port/database and supplies the password out-of-band
via the PGPASSWORD env var, from postgres.password or
postgres.passwordSecretName. node-postgres reads PGPASSWORD when the DSN
omits the password (verified: pg-connection-string yields an empty password
for user@host, and pg's val() uses `||` so it falls through to PGPASSWORD),
so the password is never parsed as a URL and needs no escaping -- any
characters are safe. This is what lets a password-only secret be reused.
3. postgres.urlSecretName -- existing secret holding the full DSN.
4. externalSecret.name -- catch-all secret, postgres-url key.
Changes:
- Add retool.agentSandbox.validateSecrets: fail at install time when an enabled
workload is missing a Postgres source, user/database for the assemble path, a
JWT public key, or a JWT private key.
- Promote the controller/proxy URL block to retool.agentSandbox.postgresUrlEnv,
which implements the resolution order above (emitting PGPASSWORD for the
assemble path).
- Only write postgres-url into the chart-managed secret when a plaintext url is
set, so empty keys are never emitted.
- Document the canonical shapes and the password-secret reuse path.
Audit: mcp already fails on its missing required secret; js_executor has no
secrets, so neither needs changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b5817c9 to
14df4f9
Compare
|
| Filename | Overview |
|---|---|
| charts/retool/templates/_helpers.tpl | Adds validateSecrets and postgresUrlEnv helpers; the assembled-URL path does not percent-encode user/database, which breaks Azure-style @-containing usernames at runtime. |
| charts/retool/templates/deployment_agent_sandbox.yaml | Calls validateSecrets at the top of the enabled block; replaces the two identical inline URL env blocks with the shared helper; guards postgres-url in the chart-managed Secret so empty DSNs are never written. |
| charts/retool/values.yaml | Adds the four Postgres sourcing options (url, host-assembly, urlSecretName, externalSecret) and marks JWT keys as required; mirrors changes in root values.yaml; well-documented. |
| values.yaml | Identical update to charts/retool/values.yaml kept in sync; no independent logic. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[helm install / upgrade
agentSandbox.enabled=true] --> B[validateSecrets]
B -->|postgres.url OR host OR urlSecretName OR externalSecret| C{Postgres source?}
B -->|none set| FAIL1[❌ fail: Postgres required]
C -->|host set| D{user + database set?}
D -->|no| FAIL2[❌ fail: user+database required]
D -->|yes| E[postgresUrlEnv: assemble URL
PGPASSWORD from password/passwordSecretName]
C -->|url set| F[postgresUrlEnv: value: url]
C -->|urlSecretName set| G[postgresUrlEnv: secretKeyRef urlSecretName/urlSecretKey]
C -->|externalSecret only| H[postgresUrlEnv: secretKeyRef externalSecret.name]
B -->|jwtPublicKey OR externalSecret| I{JWT public key?}
B -->|neither set| FAIL3[❌ fail: jwtPublicKey required]
I -->|jwtPrivateKey OR externalSecret| J{JWT private key?}
I -->|neither set| FAIL4[❌ fail: jwtPrivateKey required]
J --> K[✅ Render controller + proxy Deployments]
E --> K
F --> K
G --> K
H --> K
Reviews (1): Last reviewed commit: "agent-sandbox: validate required secrets..." | Re-trigger Greptile
| {{- else if $pg.host }} | ||
| {{- $port := $pg.port | default 5432 -}} | ||
| {{- if $pg.passwordSecretName }} | ||
| - name: PGPASSWORD | ||
| valueFrom: | ||
| secretKeyRef: | ||
| name: {{ $pg.passwordSecretName }} | ||
| key: {{ $pg.passwordSecretKey | default "password" }} | ||
| {{- else if $pg.password }} | ||
| - name: PGPASSWORD | ||
| value: {{ $pg.password | quote }} | ||
| {{- end }} | ||
| - name: AGENT_SANDBOX_POSTGRES_URL | ||
| value: {{ printf "postgres://%s@%s:%v/%s" $pg.user $pg.host $port $pg.database | quote }} |
There was a problem hiding this comment.
No install-time guard when
postgres.host is set with no password source
When postgres.host is set and neither postgres.password nor postgres.passwordSecretName is provided, the template emits no PGPASSWORD entry and the assembled URL carries no credentials. For most production Postgres deployments this results in a rejected connection, but the failure surfaces at pod startup rather than at helm install time.
validateSecrets already enforces user and database on the host path; extending it to check (not (or $pg.password $pg.passwordSecretName)) and {{- fail }} when true (or document it as intentional for trust-auth) would make the behaviour explicit.
| value: {{ $pg.password | quote }} | ||
| {{- end }} | ||
| - name: AGENT_SANDBOX_POSTGRES_URL | ||
| value: {{ printf "postgres://%s@%s:%v/%s" $pg.user $pg.host $port $pg.database | quote }} |
There was a problem hiding this comment.
Unencoded
@ in assembled URL breaks cloud-managed Postgres usernames
The printf call embeds $pg.user verbatim in the RFC 3986 userinfo segment. Azure Database for PostgreSQL and similar managed services require the username to include an @hostname suffix. A username containing a literal @ produces two @ characters in the connection string; pg-connection-string treats the second one as the userinfo-host delimiter, so both the username and the host are parsed incorrectly and the connection fails at runtime.
The PR correctly routes the password through PGPASSWORD to avoid escaping, but user and database are still URL-embedded and need percent-encoding for such values. Consider applying Helm's urlquery function to $pg.user and $pg.database, or add a {{- fail }} guard that rejects @, :, and / in those fields on the host-assembly path.
Part of the r2-cleanup integration branch (merges into
r2-cleanup, thenr2via #303).Problems
{{ $as.postgres.url | default "" | b64enc }}base64-encodes an empty value, so a deploy missing the DSN installs cleanly and the controller/proxy crash-loop at runtime.jwtPublicKey(controller + proxyprocess.exit(1)without it) andjwtPrivateKey(the backend signs sandbox tokens with it) had no install-time check.App constraint & approach
The agent-sandbox app passes
AGENT_SANDBOX_POSTGRES_URLstraight to Knex/pgas a connection string — no split host/user/password code path. To still reuse a password-only secret without embedding the password in a URL (and the escaping headaches that brings), the chart builds the DSN without the password and supplies it via thePGPASSWORDenv var. node-postgres readsPGPASSWORDwhen the connection string omits the password — verified against the shipped stack:pg-connection-string2.4.0 yields an empty password foruser@host, andpg8.11.3'sval()uses||so it falls through toprocess.env.PGPASSWORD. The password is never parsed as part of a URL, so any characters are safe — no escaping required.Postgres DSN sourcing (resolution order, validated at install)
postgres.url— plaintext DSN.postgres.host(+user+database) — chart assemblespostgres://user@host:port/database; password supplied viaPGPASSWORDfrompostgres.password(plaintext) orpostgres.passwordSecretName/passwordSecretKey(existing secret). This is the reuse-an-existing-password-secret path; reserved characters need no escaping.postgres.urlSecretName/urlSecretKey— existing secret holding the full DSN.externalSecret.name— catch-all secret,postgres-urlkey.Other changes
retool.agentSandbox.validateSecrets—failwhen an enabled workload lacks a Postgres source, lacksuser/databaseon the assemble path, or lacks a JWT public/private key.retool.agentSandbox.postgresUrlEnv(de-dupes two identical blocks; emitsPGPASSWORDfor the assemble path).postgres-urlonly when a plaintext url is set.values.yaml.Audit of the other r2 workloads
fails whenoauthIntrospectionAuthTokenis absent. No change.Verification (
helm template, agentSandbox.enabled=true)postgres.url+ jwt pub/privpasswordSecretNamePGPASSWORDfrom secret; URLpostgres://retool@pg.internal:5432/retool(no password)password=p@ss/w:rdPGPASSWORD: "p@ss/w:rd"verbatim; URL has no password (special chars unescaped)user/databaseurlSecretName/urlSecretKeysecretKeyRef; chart secret omits emptypostgres-urlexternalSecret.namepostgres-urljwtPublicKey/ nojwtPrivateKeyagentSandbox.enabled=falseBoth values.yaml copies stay in sync;
helm lintpasses; no CI fixture enables agentSandbox.🤖 Generated with Claude Code