feat(agent-proxy): add policy-mode agent proxy for agent and user policy intersection - #361
feat(agent-proxy): add policy-mode agent proxy for agent and user policy intersection#361saifsmailbox98 wants to merge 3 commits into
Conversation
|
💬 Discussion in Slack: #pr-review-cli-361-feat-agent-proxy-add-policy-mode-agent-proxy-for-agent-and-use Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel. |
|
| Filename | Overview |
|---|---|
| packages/agentproxy/policy_server.go | Implements the policy proxy runtime, but exposes session authentication over a plaintext all-interface listener and omits response hop-by-hop filtering. |
| packages/agentproxy/policy_match.go | Implements policy parsing, matching, intersection evaluation, and agent-policy specificity; hostname authorization does not bind decisions to resolved destinations. |
| packages/agentproxy/policy_resolver.go | Adds bounded session caching, active-session refresh, revocation eviction, and batched activity reporting without an accepted finding. |
| packages/api/agent_policies.go | Adds typed request and response models for agent-proxy enrollment, heartbeat, session resolution, activity, and creation endpoints. |
| packages/cmd/agent_proxy_server.go | Registers and configures the policy-mode proxy command, enrollment-token persistence, logging, and startup options. |
| packages/agentproxy/policy_match_test.go | Covers pattern parsing, scheme and method restrictions, wildcard behavior, policy intersection, specificity, allowlisting, and proxy-auth parsing. |
Reviews (1): Last reviewed commit: "feat(agent-proxy): add policy-mode agent..." | Re-trigger Greptile
| return fmt.Errorf("failed to get an intermediate CA signed by Infisical: %w", err) | ||
| } | ||
|
|
||
| listener, err := net.Listen("tcp", fmt.Sprintf(":%d", opts.Port)) |
There was a problem hiding this comment.
Plaintext proxy authentication listener
When an agent connects across a shared or untrusted network, this all-interface plaintext listener exposes its reusable Basic or Bearer session token to on-path observers, allowing them to submit policy-authorized requests through the proxy.
How this was verified: The listener binds :<port> and serves a plain http.Server before sessionToken reads Proxy-Authorization.
Rule Used: TLS should be enabled by default for security best... (source)
Learned From
Infisical/cli#58
Knowledge Base Used: Agent Proxy Module
| decision = "brokered" | ||
| } | ||
|
|
||
| resp, err := ps.transport.RoundTrip(r) |
There was a problem hiding this comment.
Hostname-only destination authorization
If an allowed hostname resolves or rebinds to a loopback, link-local, or private address, the policy check still authorizes the textual hostname and the transport dials the internal destination, making the proxy an SSRF pivot and potentially sending an injected credential there.
How this was verified: The matching path checks only hostname strings, while RoundTrip performs DNS resolution without a guarded dialer or resolved-IP validation.
Context Used: Flag SSRF risks (source)
Knowledge Base Used: Agent Proxy Module
| for name, values := range resp.Header { | ||
| for _, value := range values { | ||
| w.Header().Add(name, value) | ||
| } | ||
| } |
There was a problem hiding this comment.
Hop-by-hop response headers forwarded
When an upstream response includes Connection or another hop-by-hop header, this loop forwards it unchanged instead of consuming it at the proxy boundary, causing downstream response-framing or connection-reuse failures.
Knowledge Base Used: Agent Proxy Module
| return fmt.Errorf("failed to get an intermediate CA signed by Infisical: %w", err) | ||
| } | ||
|
|
||
| listener, err := net.Listen("tcp", fmt.Sprintf(":%d", opts.Port)) |
There was a problem hiding this comment.
Medium: Session tokens exposed on the proxy connection
This listener accepts plain HTTP on every interface, and Proxy-Authorization is transmitted before the inner CONNECT tunnel is established. An observer between an agent and the proxy can capture and replay the session token to issue requests under that session; protect the ingress with TLS or mTLS rather than relying on the tunneled upstream TLS.
| func (p policyPattern) match(scheme, host, port, path, method string) (bool, matchDetail) { | ||
| detail := matchDetail{} | ||
|
|
||
| if p.scheme != "" && p.scheme != strings.ToLower(scheme) { |
There was a problem hiding this comment.
Medium: Scheme-less policies permit plaintext credential injection
An authenticated agent can select an http:// URL for any rule whose host pattern omits a scheme, causing the proxy to inject the policy's credential into a plaintext upstream request. Treat omitted schemes as HTTPS for credential-bearing policies, or require an explicit scheme and keep bare-host allowlist matching separate.
|
|
||
| resp, err := ps.transport.RoundTrip(r) | ||
| if err != nil { | ||
| ps.record(session, "error", r.Method, hostname, port, r.URL.Path, http.StatusBadGateway, policyName(matched), err.Error()) |
There was a problem hiding this comment.
Low: Substituted credentials are written to activity records
applyCredentials can replace a placeholder in r.URL.Path, after which this branch and the success branch record the mutated path; transport errors may also include the rewritten URL in err.Error(). An agent can therefore force a path-substitution secret into local logs and the activity API. Snapshot the original escaped path before applying credentials and record only that value, with a sanitized transport-error reason.
PR overviewThis pull request adds a policy-mode agent proxy that intersects agent and user policies when authorizing and forwarding requests. It also supports policy-based credential application and activity recording. Four security issues remain open, including a path canonicalization flaw that can bypass policy restrictions and send injected credentials to an unintended upstream path. Plaintext proxy ingress and scheme-less policy matching can also expose replayable session tokens or policy credentials, while substituted credentials may leak into activity records and errors. No reported issues have yet been addressed. Open issues (4)
Fixed/addressed: 0 · PR risk: 7/10 |
| return | ||
| } | ||
|
|
||
| matched, matchedUser := evaluate(session.agentPolicies, session.userPolicies, scheme, hostname, port, r.URL.Path, r.Method) |
There was a problem hiding this comment.
Medium: Uncanonicalized path bypasses policy rules
A request such as /allowed/../admin or its percent-encoded equivalent matches an /allowed/* rule, but an upstream that normalizes dot segments can process it as /admin with the injected credential. Reject non-canonical paths before policy evaluation, or canonicalize once and use the same canonical path for both authorization and forwarding.
Description 📣
Adds the runtime for agent policies: a long-standing agent proxy that brokers credentials on the intersection of an agent's policies and a user's.
infisical agent-proxy start --token=<enrollment-token>enrols against an agent proxy registered under Networking, swaps the one-time token for an access token (saved to~/.infisical/agent-proxy/access-tokenso a restart doesn't need a new one), signs a MITM intermediate off the org's agent proxy CA, and serves an HTTP forward proxy.An agent points
HTTP_PROXYat it with its session token as the proxy credential. Per request, the proxy resolves that session against Infisical and allows the request only when it matches at least one rule on an agent policy and at least one on a user policy. Neither side is a subset of the other, so this is a per-request check and not set arithmetic: a user rule ofGETnarrows an agent rule ofAnyon the same host. The matching agent policy is the most specific one, and that is what decides whose credentials get injected.policy_match.go— host patterns with scheme and HTTP method, and the intersection evaluator. A rule naminghttpsrefuses a plaintext request so a credential can't leave in the clear.CONNECTonly carries host and port, so the tunnel opens on a host match and the real decision happens inside it once the method and path are knownpolicy_resolver.go— per-session cache with a bounded size, refresh on a poll, and batched activity reporting. A hard auth failure drops the session and fails closed, so revocation needs no invalidation callpolicy_server.go— the proxy itself, reusing this package's existingca.goandrewrite.gorather than forking themCompanion PR: Infisical/infisical#7646
Type ✨
Tests 🛠️
Unit tests cover the evaluator, the pattern matcher and the proxy-auth parsing (
policy_match_test.go), including the case the whole model rests on: agent allowsAny, user allowsGET, so thePOSTis refused and theGETis brokered.Driven end to end against a local stack with one agent policy (Slack, any method) and one user policy (Slack, GET only):
Substitution verified on the wire rather than inferred: with no credential Slack answers
not_authed, and when the agent sends the placeholder it answersinvalid_auth, so the upstream received a token the agent never held.