fix(security): daemon hardening + auto/run endpoints for cloud agent#69
Merged
fix(security): daemon hardening + auto/run endpoints for cloud agent#69
Conversation
…ints
Exposes autonomous agent execution via the relay tunnel.
Cloud agents can trigger lukan auto via local_agent tool:
- POST /api/auto/run { prompt, max_turns } → { job_id } (returns immediately)
- GET /api/auto/jobs/:id → { status, output, summary, error }
Auth is handled by the existing x-relay-internal bypass in require_auth.
… CORS whitelist Three independent hardening fixes for the daemon HTTP API that exposes file I/O, command execution, and autonomous agent spawning: 1. Require loopback peer for x-relay-internal bypass The relay_bridge runs in-process and tunnels REST requests to 127.0.0.1, so legitimate relay traffic always has a loopback peer. Previously the bypass trusted the header alone, allowing any LAN attacker (when the daemon bound to 0.0.0.0) to send `x-relay-internal: true` and skip authentication entirely even with a password configured. 2. Bind to 127.0.0.1 by default (LAN access is opt-in) The daemon previously bound to 0.0.0.0 unless LUKAN_LOCAL_ONLY=1 was set. Inverted the default: local-only is now default, LAN exposure requires explicit opt-in via LUKAN_BIND_ALL=1 env var, bindAll: true in config.json, or the --bind-all CLI flag. Removed the now-redundant LUKAN_LOCAL_ONLY env var, localOnly config field, and --local-only flag. 3. Restrict CORS to loopback + tauri origins Replaced `allow_origin(Any)` with a predicate that only allows http(s)://localhost:*, http(s)://127.0.0.1:*, tauri://*, and http://tauri.localhost[:port]. Prevents cross-origin requests from malicious websites the user visits (the browser IS 127.0.0.1 from the daemon's POV, so the new bind default alone doesn't stop this vector). Server-to-server callers (relay bridge) don't send an Origin header so the relay flow is unaffected.
Rust 1.95's clippy enforces `collapsible_match` more strictly, catching 11 pre-existing patterns where an `if` inside a match arm can be folded into the match's arm pattern via a guard. CI on master was failing as of Rust 1.95 even though the code compiled on older toolchains. Affected files: - crates/lukan-browser/src/ax_tree.rs (4 match arms) - crates/lukan-providers/src/gemini.rs (3 match arms) - crates/lukan-providers/src/openai_codex.rs (2 match arms) - crates/lukan-providers/src/openai_compat.rs (2 match arms) Purely mechanical refactor — same runtime behavior, no logic change.
Rust 1.95's clippy flags `sort_by(|a, b| b.cmp(&a))` patterns as `clippy::unnecessary_sort_by`. Replace with `sort_by_key(|x| std::cmp::Reverse(x))` for the same descending-order behavior. - crates/lukan-tools/src/glob_tool.rs: sort paths by mtime desc - crates/lukan-tools/src/remember.rs: sort by keyword-match score desc
After upgrading local Rust toolchain to 1.95 (matching CI), clippy flagged 15 more patterns across lukan-agent and lukan-tui: - collapsible_match in 14 match arms (13 auto-fixed via cargo clippy --fix) - unnecessary_sort_by in 1 descending sort (manual Reverse) - 1 manual collapsible_match in executor.rs tool-result arm Verified by running `cargo clippy --all-targets -- -D warnings` locally with rustc 1.95.0 — zero warnings on the whole workspace.
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.
Summary
Three independent security hardening fixes for the daemon HTTP API, plus the
/api/auto/runendpoints needed by thelocal_agentcloud-agent tool.Security fixes
1. Require loopback peer for
x-relay-internalbypassVulnerability: The
require_authmiddleware bypasses authentication when the request carriesx-relay-internal: true. Previously this header was trusted unconditionally — so any LAN attacker (when the daemon bound to0.0.0.0) could forge the header to skip authentication, even with a password configured.Fix: Bypass now requires the TCP peer to be loopback (
127.0.0.1/::1). Therelay_bridgeruns in-process and tunnels tohttp://127.0.0.1:{port}so legitimate traffic always passes. Attacker traffic from LAN has a non-loopback peer →401.Impact: Zero — relay bridge path is unaffected.
2. Bind to
127.0.0.1by defaultVulnerability: Daemon bound to
0.0.0.0unlessLUKAN_LOCAL_ONLY=1was set. Users running with password-required auth were still exposed to the bypass above if they didn't opt in to local-only binding.Fix: Inverted the default — daemon binds to
127.0.0.1unless the user opts in to LAN viaLUKAN_BIND_ALL=1,bindAll: truein config, or--bind-allCLI flag. Removed the now-redundantLUKAN_LOCAL_ONLY,localOnly,--local-only(unified to one knob).Impact: Users who relied on LAN access must set
LUKAN_BIND_ALL=1orbindAll: true. Desktop client, browser UI, plugin UIs, and relay flow unaffected.3. Restrict CORS to loopback + Tauri origins
Vulnerability:
CorsLayer::new().allow_origin(Any)allowed any website the user visits to make cross-origin requests to the daemon. Combined withauth_required: false(common) this enabled exfiltration/CSRF. Bind default (127.0.0.1) doesn't mitigate because the user's own browser IS127.0.0.1from the daemon's point of view.Fix: Whitelist only
http(s)://localhost:*,http(s)://127.0.0.1:*,tauri://*,http://tauri.localhost[:port]. Server-to-server callers (relay bridge, internal tooling) don't send anOriginheader so CORS middleware doesn't activate for them.Impact: Zero for legitimate flows. Blocks external-origin browser attacks. Validated end-to-end against staging relay with multiple origins.
Autonomous agent endpoints (needed by
local_agentcloud tool)POST /api/auto/run— starts an autonomous agent job (returnsjob_idimmediately)GET /api/auto/jobs/:id— polls status/output/errorAuth uses the existing
x-relay-internalbypass (now loopback-verified).Test plan
cargo fmt --all --check— passescargo clippy --all-targets -- -D warnings— no warningscargo check— buildsAccess-Control-Allow-OriginLUKAN_LOCAL_ONLY=1orlocalOnlystill works (they become no-ops since default is now local; user should see no behavior change)