Changelog

View on GitHub →

New: Postgres Single-Listener Architecture

The postgres configuration has been redesigned from a list of independent listeners to a single shared listener fronting multiple upstream databases. One bind address serves many upstreams: the client selects the upstream by the dbname it sends in its startup message, and the proxy routes accordingly. A single shared credential handles all client authentication; routing is by database name, so per-database credentials are not required.

Multi-statement Simple Queries are now also supported. The proxy forwards a batch as long as every statement passes the role policy, and rejects the batch only if any statement attempts a role mutation (SET ROLE, set_config('role', ...)) or a DO block.

# Before
postgres:
  - name: primary
    listen: ":5432"
    upstream:
      host: "db.internal"
      port: 5432
      sslmode: "require"
      user_env: "PG_UPSTREAM_USER"
      password_env: "PG_UPSTREAM_PASSWORD"
      database: "appdb"
    client:
      user: "app_user"
      password_env: "PG_PROXY_PASSWORD"
    role: "tenant_role"
 
# After
postgres:
  listen: ":5432"
  client:
    user: app_user
    password_env: PG_PROXY_PASSWORD
  upstreams:
    - database: appdb
      dsn:
        type: env
        var: PG_APPDB_DSN
      role: tenant_role
    - database: analyticsdb
      dsn:
        type: env
        var: PG_ANALYTICS_DSN
      role: analytics_role

This is a breaking config change for existing Postgres users. Migrate each entry in your postgres: list to an upstream under the new upstreams: key, replacing the per-listener upstream.host/port/... fields with a DSN source block. Postgres support is experimental.

New: File Secret Source

A new file secret source reads the secret from a path on disk. The file is re-read on every POST /v1/reload and on ttl expiry, so a long-lived proxy can pick up a rotated credential without restarting. Write the secret atomically (write-temp + rename) and call reload.

- source:
    type: file
    path: /etc/iron-proxy/secrets/OPENAI_API_KEY
  proxy_value: "proxy-token-123"
  match_headers: ["Authorization"]
  rules:
    - host: "api.openai.com"

Optional ttl and failure_ttl are supported. The value is the exact file contents (no trimming), so the writer controls trailing whitespace. Thanks to @drewstone for the contribution.

New: Outbound Proxy Routing

iron-proxy now routes its own outbound connections through an upstream SOCKS5 or HTTP CONNECT proxy. The standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables are honored automatically, so most deployments need no config change. An optional upstream_proxy block is available when environment variables aren't convenient.

proxy:
  upstream_proxy:
    http_proxy: "http://proxy.corp:3128"
    https_proxy: "http://proxy.corp:3128"
    no_proxy: "localhost,127.0.0.1,.internal.example.com"

This covers the HTTP/HTTPS forward data path (including CONNECT in mitm mode) and the OAuth credential-refresh client. Raw TCP tunnels (SNI-only passthrough, WebSocket) still dial directly. Note: when an upstream proxy is in use, upstream_deny_cidrs is enforced against the proxy's address rather than the final target, since the proxy resolves and connects to the target on iron-proxy's behalf.

New: 1Password Config Simplification

The host_env and token_env config fields on the 1password and 1password_connect secret sources have been removed. The SDK environment variable conventions (OP_SERVICE_ACCOUNT_TOKEN for 1password; OP_CONNECT_HOST and OP_CONNECT_TOKEN for 1password_connect) are now the only supported approach.

This is a breaking config change for 1password and 1password_connect users who set host_env or token_env. Remove those fields from your config.

Fixes

  • Fixed aws_auth rejecting HTTPS CONNECT tunnel setup with 400/missing_sigv4 before the signed request was ever sent. The synthetic CONNECT request used for tunnel policy checks can never carry a SigV4 signature; the transform now passes it through and signs the post-MITM request as intended. This affected boto3 and other clients routing HTTPS through a CONNECT proxy. Thanks to @0xdiid for the contribution.
  • Fixed percent-encoded reserved characters (e.g., %2F) being decoded before the upstream request was constructed, breaking APIs like GCS that route on encoded slashes within a path segment.
  • Fixed gcp_auth, hmac_sign, and oauth_token transforms being silently dropped in managed mode at startup and on every reload.
View on GitHub →

New: Workload Identity for AWS and GCP Auth

Both aws_auth and gcp_auth now accept a credentials_provider block in place of static key material. Setting type: workload_identity delegates credential resolution to the respective cloud SDK default chain: IRSA, EKS Pod Identity, and IMDSv2 for AWS; GKE Workload Identity, GOOGLE_APPLICATION_CREDENTIALS, and Workload Identity Federation for GCP. The proxy holds the rotating pod credentials and the agent SDK runs with placeholder credentials, so real keys never reach the agent.

For gcp_auth, the subject field (domain-wide delegation) is rejected when credentials_provider is set, since metadata-server credentials cannot impersonate.

# AWS: workload identity (IRSA / Pod Identity / IMDSv2)
- name: aws_auth
  config:
    credentials_provider:
      type: workload_identity
      # region: us-east-1   # optional; overrides AWS_REGION discovery
    allowed_services: ["bedrock"]
    rules:
      - host: "*.amazonaws.com"
 
# GCP: workload identity (GKE Workload Identity / ADC)
- name: gcp_auth
  config:
    credentials_provider:
      type: workload_identity
    scopes:
      - "https://www.googleapis.com/auth/cloud-platform"
    rules:
      - host: "*.googleapis.com"
View on GitHub →

New: AWS Request Signing

The new aws_auth transform re-signs inbound AWS SigV4 requests with real credentials pulled from any registered secret source (env, aws_sm, aws_ssm, 1password, and others). Clients configure their AWS SDK with placeholder credentials and point it at the proxy. The proxy reads the region and service from the inbound credential scope and re-signs with the real ones, so a single config entry covers every AWS service the client talks to. Use allowed_regions and allowed_services to gate which scopes the entry will sign for.

- name: aws_auth
  config:
    access_key_id:     {type: env, var: AWS_ACCESS_KEY_ID}
    secret_access_key: {type: env, var: AWS_SECRET_ACCESS_KEY}
    # session_token:   {type: env, var: AWS_SESSION_TOKEN}  # optional, for STS / role creds
    # allowed_regions:  ["us-east-1", "eu-west-1"]          # optional; default allows any region
    # allowed_services: ["bedrock", "s3", "dynamodb"]       # optional; default allows any service
    # unsigned_payload: false
    # allow_chunked_body: false
    rules:
      - host: "*.amazonaws.com"

Requires MITM mode. Chunked request bodies are rejected (400) unless allow_chunked_body: true. For streaming uploads or cases where the body must not be buffered (S3 multipart, etc.), set unsigned_payload: true.

View on GitHub →

New: JWT-Bearer Grant (RFC 7523)

The oauth_token transform now supports grant: jwt_bearer for RFC 7523 JWT-bearer flows. The proxy mints a JWT signed with an RSA private key sourced from any secrets backend, exchanges it at the token endpoint, and injects the resulting bearer on matching requests. This covers DocuSign, Salesforce, Box, Zoom Server-to-Server, Adobe Sign, and any other vendor that trades a signed JWT assertion for an access token. Token caching, single-flight, fingerprint-based credential rotation, and token_endpoint_headers all apply as with the other grant types.

- name: oauth_token
  config:
    tokens:
      - grant: jwt_bearer
        issuer:         {type: env, var: DOCUSIGN_INTEGRATION_KEY}
        subject:        {type: env, var: DOCUSIGN_USER_GUID}
        private_key:
          type: 1password_connect
          secret_ref: "op://Engineering/DOCUSIGN/private-key.pem"
        private_key_id: {type: env, var: DOCUSIGN_KEY_ID}   # optional; emitted as JWT kid header
        audience: "account.docusign.com"
        token_endpoint: "https://account.docusign.com/oauth/token"
        scopes: ["signature", "impersonation"]
        rules:
          - host: "*.docusign.net"

The same shape works for Salesforce (audience: https://login.salesforce.com, token_endpoint: https://login.salesforce.com/services/oauth2/token), Box, and Zoom S2S. Only audience, token_endpoint, and scopes change. The existing gcp_auth transform remains the convenience wrapper for Google's keyfile format.

View on GitHub →

New: OAuth Password Grant

The oauth_token transform now supports the RFC 6749 4.3 resource-owner password grant. Configure grant: password with username, password, and client_id (plus optional client_secret). Caching, single-flight, and token refresh on expiry behave the same as the other grant types.

- name: oauth_token
  config:
    tokens:
      - grant: password
        username:       {type: env, var: API_USERNAME}
        password:       {type: env, var: API_PASSWORD}
        client_id:      {type: env, var: API_CLIENT_ID}
        client_secret:  {type: env, var: API_CLIENT_SECRET}
        token_endpoint: "https://api.example.com/token"
        rules:
          - host: "api.example.com"

New: Token Endpoint Headers

A new token_endpoint_headers map on any oauth_token grant sends extra headers on the token POST itself. Each value is a discrete secret source. Use this when the token endpoint requires an API key or other credential alongside the standard form-body client auth.

token_endpoint_headers:
  x-api-key: {type: env, var: API_KEY}