fix(security): route .env writes through host agent, restore :ro mount#908
Merged
Lightheartdevs merged 2 commits intoLight-Heart-Labs:mainfrom Apr 18, 2026
Conversation
This was referenced Apr 11, 2026
f93ebab to
9bb03bc
Compare
This was referenced Apr 16, 2026
Lightheartdevs
previously approved these changes
Apr 18, 2026
Collaborator
Lightheartdevs
left a comment
There was a problem hiding this comment.
Solid security improvement. The architecture is right: container .env mount goes :ro, dashboard-api delegates writes to the host agent via authenticated /v1/env/update, host agent performs allowlist validation + atomic write + backup.
Reviewed the host agent endpoint:
- MAX_ENV_BODY=65536 — correctly bypasses the default 16 KB
read_json_bodycap, which would truncate a real.env(example alone is ~11 KB). - Key-name validation via
^[A-Za-z_][A-Za-z0-9_]*$regex — prevents injection via weird key characters. - Schema allowlist — warns but accepts unknown keys with a logged note. Pragmatic: extensions and GPU pinning write keys not in the core schema. I'd consider making this configurable (strict mode for prod, lenient for dev), but the current compromise is defensible.
- Control-char rejection in values — good defense in depth; catches what
splitlines()doesn't. _model_activate_lockcoordination — correct. Model activation also writes.env; without this lock, concurrent writes would clobber each other.- Atomic write via
os.replace— correct. - Backup path under
data/config-backups/— good, stays on the data volume.
Dashboard-api side:
- Correctly removes now-unused
_write_text_atomicand_resolve_env_backup_roothelpers. - Handles
URLError/HTTPErrorwith 503 (agent unreachable) — user-friendly. - Async wrapping of the agent call keeps the event loop responsive.
Nitpick: the \x00 test in _handle_env_update tests looks reachable via Body-escape — may want to add a len(value) < 65536 per-key cap as belt-and-suspenders.
Cross-PR note: overlaps with draft #975 which also touches .env handling and the host agent binding. This PR's scope is tighter and should land first. Ship.
Commit c7ffea3 (settings environment editor) changed the dashboard-api .env bind mount from :ro to writable so the new PUT /api/settings/env endpoint could call _write_text_atomic() from inside the container. This introduces a container-escape risk: any RCE in the dashboard-api container (dependency CVE, SSRF chain, malicious extension manifest) can now overwrite .env at the filesystem level, bypassing the API key check. An attacker can plant a known DREAM_AGENT_KEY and reach the host agent for full container lifecycle control. Restore the mount to :ro and route env writes through a new host-agent endpoint POST /v1/env/update, mirroring the pattern _do_model_activate already uses for model-switch .env updates. The dashboard-api keeps its validation (_prepare_env_save) for UX and passes the prepared raw_text to the host agent via _call_agent_env_update. The host agent re-validates every key against .env.schema.json (defense in depth), rejects values with embedded control characters, acquires _model_activate_lock non-blocking to avoid racing model switches, backs up the previous .env under DATA_DIR/config-backups/, and writes atomically via tempfile + os.replace. Includes 9 unit tests covering happy path, 413 oversize body, 400 unknown key / malformed line / control-char value, 409 lock contention, and 500 missing schema. Deletes three helpers (_write_text_atomic, _resolve_env_backup_root, _display_backup_path) that are no longer referenced after the refactor. Known residual: an attacker with an already-valid DREAM_AGENT_KEY can still set any schema-allowed key. This is the intended threat model for the endpoint; the raw_text API is a tradeoff vs. structured key-value JSON and is worth revisiting in future hardening. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…trip Two fixes for the .env write path: 1. Host agent: change strict reject (400) to warn-and-accept for keys not in .env.schema.json. Extension install hooks and GPU pinning write keys that are absent from the core schema (e.g. JWT_SECRET from LibreChat, COMFYUI_GPU_UUID from the installer). Rejecting them made the dashboard Settings save unusable after any extension install. 2. Dashboard API: _render_env_from_values no longer drops extras with empty values. The filter `value != ""` silently discarded keys like LLAMA_ARG_TENSOR_SPLIT="" on round-trip, even though empty values are semantically meaningful (disabling a setting). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9bb03bc to
36f9412
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Restores the
.envmount for thedashboard-apicontainer to:roand routes env-editor writes through a new host-agent endpointPOST /v1/env/update, mirroring the existing host-agent-owned write path used by model activation.Why (security regression)
Commit
c7ffea39(settings environment editor) changed the dashboard-api.envmount from:roto writable so the newPUT /api/settings/envendpoint could write.envwith_write_text_atomic(env_path, raw_text)from inside the container.The endpoint itself is API-key-gated, but the filesystem-level
:rwmount is a container escape risk: any RCE in the dashboard-api container (dependency CVE, SSRF chain, malicious extension manifest during install) now has write access to.envat the filesystem level, bypassing the API key check entirely.Before this regression, container RCE meant credential read. After the regression, it meant credential overwrite — an attacker could plant a known
DREAM_AGENT_KEY, resetDASHBOARD_API_KEY, overwrite cloud API keys (OPENAI_API_KEY,ANTHROPIC_API_KEY), then reach the host agent on port 7710 for full container lifecycle control.How
- ./.env:/dream-server/.env:ro.POST /v1/env/updateendpoint +_handle_env_updatemethod that:check_authMAX_ENV_BODY = 65536(defaultMAX_BODY = 16384truncates real.envfiles which routinely exceed 16KB)raw_textstring.env.schema.jsonfromINSTALL_DIR, usespropertieskeys as the allowlist_model_activate_locknon-blocking (409 on contention) to avoid racing concurrent_do_model_activatecalls, which also read-modify-write.env.envunderDATA_DIR/config-backups/.env.backup.<timestamp>os.replace_call_agent_env_update(raw_text)helper mirroring_call_agent_core_recreate.api_settings_env_savestill runs the existing_prepare_env_savevalidation for UX, then delegates the write to the host agent via the helper, handlingHTTPError/URLError/OSErrorinto 503/500.main.py:_write_text_atomic,_resolve_env_backup_root,_display_backup_path.test_settings_env.pyfixturesettings_env_fixturewith a monkeypatch for_call_agent_env_updatethat fakes the agent response (fake also writes the target file so existing read-back tests pass).TestHandleEnvUpdatetotest_host_agent.py— 9 tests covering happy path, 413 oversize, 400 unknown key / malformed line / control char (\x00and\x1b), tab allowed, 409 lock contention, 500 missing schema.Testing
Platform Impact
Known residual risk
An attacker with a valid `DREAM_AGENT_KEY` can still set any schema-allowed key — this is the intended threat model for the endpoint. The raw-text-blob API (versus structured key-value JSON) is an architectural tradeoff that is worth revisiting in a future hardening pass, but is out of scope for this security regression fix.
Multi-line value injection via JSON-embedded `\n`: `splitlines()` will decompose the attacker's raw_text into multiple lines and each line re-enters full key + control-char validation. A smuggled line whose key is ALSO in `.env.schema.json` would be written — but such a write requires an already-authenticated caller, so the attacker has already passed the API-key gate and could use the legitimate API to set the same keys. This is the accepted tradeoff of the raw-text API.
Follow-up items (deferred, not blocking)