Changelog

View on GitHub →

New: GCP Service Account Token Injection

iron-proxy can now authenticate outbound requests to Google Cloud APIs on behalf of your agents. The new gcp_auth transform signs a JWT with a service account keyfile, exchanges it for a short-lived OAuth2 access token, and injects Authorization: Bearer onto matching requests, with no credentials required in agent code.

The keyfile can be loaded from disk or from any registered secret source (env, aws_sm, aws_ssm, 1password, 1password_connect) using a nested keyfile: block. Tokens are cached and refreshed automatically via golang.org/x/oauth2/google. By default, a token-mint failure causes the request to be rejected with 403; set fallback: skip to pass through unauthenticated instead.

transforms:
  - name: gcp_auth
    config:
      keyfile_path: "/etc/iron-proxy/gcp-sa.json"
      scopes:
        - "https://www.googleapis.com/auth/cloud-platform"
      rules:
        - host: "*.googleapis.com"
 
# Or load the keyfile from a secret source:
  - name: gcp_auth
    config:
      keyfile:
        type: 1password_connect
        secret_ref: "op://Engineering/GCP-SA/credential"
      scopes:
        - "https://www.googleapis.com/auth/cloud-platform"
      rules:
        - host: "*.googleapis.com"
View on GitHub →

New: 1Password Connect Secret Source

iron-proxy can now resolve op://vault/item/[section/]field references from a self-hosted 1Password Connect server — the same URI format already supported for the CLI-backed 1password source. Host and token are read from OP_CONNECT_HOST and OP_CONNECT_TOKEN by default, and can be overridden per-source via host_env / token_env.

secrets:
  sources:
    - type: 1password_connect
      host_env: OP_CONNECT_HOST       # default
      token_env: OP_CONNECT_TOKEN     # default

New: Tool Call Arguments in MCP Audit Log

tools/call audit entries now include the raw JSON arguments passed to each tool, truncated to 64 bytes (with a suffix) when the payload exceeds the cap. This gives you enough context to identify what a tool was called with — file paths, query strings, etc. — without letting large payloads bloat audit records.

New: Client-Cancelled Requests No Longer Surface as Failures

When a client closes its connection mid-flight, iron-proxy now detects the resulting context.Canceled from the upstream round-trip and records the request as action=client_cancel at INFO level with status 200. Previously these appeared as 502 errors in audit logs, creating phantom upstream failure noise in dashboards and alerting.

View on GitHub →

New: Postgres MITM with Role Injection (Experimental)

iron-proxy can now MITM-proxy PostgreSQL traffic, injecting SET ROLE "<role>" on the upstream session at startup so that Row-Level Security policies keyed on current_role scope per-tenant access even when your application connects as a shared superuser. Clients authenticate against proxy-managed credentials; the wire protocol is relayed transparently.

Client-issued role mutations are rejected: direct SET ROLE / RESET ROLE / SET SESSION AUTHORIZATION, indirect bypasses via SELECT set_config('role', ...) and CTE/subquery-wrapped variants, and opaque DO blocks.

Experimental. The Postgres listener is functional and integration-tested against Postgres 16, but the config surface and injection semantics are still stabilizing. Expect breaking changes in minor releases.

View on GitHub →

New: Lazy Secret Resolution

Secrets are no longer fetched from providers at pipeline-build time. Each resolver now validates its static configuration on startup and returns a deferred fetch that fires on the first matching request. Results are cached per the source's configured TTL; initial-fetch failures are also cached for the source TTL (default 30 s) so a slow or unavailable backend is not hammered on every request. Existing serve-stale-on-refresh-error behavior is preserved.

New: require on Secret Inject

The require option now applies to inject steps in addition to replace. Setting require: true rejects the request when the secret cannot be resolved; require: false silently skips the step and adds a secret_unavailable annotation to the audit log. This replaces the previous behavior where any resolve failure unconditionally returned a 502.

secrets:
  transforms:
    - inject:
        header: "Authorization"
        value: "Bearer {{vault.my-token}}"
        require: false   # skip silently if unavailable

New: Hot-Swap MCP Policy

MCP policy can now be updated without restarting the proxy. Policy is held in an atomic PolicyHolder so each request snapshots the current policy at start and stays consistent across request and response evaluation. Parse or compile errors on an incoming policy are logged and the prior policy is preserved — matching the existing behavior for transform pipeline errors. Standalone mode continues to use YAML as the source of truth, and the /v1/reload management endpoint now reloads MCP policy alongside the transform pipeline.

View on GitHub →

New: MCP-Aware Policy Interceptor

Adds a top-level mcp: config block that enforces a default-deny tool allowlist on Streamable HTTP MCP servers. Denied tools/call invocations return a JSON-RPC error envelope without reaching upstream. tools/list responses are filtered per-event over both application/json and text/event-stream so denied tools never reach the agent. Argument matchers (equals, in, matches) can be applied against params.arguments for fine-grained control. The audit log gains an mcp section with one structured record per JSON-RPC message observed. The interceptor runs after the transform pipeline, so allowlist still gates which hosts can be reached and secrets has already swapped proxy tokens before the interceptor evaluates the body.

mcp:
  error:
    code: -32001
    message: "blocked by iron-proxy policy"
  servers:
    - name: github
      rules:
        - host: "mcp.github.com"
          paths: ["/mcp", "/mcp/*"]
      tools:
        - name: "search_repositories"
        - name: "create_issue"
          when:
            - path: "owner"
              equals: "ironsh"
            - path: "repo"
              in: ["iron-proxy", "tunis-v2"]
        # Anything not listed is denied (default-deny).

v1 limitations: Only Streamable HTTP transport is supported. Legacy HTTP+SSE transport is not. A JSON-RPC batch with any denied entry is rejected as a whole. Resources and prompts (resources/list, resources/read, etc.) are not policy-filtered.