Skip to content

fix(cloud): encrypt agent sandbox environment secrets at rest - #11402

Merged
lalalune merged 1 commit into
developfrom
nubs/sandbox-envvars-encrypt
Jul 2, 2026
Merged

lalalune merged 1 commit into
developfrom
nubs/sandbox-envvars-encrypt

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

Problem

agent_sandboxes.environment_vars is a plain jsonb column. PATCH /v1/eliza/agents/:id/environment — the only BYO-key surface today — persisted user provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, GitHub tokens, ...) verbatim into the row, so user secrets sat unencrypted at rest. Agent create and coding-container create take the same plaintext path. This is the environment_vars finding in #11332.

Verified before fixing: schema (packages/cloud/shared/src/db/schemas/agent-sandboxes.ts:124), the write path (updateAgentEnvironment → agentSandboxesRepository.update, no crypto anywhere), and the read path (provision() spreads rec.environment_vars straight into provider.create; buildRuntimeBootstrapAgent copies ANTHROPIC_API_KEY/OPENAI_API_KEY/... into runtime secrets). The new test fails against that write path (negative control below).

Fix — reuse the existing envelope crypto, no schema change, no migration

  • Encrypt on write (updateAgentEnvironment, createAgent, createCodingContainerAgent): values whose key looks secret-bearing (KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE) are encrypted with the existing FieldEncryptionService — AES-256-GCM, per-org DEK wrapped by SECRETS_MASTER_KEY, enc:v1: encoded strings — the same primitive already protecting tenant DB DSNs (user-database.ts). No new crypto was written.
  • Decrypt at materialization only: provision() (container env, also covers wake), fleet executeUpgrade/executeDowngrade, and the runtime bootstrap payload. The running agent sees identical real values.
  • Never encrypted: RESERVED_PLATFORM_ENV_KEYS + the legacy ELIZAOS_API_KEY alias — platform-minted tokens the control plane reads synchronously from the stored row (bridge auth headers, dedicated-agent proxy, pairing routes). These are blocked from the user PATCH surface by the existing reserved-key gate anyway.

Backward compatibility (prod table, conservative rollout)

  • Decrypt passes any non-enc:v1: value through untouched → legacy plaintext rows keep working with no forced backfill. They are opportunistically re-encrypted the next time the env is written.
  • Without SECRETS_MASTER_KEY configured, writes keep exact legacy plaintext behavior with a loud structured warning — local dev / self-hosters don't break. To activate, set the same key on the cloud API Worker and the provisioning daemon (the same deployment requirement tenant-DB DSN encryption already imposes).
  • Encryption failures when the key IS configured propagate (never silently persist plaintext); decrypt failures fail closed with the key name (never boot a container with ciphertext standing in for a secret).
  • A read-modify-write PATCH echoing stored ciphertext back is not double-encrypted (isEncrypted guard).
  • Zero DDL: values stay strings in the same jsonb column.

Tests

packages/cloud/shared/src/lib/services/agent-env-crypto.test.ts — real FieldEncryptionService crypto (only its org-key persistence is swapped for an in-memory store, the same db-helpers boundary the sibling suites already mock), repository write captured via spyOn so assertions run against the exact bytes that would land in Postgres:

  1. PATCHed provider key is enc:v1: ciphertext at rest, not the plaintext, and round-trips on materialization
  2. legacy plaintext row materializes unchanged (no backfill required)
  3. platform bridge tokens stay plaintext (control-plane sync reads intact)
  4. ciphertext echo is not double-encrypted
  5. no SECRETS_MASTER_KEY → legacy plaintext (graceful degradation)
  6. decrypt failure fails closed naming the key
  7. sensitivity classifier boundaries

Verification (real output)

Negative control — same test against the pre-fix write path (fix stashed):

137 |     // The at-rest bug: pre-fix these assertions fail — the row held the secret verbatim.
138 |     expect(stored.ANTHROPIC_API_KEY).not.toBe(SECRET);
error: expect(received).not.toBe(expected)
(fail) agent environment secrets are encrypted at rest (#11332) > a PATCHed provider key is ciphertext at rest — NOT the plaintext secret
 3 pass
 4 fail

With the fix (plus all neighboring sandbox suites):

bun test src/lib/services/agent-env-crypto.test.ts src/lib/services/eliza-sandbox.test.ts \
  src/lib/services/eliza-sandbox-create-idempotency.test.ts src/lib/services/managed-eliza-config.test.ts \
  src/lib/services/coding-containers.test.ts src/lib/services/eliza-sandbox-dedicated-bootstrap.test.ts \
  src/lib/services/eliza-sandbox-shared-billing.test.ts src/lib/services/managed-eliza-lean-chat-boot.integration.test.ts
 117 pass / 0 fail (445 expect() calls)

bun run --cwd packages/cloud/shared typecheck   # tsgo --noEmit → clean, exit 0

Biome on the touched files: no new findings (the pre-existing eliza-sandbox.ts lints are byte-identical on develop, line numbers shifted).

Evidence notes: UI screenshots/video N/A — backend-only, no UI surface changed. Live-LLM trajectory N/A — no agent/prompt/model behavior changed; the container env contract is proven identical by the round-trip + neighboring-suite tests.

Scope / follow-ups

Fixes the agent_sandboxes.environment_vars plaintext finding in #11332.

agent_sandboxes.environment_vars is plain jsonb — the PATCH
/v1/eliza/agents/:id/environment BYO-key path (and agent/coding-container
create) persisted user provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, ...)
in plaintext at rest.

encrypt secret-bearing values on write with the existing org-scoped envelope
crypto (FieldEncryptionService: AES-256-GCM, org DEK wrapped by
SECRETS_MASTER_KEY, enc:v1: strings — same primitive as tenant DB DSNs);
decrypt only where the env is materialized for the agent (provision, fleet
upgrade/rollback, runtime bootstrap), so containers still see real values.

backward compatible: legacy plaintext rows pass through decrypt untouched (no
backfill required); platform control-plane tokens (reserved keys +
ELIZAOS_API_KEY) stay plaintext for the sync bridge-auth/proxy/pairing reads;
without SECRETS_MASTER_KEY writes keep legacy plaintext behavior with a loud
warning. no schema change, no migration.

test proves the written secret is enc:v1: ciphertext at rest (fails on the
old write path), round-trips on materialization, and legacy plaintext rows
still read correctly.

fixes the environment_vars finding in #11332

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8109de7-5e93-4e34-9ff7-0b615bb1cf16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nubs/sandbox-envvars-encrypt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member

Maintainer review (security): reuses FieldEncryptionService (AES-256-GCM, per-org DEK under SECRETS_MASTER_KEY) — no new key material; decryption failures fail closed (agent errored, propagated) with the only fail-open being key-not-configured legacy passthrough (loud warn); all materialization paths decrypt, direct env reads touch only NEVER_ENCRYPT platform keys. No backfill job — untouched plaintext rows persist until next write, worth a follow-up sweep. 7/7. Merging.

@lalalune
lalalune merged commit eb11906 into develop Jul 2, 2026
36 of 44 checks passed
@lalalune
lalalune deleted the nubs/sandbox-envvars-encrypt branch July 2, 2026 09:34
lalalune pushed a commit that referenced this pull request Jul 2, 2026
…ey gate #11271 clobbered that neither #11403 nor #11422 covers (#11429)

#11271 clobbered 3 [cloud-security] money gates. Coverage on develop:
- #11227 pairing-token gate → my #11403 (merged; only this commit landed).
- #11240 eliza-app provisioning gate → shaw's #11422 (open).
- #11261 shared-turn refund guard → NOT restored by either. Fell through the
  cracks (my #11403's 2nd commit never merged; #11422's 5 files don't touch
  eliza-sandbox). Still a live hole: on the DEFAULT (shared) agent tier, a throw
  between reserveCredits and settle strands the hold (settleReservation(0) was
  back to 2 — the pre-existing degraded+billing-catch paths; my outer guard, the
  3rd, was gone).

eliza-sandbox.ts EVOLVED since (#11402 secret-encryption, #11375 quota), so this
re-applies ONLY the outer try/catch → settleReservation(0) guard onto the current
file — verified the #11402/#11375 changes are preserved (25 evolution markers
intact; no reverse-clobber). Test restored (was deleted by #11271).

typecheck + biome clean; shared-turn refund test 2 pass. This closes the last
open #11271 [cloud-security] money hole. Refs #11271 #11413 #11419. [cloud-security]
@claude

claude Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot added the Tests label Jul 2, 2026
@NubsCarson NubsCarson mentioned this pull request Jul 2, 2026
18 of 20 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants