Skip to content

fix(security): daemon hardening + auto/run endpoints for cloud agent#69

Merged
enzoevn merged 8 commits intomasterfrom
fix/daemon-security-hardening
Apr 17, 2026
Merged

fix(security): daemon hardening + auto/run endpoints for cloud agent#69
enzoevn merged 8 commits intomasterfrom
fix/daemon-security-hardening

Conversation

@enzoevn
Copy link
Copy Markdown
Contributor

@enzoevn enzoevn commented Apr 17, 2026

Summary

Three independent security hardening fixes for the daemon HTTP API, plus the /api/auto/run endpoints needed by the local_agent cloud-agent tool.

Security fixes

1. Require loopback peer for x-relay-internal bypass

Vulnerability: The require_auth middleware bypasses authentication when the request carries x-relay-internal: true. Previously this header was trusted unconditionally — so any LAN attacker (when the daemon bound to 0.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). The relay_bridge runs in-process and tunnels to http://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.1 by default

Vulnerability: Daemon bound to 0.0.0.0 unless LUKAN_LOCAL_ONLY=1 was 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.1 unless the user opts in to LAN via LUKAN_BIND_ALL=1, bindAll: true in config, or --bind-all CLI flag. Removed the now-redundant LUKAN_LOCAL_ONLY, localOnly, --local-only (unified to one knob).

Impact: Users who relied on LAN access must set LUKAN_BIND_ALL=1 or bindAll: 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 with auth_required: false (common) this enabled exfiltration/CSRF. Bind default (127.0.0.1) doesn't mitigate because the user's own browser IS 127.0.0.1 from 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 an Origin header 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_agent cloud tool)

  • POST /api/auto/run — starts an autonomous agent job (returns job_id immediately)
  • GET /api/auto/jobs/:id — polls status/output/error

Auth uses the existing x-relay-internal bypass (now loopback-verified).

Test plan

  • cargo fmt --all --check — passes
  • cargo clippy --all-targets -- -D warnings — no warnings
  • cargo check — builds
  • Relay bridge end-to-end test (file created through cloud agent → relay → daemon)
  • CORS enforcement test — 5 origins tested (evil.com, localhost:5173, tauri://localhost, attacker.io, 127.0.0.1:8080), whitelisted origins echoed, others receive no Access-Control-Allow-Origin
  • Manual verification on existing install that sets LUKAN_LOCAL_ONLY=1 or localOnly still works (they become no-ops since default is now local; user should see no behavior change)

enzoevn added 8 commits April 17, 2026 00:00
…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.
@enzoevn enzoevn merged commit ecef7d0 into master Apr 17, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant