Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Static Secrets

The secrets transform applies a fixed credential (API key, bearer token, basic-auth password) to outbound requests so the workload never holds the real value.

transforms:
  - name: secrets
    config:
      secrets:
        - source: {type: env, var: OPENAI_API_KEY}
          inject:
            header: "Authorization"
            formatter: "Bearer {{ .Value }}"
          rules:
            - host: "api.openai.com"
              methods: ["POST"]
              paths: ["/v1/*"]

This adds Authorization: Bearer $OPENAI_API_KEY to every POST to api.openai.com/v1/*. The workload sends a plain request; iron-proxy reads OPENAI_API_KEY from its environment and attaches the header on the way out.

Use secrets when the upstream accepts a static credential. For other patterns, see OAuth2 Token Injection, HMAC Request Signing, AWS Request Signing, or GCP Service Accounts.

How It Works

Each secret entry has three parts:

  • Source. Where iron-proxy reads the credential value from. An environment variable, AWS Secrets Manager, 1Password Connect, and so on.
  • Mode. How the credential gets applied. Inject adds it unconditionally. Replace swaps a placeholder token in the workload's request for the real value.
  • Rules. Which destinations the secret applies to. Same host/method/path matching as allowlist rules.

When a request matches a secret's rules, iron-proxy applies the credential before forwarding upstream.

Inject Mode

iron-proxy sets a header or query parameter on every matching request. The workload doesn't carry a credential at all: it sends a plain request and the proxy adds the authentication on the way out. The opening example above is in inject mode.

Inject Options

You can inject into either a header or a query parameter:

  • header: sets the named header. The header name is sent upstream with the exact casing written here (HTTP/1.x only: HTTP/2 lowercases header names regardless). Use formatter to control the value format. The formatter field is a Go template that receives .Value (the resolved secret) and a base64 helper.
  • query_param: appends the named query parameter with the secret as its value. No formatter is needed.
# Inject as a query parameter
inject:
  query_param: "api_key"

The base64 helper is useful for services that use HTTP Basic authentication. For example, GitHub's API accepts Basic auth where the password is a personal access token. The base64 helper concatenates its arguments and base64-encodes the result:

# GitHub Basic auth: base64("x-access-token:<token>")
inject:
  header: "Authorization"
  formatter: 'Basic {{ base64 "x-access-token:" .Value }}'

This produces a header like Authorization: Basic eC1hY2Nlc3MtdG9rZW46Z2hwX2FiYzEyMw==, which GitHub decodes and authenticates as a token-based login.

When to Use Inject Mode

Inject mode is the stronger isolation model. Workloads have no knowledge of credentials at all: no environment variables, no tokens, no placeholders. Reach for it when:

  • You control the workload's code and it doesn't need to reference the credential directly.
  • You want to rule out credential leakage from the workload's process environment.
  • The upstream API takes credentials in a single header or query parameter.

Replace Mode

The workload sends a proxy token: an opaque placeholder that means nothing outside iron-proxy. The proxy scans outbound requests for this token and swaps it for the real credential.

transforms:
  - name: secrets
    config:
      secrets:
        - source:
            type: aws_sm
            secret_id: "arn:aws:secretsmanager:us-west-1:123456789:secret:anthropic-key"
            region: "us-west-1"
            ttl: 5m
          replace:
            proxy_value: "pk-proxy-anthropic-xyz"
            match_headers: ["x-api-key"]
          rules:
            - host: "api.anthropic.com"

The workload sends x-api-key: pk-proxy-anthropic-xyz. iron-proxy swaps the placeholder for the real key from AWS Secrets Manager before the request reaches Anthropic.

Replace Options

  • proxy_value (required): the placeholder token the workload uses. Choose something clearly identifiable (e.g., pk-proxy-<service>-<random>).
  • match_headers: list of header names to scan for the token. An empty list scans all headers. Each entry is either a literal header name (case-insensitive) or a /regex/ pattern. Literal and regex entries can be mixed in the same list. Headers are forwarded upstream with the exact casing written here (HTTP/1.x only: HTTP/2 lowercases header names regardless).
  • match_body: when true, the proxy also scans the request body for the token.
  • match_path: when true, the proxy scans req.URL.Path for the token. Off by default because URL paths often land in access logs on either side of the proxy.
  • match_query: when true, the proxy scans the URL query string for the token. Off by default for the same reason as match_path: query strings frequently appear in access logs.

Path-Embedded Tokens

Some upstreams put the credential in the URL path. Telegram is the canonical example: clients call /bot<TOKEN>/sendMessage. Set match_path: true to swap a placeholder token in the path:

- source:
    type: env
    var: TELEGRAM_BOT_TOKEN
  replace:
    proxy_value: "proxy-tg-token-123"
    match_headers: []
    match_path: true
    require: true
  rules:
    - host: "api.telegram.org"

Inject mode doesn't extend to paths. There's no anchor for the proxy to insert at, so the secret would either duplicate or overwrite the client's path.

When to Use Replace Mode

Reach for replace mode when:

  • You can't modify the workload's code, but you can set its environment variables or configuration.
  • The workload or its libraries expect a credential in the environment and include it in requests automatically. git reading GITHUB_TOKEN is the textbook case.
  • You want a stable proxy token that survives secret rotation. The real value changes; the proxy token stays the same.

The require Flag

Replace mode supports a require flag. When true, iron-proxy rejects matching requests with HTTP 403 if the proxy token is missing from every scanned location. This stops workloads from bypassing the proxy with their own credentials.

- source:
    type: env
    var: ANTHROPIC_API_KEY
  replace:
    proxy_value: "pk-proxy-anthropic"
    match_headers: ["x-api-key"]
    require: true
  rules:
    - host: "api.anthropic.com"

Secret Sources

iron-proxy reads secret values from one of five backends: environment variables, AWS Secrets Manager, AWS Systems Manager Parameter Store, 1Password Connect, and 1Password. Each source is an object on the source field, discriminated by type.

For 1Password deployments, prefer 1password_connect over 1password. The hosted 1Password SDK is rate-limited per account and can stall request handling under load. 1Password Connect runs in your own infrastructure and isn't subject to those limits.

Every source supports an optional json_key field. When set, iron-proxy parses the value as JSON and extracts the named field. Use this to pack multiple credentials into a single JSON secret.

Environment Variable (env)

Reads the secret from an environment variable on the iron-proxy process. The simplest option: set the variable when starting iron-proxy and the proxy reads it at startup.

source:
  type: env
  var: MY_API_KEY

The value is read once at startup and held in memory. Restart iron-proxy with a new value to rotate.

AWS Secrets Manager (aws_sm)

Reads the secret from AWS Secrets Manager. The value is cached locally and refreshed on a TTL, so you can rotate secrets in Secrets Manager without restarting iron-proxy.

source:
  type: aws_sm
  secret_id: "arn:aws:secretsmanager:us-west-1:123456789:secret:my-key"
  region: "us-west-1"
  json_key: "api_key"
  ttl: 15m
FieldTypeDefaultDescription
secret_idstringrequiredSecret ARN or name in AWS Secrets Manager.
regionstringAWS SDK defaultAWS region where the secret is stored.
json_keystringWhen set, parse the fetched value as JSON and extract this field.
ttlduration0Re-fetch interval. Set to 0 to read the value once at startup.

AWS Systems Manager Parameter Store (aws_ssm)

Reads the secret from AWS Systems Manager Parameter Store. A lower-cost alternative to Secrets Manager. A good fit for teams already storing application configuration there.

source:
  type: aws_ssm
  name: "/myapp/api-key"
  region: "us-east-1"
  with_decryption: true
  json_key: "api_key"
  ttl: 15m
FieldTypeDefaultDescription
namestringrequiredParameter name or ARN.
regionstringAWS SDK defaultAWS region where the parameter is stored.
with_decryptionbooleantrueDecrypt SecureString parameters. Leave enabled for encrypted parameters.
json_keystringWhen set, parse the fetched value as JSON and extract this field.
ttlduration0Re-fetch interval. Set to 0 to read the value once at startup.

iron-proxy authenticates to both aws_sm and aws_ssm through the standard AWS credential chain: environment variables, instance profile, ECS task role, and so on. No AWS configuration goes in the iron-proxy config file. The resolved identity must be authorized to read each referenced secret. See Required AWS Permissions below.

1Password (1password)

Reads the secret from 1Password using a service account token. Values are cached locally and refreshed on a TTL. Prefer 1password_connect (below) where possible. The hosted SDK is rate-limited per account and can become a bottleneck under load.

source:
  type: 1password
  secret_ref: "op://Engineering/OpenAI/credential"
  token_env: OP_SERVICE_ACCOUNT_TOKEN
  ttl: 15m
FieldTypeDefaultDescription
secret_refstringrequired1Password reference using the op://vault/item/[section/]field syntax.
token_envstringOP_SERVICE_ACCOUNT_TOKENEnvironment variable holding the 1Password service account token.
ttlduration0Re-fetch interval. Set to 0 to read the value once at startup.

The 1Password SDK loads a Wasm module on each client construction. iron-proxy reuses one client per token_env value across all entries that reference it.

1Password Connect (1password_connect)

Reads the secret from a 1Password Connect server you run. The recommended 1Password integration: Connect runs locally and isn't subject to the per-account rate limits that throttle the hosted SDK. Values are cached locally and refreshed on a TTL.

source:
  type: 1password_connect
  secret_ref: "op://Engineering/OpenAI/credential"
  host_env: OP_CONNECT_HOST
  token_env: OP_CONNECT_TOKEN
  ttl: 15m
FieldTypeDefaultDescription
secret_refstringrequired1Password reference using the op://vault/item/[section/]field syntax.
host_envstringOP_CONNECT_HOSTEnvironment variable holding the Connect server URL.
token_envstringOP_CONNECT_TOKENEnvironment variable holding the Connect API token.
ttlduration0Re-fetch interval. Set to 0 to read the value once at startup.

Scoping Secrets With Rules

Every secret entry supports a rules array that restricts which requests the secret applies to. Rules use the same format as allowlist rules: host globs, methods, path patterns.

rules:
  - host: "api.openai.com"
    methods: ["POST"]
    paths: ["/v1/*"]

If you omit rules, the secret applies to every request through the proxy. Always scope secrets to the narrowest set of destinations that work, so a misconfiguration leaks as little as possible.

Combining Multiple Secrets

One secrets transform can hold many entries. Each one is evaluated independently. A request can match zero, one, or several.

transforms:
  - name: secrets
    config:
      secrets:
        - source:
            type: env
            var: OPENAI_API_KEY
          inject:
            header: "Authorization"
            formatter: "Bearer {{ .Value }}"
          rules:
            - host: "api.openai.com"
 
        - source:
            type: aws_sm
            secret_id: "arn:aws:secretsmanager:us-west-1:123456789:secret:anthropic-key"
            region: "us-west-1"
            ttl: 15m
          replace:
            proxy_value: "pk-proxy-anthropic"
            match_headers: ["x-api-key"]
          rules:
            - host: "api.anthropic.com"

You can mix inject and replace mode secrets in the same transform.

Required AWS Permissions

The aws_sm and aws_ssm sources call AWS at runtime, so the proxy host needs an AWS identity authorized to read each referenced secret. Without that identity, the proxy starts but every fetch fails. Configure permissions on the AWS side, not in the iron-proxy config.

For Secrets Manager (aws_sm), attach a policy that grants secretsmanager:GetSecretValue (and kms:Decrypt if the secret is encrypted with a customer-managed KMS key) on each secret ARN the proxy will read:

{
  "Effect": "Allow",
  "Action": ["secretsmanager:GetSecretValue"],
  "Resource": [
    "arn:aws:secretsmanager:us-west-1:123456789:secret:openai-key-*",
    "arn:aws:secretsmanager:us-west-1:123456789:secret:anthropic-key-*"
  ]
}

For Parameter Store (aws_ssm), grant ssm:GetParameter on each parameter, plus kms:Decrypt when reading SecureString parameters with a customer-managed key:

{
  "Effect": "Allow",
  "Action": ["ssm:GetParameter"],
  "Resource": ["arn:aws:ssm:us-east-1:123456789:parameter/myapp/*"]
}

Attach the policy to whatever identity the proxy host runs under: an EC2 instance profile, an ECS task role, an EKS IRSA service account, or an IAM user whose access keys are exported in the environment. Scope the resource ARNs to exactly the secrets the proxy needs. A fleet-wide Resource: "*" defeats the point of running a proxy in front of credentials.

When the same secret is served by many proxies, give each proxy host its own role rather than sharing one. Per-host roles let you revoke a single compromised host without rotating every secret it could reach, and CloudTrail attributes the read to the right caller for audit.

Related