Changelog

View on GitHub →

New: HMAC Request Signing

The new hmac_sign transform signs outbound requests with HMAC before forwarding them upstream. Algorithm (sha256, sha512, sha1), key encoding, output encoding, timestamp format, and the signed message template are all configurable. The signed message is a Go template with access to .Timestamp, .Method, .Path, .PathWithQuery, .Query, .Host, and .Body. Headers are injected in declaration order and preserve user-specified casing on the wire. Credentials (including the HMAC key) resolve through the existing secrets sources — env, AWS Secrets Manager, SSM, and 1Password all work.

Because a truncated body would produce an invalid signature, the transform enforces body integrity: requests whose buffered body falls short of Content-Length (truncated by proxy.max_request_body_bytes) are rejected with 413, and chunked bodies are rejected with 400 unless allow_chunked_body: true is set. Requires MITM mode.

- name: hmac_sign
  config:
    timestamp:
      format: unix_seconds        # unix_seconds | unix_millis | unix_nanos | rfc3339
    signature:
      algorithm: sha256           # sha256 | sha512 | sha1
      key_encoding: base64        # raw | base64 | hex
      output_encoding: base64     # base64 | hex
      # Go template. Available: .Timestamp .Method .Path .PathWithQuery .Query .Host .Body
      message: "{{.Timestamp}}{{.Method}}{{.PathWithQuery}}{{.Body}}"
    credentials:
      key:        {type: env, var: FALCONX_API_KEY}
      secret:     {type: env, var: FALCONX_SECRET}      # required: the HMAC key
      passphrase: {type: env, var: FALCONX_PASSPHRASE}
    headers:                      # ordered list — case is preserved on the wire
      - {name: "FX-ACCESS-KEY",        value: "{{.Credentials.key}}"}
      - {name: "FX-ACCESS-SIGN",       value: "{{.Signature}}"}
      - {name: "FX-ACCESS-TIMESTAMP",  value: "{{.Timestamp}}"}
      - {name: "FX-ACCESS-PASSPHRASE", value: "{{.Credentials.passphrase}}"}
    # allow_chunked_body: false   # opt-in to signing chunked (no Content-Length) bodies
    rules:
      - host: "api.falconx.io"
View on GitHub →

New: Preserved Header Casing for Inject Mode

The secrets transform now sends inject-mode headers upstream with the exact casing written in config rather than Go's canonical HTTP form. Previously, a configured header: X-API-KEY was forwarded as X-Api-Key due to http.Header.Set canonicalization. The header is now assigned directly, matching the behavior added for match_headers in v0.35.0. The injected audit annotation now also reflects the wire casing.

inject:
  # The header name is sent upstream with the casing written here.
  header: "X-API-KEY"
  formatter: "Bearer {{ .Value }}"

Note: HTTP/2 upstreams lowercase all header names regardless of what iron-proxy sends, so this casing control applies to HTTP/1.x connections only.

New: Hop-by-Hop Header Stripping

iron-proxy now strips hop-by-hop headers and Connection-named tokens from requests before forwarding them upstream, on both HTTP and WebSocket paths. TE: trailers is preserved for gRPC. This prevents proxy-internal headers (such as Transfer-Encoding, Proxy-Authorization, and any custom Connection tokens) from reaching upstream services.

New: Dot-Segment Path Rejection

Request paths containing . or .. segments are now rejected before policy evaluation. This ensures that policy rules are applied to well-formed, normalized paths and that these segments don't reach upstream services.

View on GitHub →

New: Preserved Header Casing in match_headers

The secrets transform now forwards headers upstream with the exact casing written in match_headers rather than Go's canonical HTTP form. A literal entry like x-api-KEY still matches the inbound header case-insensitively, but the rewritten header is sent upstream as x-api-KEY. Regex entries (/pattern/) are unaffected and preserve the header's existing casing on the request.

replace:
  match_headers:
    - "x-api-key"   # forwarded upstream as x-api-key, not X-Api-Key

Note: HTTP/2 upstreams lowercase all header names regardless of what iron-proxy sends, so this casing control applies to HTTP/1.x connections only.

View on GitHub →

New: Query String Scanning

The secrets transform's replace mode no longer scans the URL query string for proxy tokens by default. Query string scanning must now be enabled explicitly with match_query: true, consistent with how match_path already works. Query strings frequently appear in access logs, so this is now opt-in to avoid unintended credential exposure.

# Before (implicit query string scanning)
transforms:
  - name: secrets
    config:
      secrets:
        - source:
            type: env
            var: MAPS_API_KEY
          replace:
            proxy_value: "proxy-maps-token-123"
          rules:
            - host: "maps.googleapis.com"
 
# After (opt in explicitly)
transforms:
  - name: secrets
    config:
      secrets:
        - source:
            type: env
            var: MAPS_API_KEY
          replace:
            proxy_value: "proxy-maps-token-123"
            match_query: true
          rules:
            - host: "maps.googleapis.com"

This is a breaking config change for existing users who pass proxy tokens via URL query parameters. Add match_query: true to any replace block that needs to swap query string values.

View on GitHub →

New: Body Capture

The new body_capture transform records decoded request bodies of matching hosts onto the audit log as body_capture.request_body and body_capture.request_body_truncated fields. Useful for auditing the payloads passing through the proxy (such as the prompts a sandbox sends to an LLM provider) without modifying upstream traffic. Thanks to @elenaxzhao for the contribution.

max_request_body_bytes caps how much of each body is captured; bodies larger than the cap are truncated to the prefix and body_capture.request_body_truncated is set to true. The cap defaults to 16 KiB and is independent of the global proxy.max_request_body_bytes limit. The transform is observation-only: it never rejects a request, and body read errors are annotated on the trace rather than failing the request. Response bodies are not captured.

Note: Captured bodies are written to the audit log in plain text. When secrets runs with match_body: true, place body_capture before secrets so the audit log records the sandbox's proxy tokens rather than the real credentials secrets swaps in.

transforms:
  - name: body_capture
    config:
      max_request_body_bytes: 16384
      rules:
        - host: "api.anthropic.com"
          methods: ["POST"]
          paths: ["/v1/messages"]
        - host: "api.openai.com"
          methods: ["POST"]
          paths: ["/v1/chat/completions"]