Skip to content

feat(proxy): add destination-scoped credential brokering - #667

Merged
jdx merged 8 commits into
mainfrom
agent/fnox-proxy
Jul 29, 2026
Merged

feat(proxy): add destination-scoped credential brokering#667
jdx merged 8 commits into
mainfrom
agent/fnox-proxy

Conversation

@jdx

@jdx jdx commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • add [proxy] configuration with strict/permissive egress and exact domain, method, path, and header rules
  • add fnox proxy rules and fnox proxy run -- <command> with session placeholders and an ephemeral loopback TLS interception proxy
  • resolve policy-referenced secrets plus provider-auth dependencies, scrub credentials from the child environment, prevent redirects and ambient upstream proxies, and redact reflected secret values from responses
  • document the security model and the intentionally narrow first-pass protocol surface

Current limits

  • OS sandboxing is not included yet; the command warns that same-user processes can bypass proxy environment variables
  • intercepted traffic is HTTPS on port 443 using HTTP/1.1
  • request credential substitution is header-only; chunked request bodies and responses over 10 MiB are rejected

Testing

  • mise run test:cargo
  • mise run lint
  • SKIP_KEYCHAIN_TESTS=1 mise exec -- fnox exec -- bats test (707 tests; keychain integration skipped because this container has no Secret Service)
  • live curl smoke test against httpbin verified upstream substitution and downstream redaction

AI-assisted — Tool: Codex; model: OpenAI/GPT-5; version: unavailable.


Note

High Risk
Adds TLS interception and in-memory credential injection with a broad attack surface; misconfiguration or proxy bypass could leak secrets despite placeholders and scrubbing.

Overview
Introduces a credential proxy so agent workloads can call external APIs without receiving real secret values in their environment.

Configuration and CLI: New [proxy] block with strict/permissive egress, audit logging, and per-rule matching on domain, HTTP method, path globs, and header. fnox proxy rules prints effective policy; fnox proxy run -- <cmd> resolves rule secrets (plus provider auth dependencies), starts a session-local loopback CONNECT proxy with an ephemeral CA, and runs the child with placeholders and standard proxy/CA env vars. Overlay configs replace [proxy] wholesale so layered rules cannot silently broaden authority.

Proxy behavior: For matched HTTPS destinations on port 443, fnox terminates TLS, substitutes placeholders in allowed headers only when route rules match, forwards via a no-redirect upstream client, and redacts reflected secrets in responses. Unmatched destinations are blocked in strict mode or tunneled in permissive mode. The child environment is scrubbed of profile secrets and a large set of ambient provider credential env vars.

Docs and deps: User guide, CLI reference, schema, and usage spec are updated; rcgen, rustls-native-certs, tokio-rustls, and reqwest support the TLS stack.

Reviewed by Cursor Bugbot for commit 466cdcb. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Added a Credential Proxy that routes destination-scoped HTTPS requests while keeping real credentials out of child processes.
    • Introduced proxy CLI tooling, including proxy rules (inspect effective rules) and proxy run (run a command through the local proxy with placeholders).
    • Added new proxy configuration options (egress strict/permissive, auditing, and per-rule matching/substitution).
  • Documentation
    • Added a Credential Proxy guide, CLI reference pages, and configuration/reference updates, including schema support.
  • Bug Fixes / Tests
    • Improved configuration overlay behavior for proxy settings (overlay proxy replaces base proxy); expanded TOML/schema and merge test coverage.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds credential proxy configuration and schema support, a loopback HTTPS proxy with scoped credential substitution and redaction, fnox proxy rules/run commands, daemon resolution support, and related CLI and configuration documentation.

Changes

Credential Proxy

Layer / File(s) Summary
Proxy configuration contract
crates/fnox-core/src/config.rs, docs/public/schema.json
Defines proxy settings, egress modes, rule matching fields, replacement merge behavior, TOML parsing, and JSON schema entries.
Proxy planning and request handling
Cargo.toml, src/lib.rs, src/proxy.rs
Implements placeholder planning, rule validation, loopback TLS handling, HTTPS forwarding, request constraints, response redaction, and unit tests.
CLI execution and secret resolution
src/commands/mod.rs, src/commands/proxy.rs, src/daemon.rs, fnox.usage.kdl, docs/cli/commands.json
Adds proxy CLI dispatch, rules inspection, secret resolution with proxy purpose, child-process environment setup, proxy lifecycle management, and exit-status propagation.
Proxy command and configuration documentation
docs/cli/*, docs/guide/*, docs/reference/configuration.md, docs/.vitepress/config.mjs
Documents proxy commands, configuration, workflow, egress modes, limitations, auditing, and navigation links.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChildProcess
  participant RunningProxy
  participant ProxyPolicy
  participant UpstreamHTTPS
  ChildProcess->>RunningProxy: CONNECT destination:443
  RunningProxy->>ProxyPolicy: authorize request and inject placeholder
  ProxyPolicy-->>RunningProxy: approved request
  RunningProxy->>UpstreamHTTPS: forward HTTPS request
  UpstreamHTTPS-->>RunningProxy: response body and headers
  RunningProxy-->>ChildProcess: redacted response
Loading

Poem

A rabbit hops through tunnels bright,
With placeholders tucked out of sight.
Rules guard paths and headers true,
Secrets bloom only where they’re due.
TLS moonlight keeps the trail clear—
Proxy carrots, safely near.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding destination-scoped credential brokering via the new proxy feature.

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.

@socket-security

socket-security Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​rcgen@​0.14.89610093100100

View full report

Comment thread src/proxy.rs Outdated
Comment thread src/proxy.rs
Comment thread src/commands/proxy.rs
Comment thread src/proxy.rs Outdated
Comment thread src/proxy.rs Outdated
Comment thread src/commands/proxy.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Adds destination-scoped credential brokering through a local TLS interception proxy.

  • Introduces strict/permissive egress policies with domain, method, path, and header rules.
  • Adds proxy CLI commands, provider-auth dependency resolution, credential-environment scrubbing, bounded response handling, and response redaction.
  • Updates configuration schemas, generated CLI references, and proxy security documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; all three previously reported defects are corrected in the current code.

Important Files Changed

Filename Overview
src/proxy.rs Implements bounded TLS-intercepted request forwarding, policy authorization, credential substitution, and response redaction; the previously reported response-limit and sibling-rule defects are fixed.
src/commands/proxy.rs Adds proxy command orchestration, transitive provider-auth dependency collection, child-environment scrubbing, and proxy lifecycle management; the prior dependency-resolution omission is fixed.
crates/fnox-core/src/config.rs Adds the proxy policy schema and replaces proxy configuration atomically across layered config merges.
src/daemon.rs Extends daemon-backed resolution with the proxy purpose while preserving the existing request shape and resolution flow.
Cargo.toml Adds the TLS interception, native certificate, and HTTP client dependencies required by the proxy implementation.

Reviews (8): Last reviewed commit: "fix(proxy): return HTTP errors over TLS" | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
schema ▁▁▁▁▁▁▁█ 6,827,508 → 7,234,098 +5.96% ⚠️ 3.83 → 4.77ms +24.28%
usage ▁▁▁▁▁▁▁█ 10,702,736 → 10,953,471 +2.34% ⚠️ 4.34 → 5.13ms +18.21%

2 benchmark(s) above the 1% gate: schema +5.96%, usage +2.34%

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

466cdcb0b79a vs e45c9401987c · measured on the runner, not pushed to the history.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
crates/fnox-core/src/config.rs (1)

917-922: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a merge test for the proxy replace-as-unit behavior.

The rationale for replacing proxy wholesale (rather than merging field-by-field) is sound, but unlike the analogous mcp.secrets replace-as-unit logic — which has both mcp_secrets_overlay_replaces_base_not_appends and mcp_secrets_overlay_without_secrets_preserves_base tests — there's no test verifying that an overlay's [proxy] section fully replaces (not merges with) a base config's rules, nor one verifying the base proxy is preserved when the overlay omits [proxy] entirely. Given this is a security-relevant merge path, it's worth locking in with a regression test.

♻️ Suggested test (mirrors the existing `mcp_secrets_overlay_*` tests)
#[test]
fn proxy_overlay_replaces_base_rules_not_appends() {
    let base = Config {
        proxy: Some(ProxyConfig {
            rules: vec![ProxyRule {
                secret: "A".into(),
                domain: "a.example.com".into(),
                env: None,
                header: default_proxy_header(),
                methods: vec![],
                paths: vec![],
                placeholder: None,
            }],
            ..ProxyConfig::default()
        }),
        ..Config::new()
    };
    let overlay = Config {
        proxy: Some(ProxyConfig {
            rules: vec![ProxyRule {
                secret: "B".into(),
                domain: "b.example.com".into(),
                env: None,
                header: default_proxy_header(),
                methods: vec![],
                paths: vec![],
                placeholder: None,
            }],
            ..ProxyConfig::default()
        }),
        ..Config::new()
    };
    let merged = Config::merge_configs(base, overlay).unwrap();
    let rules = merged.proxy.unwrap().rules;
    assert_eq!(rules.len(), 1, "overlay must replace, not append, base rules");
    assert_eq!(rules[0].secret, "B");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fnox-core/src/config.rs` around lines 917 - 922, Add merge regression
tests near the existing mcp_secrets_overlay_* tests for Config::merge_configs:
verify an overlay with proxy rules replaces the base rules entirely, and verify
an overlay that omits proxy preserves the base proxy. Reuse the existing
ProxyConfig, ProxyRule, default_proxy_header, and Config::new patterns,
asserting both rule count/content and preservation behavior.
src/proxy.rs (2)

550-554: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Build the upstream reqwest::Client once, not per request.

A fresh client per proxied request rebuilds the TLS config and discards the connection pool, adding a full TCP+TLS handshake to every call — noticeable for agent workloads that issue many API requests. Construct it once (e.g. in RunningProxy::start or a OnceCell) and share it via the policy/connection context. It also gives you a natural place to set a request timeout, which is currently absent on this outbound call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.rs` around lines 550 - 554, Move upstream reqwest::Client
construction out of the per-request path around the proxy request handler and
initialize it once during RunningProxy::start or equivalent shared context
setup. Pass and reuse that client through the policy/connection context for all
proxied requests, preserving the existing HTTP/redirect configuration, and
configure an appropriate outbound request timeout when constructing it.

823-905: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a negative test for path scoping.

The suite covers wrong method and wrong header but never asserts that a placeholder is rejected for a path outside paths. That is the case most likely to regress (see the literal_separator note on compile_paths) — e.g. authorize("api.github.com", "GET", "/repos/other/private", ...) should error, and with literal_separator(true) a /repos/example/* rule should not match /repos/example/a/b.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.rs` around lines 823 - 905, Add a negative path-scoping test
alongside rejects_placeholder_on_wrong_method and
rejects_placeholder_in_another_header, using the existing policy and
Authorization placeholder to assert authorize rejects an allowed method on an
outside path such as /repos/other/private. Also cover that a wildcard path rule
with literal_separator(true) does not match a nested path like
/repos/example/a/b, reusing the relevant policy/config symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/guide/proxy.md`:
- Around line 54-55: Update the proxy description near the startup flow to call
it a loopback-only HTTPS (CONNECT) proxy rather than an HTTP/HTTPS proxy, and
add a Current Limits bullet stating that plain http:// proxy requests are
rejected; keep the existing CONNECT behavior and terminology consistent with
handle_connection.

In `@src/commands/proxy.rs`:
- Around line 181-190: Ensure the proxy cleanup always executes when
command.status() fails, including spawn errors: capture the result of
command.status().await with its existing FnoxError::CommandExecutionFailed
mapping, call running.shutdown().await before propagating the result, then
return the command status outcome.
- Around line 134-150: Extend the credential scrub list in the proxy command’s
environment-removal loop to include DOPPLER_TOKEN, FNOX_DOPPLER_TOKEN,
FOKS_BOT_TOKEN, and FNOX_FOKS_BOT_TOKEN; also add
PROTON_PASS_PERSONAL_ACCESS_TOKEN and FNOX_PROTON_PASS_PERSONAL_ACCESS_TOKEN so
these ambient credentials are removed before execution.

In `@src/proxy.rs`:
- Around line 695-714: The proxy’s header and body size checks occur only after
full buffering, so oversized inputs can exceed the intended memory bounds. In
src/proxy.rs lines 695-714, update read_line_limited to enforce remaining during
reading via a bounded or incremental read, preserving the existing errors and
return behavior. In src/proxy.rs lines 583-591, replace response.bytes() with
response.chunk() streaming, track accumulated bytes, and stop immediately once
MAX_BODY_BYTES is exceeded.
- Around line 386-408: Update the accept loop in the spawned task around
listener.accept so transient accept errors are logged and the loop continues
instead of breaking. Preserve shutdown handling and only terminate the loop when
the shutdown receiver completes; include the accept error in the diagnostic log.
- Around line 512-520: Update proxy_http_request and the surrounding
handle_connection TLS error paths to flush or shut down the AsyncWrite stream
before returning. After successful write_error or response-body writes, await
AsyncWriteExt::shutdown(stream), and ensure TLS-path errors also flush/shut down
before the connection is dropped so all buffered response data is sent.
- Around line 278-297: Update compile_paths to construct each glob with
GlobBuilder configured with literal_separator(true) before building it, so
wildcard patterns do not match across directory boundaries. Preserve the
existing validation, error propagation, and GlobSetBuilder flow; do not enable
recursive matching unless explicitly documented and tested.

---

Nitpick comments:
In `@crates/fnox-core/src/config.rs`:
- Around line 917-922: Add merge regression tests near the existing
mcp_secrets_overlay_* tests for Config::merge_configs: verify an overlay with
proxy rules replaces the base rules entirely, and verify an overlay that omits
proxy preserves the base proxy. Reuse the existing ProxyConfig, ProxyRule,
default_proxy_header, and Config::new patterns, asserting both rule
count/content and preservation behavior.

In `@src/proxy.rs`:
- Around line 550-554: Move upstream reqwest::Client construction out of the
per-request path around the proxy request handler and initialize it once during
RunningProxy::start or equivalent shared context setup. Pass and reuse that
client through the policy/connection context for all proxied requests,
preserving the existing HTTP/redirect configuration, and configure an
appropriate outbound request timeout when constructing it.
- Around line 823-905: Add a negative path-scoping test alongside
rejects_placeholder_on_wrong_method and rejects_placeholder_in_another_header,
using the existing policy and Authorization placeholder to assert authorize
rejects an allowed method on an outside path such as /repos/other/private. Also
cover that a wildcard path rule with literal_separator(true) does not match a
nested path like /repos/example/a/b, reusing the relevant policy/config symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 73d6adc5-95f9-4ce0-8d53-82379f03355b

📥 Commits

Reviewing files that changed from the base of the PR and between e45c940 and 3c1e64e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • Cargo.toml
  • crates/fnox-core/src/config.rs
  • docs/.vitepress/config.mjs
  • docs/cli/commands.json
  • docs/cli/index.md
  • docs/cli/proxy.md
  • docs/cli/proxy/rules.md
  • docs/cli/proxy/run.md
  • docs/guide/how-it-works.md
  • docs/guide/proxy.md
  • docs/public/schema.json
  • docs/reference/configuration.md
  • fnox.usage.kdl
  • src/commands/mod.rs
  • src/commands/proxy.rs
  • src/daemon.rs
  • src/lib.rs
  • src/proxy.rs

Comment thread docs/guide/proxy.md Outdated
Comment thread src/commands/proxy.rs Outdated
Comment thread src/commands/proxy.rs Outdated
Comment thread src/proxy.rs
Comment thread src/proxy.rs
Comment thread src/proxy.rs
Comment thread src/proxy.rs
Comment thread src/proxy.rs Outdated
Comment thread src/proxy.rs
Comment thread crates/fnox-core/src/config.rs
Comment thread src/commands/proxy.rs
Comment thread src/commands/proxy.rs
"DENO_CERT",
] {
command.env(key, &ca_path);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Permissive egress TLS trust broken

Medium Severity

proxy run sets SSL_CERT_FILE, CURL_CA_BUNDLE, REQUESTS_CA_BUNDLE, and similar variables to only the ephemeral fnox CA. That trust store works for MITM’d rule domains, but permissive egress tunnels other hosts with end-to-end TLS, so clients need public CAs and commonly fail verification or refuse connections.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 56e5a81. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f8ccf05. Configure here.

Comment thread src/proxy.rs
@jdx
jdx merged commit 694f89a into main Jul 29, 2026
17 of 18 checks passed
@jdx
jdx deleted the agent/fnox-proxy branch July 29, 2026 17:34
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