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

AWS Request Signing

The aws_auth transform re-signs inbound AWS SigV4 requests with real credentials, so workloads can configure their AWS SDK with placeholders and point at the proxy.

- name: aws_auth
  config:
    access_key_id:     {type: env, var: AWS_ACCESS_KEY_ID}
    secret_access_key: {type: env, var: AWS_SECRET_ACCESS_KEY}
    rules:
      - host: "*.amazonaws.com"

iron-proxy reads the region and service from the inbound SigV4 credential scope and re-signs with the real credentials. One config entry covers every AWS service the client talks to. Requires MITM mode.

How It Works

  1. Inbound signature parsing. The workload's AWS SDK signs the request with placeholder credentials. iron-proxy reads the credential scope (ACCESSKEY/DATE/REGION/SERVICE/aws4_request) from the Authorization header or, for pre-signed URLs, the X-Amz-Credential query parameter.
  2. Scope gating. If allowed_regions or allowed_services is set, the inbound scope must match. Otherwise the request is rejected with HTTP 403 before any credential is resolved.
  3. Credential resolution. access_key_id and secret_access_key each resolve through their own secret source, or credentials_provider resolves them through the AWS SDK's default credential chain.
  4. Body hashing. iron-proxy reads the body, computes its SHA-256, and uses that as the payload hash. With unsigned_payload: true the proxy skips the body read and uses the literal UNSIGNED-PAYLOAD sentinel that S3 and Bedrock streaming accept.
  5. Re-signing. The proxy strips the placeholder signature headers (Authorization, X-Amz-Date, X-Amz-Security-Token, X-Amz-Content-Sha256) and re-signs the request using the AWS SDK v4 signer with the real credentials, the scope's region and service, and the current time.
  6. Header injection. The proxy attaches the new Authorization, X-Amz-Date, and X-Amz-Content-Sha256 headers.

Configuring The Workload

The workload's AWS SDK needs three things: placeholder credentials, the correct region, and the proxy as its endpoint.

import boto3
 
bedrock = boto3.client(
    "bedrock-runtime",
    region_name="us-east-1",
    endpoint_url="https://bedrock-runtime.us-east-1.amazonaws.com",
    aws_access_key_id="AKIAIOSFODNN7EXAMPLE",
    aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
)

The access key and secret are AWS's documented example values. They have the right shape (20-character access key starting with AKIA, 40-character secret) for the SDK to accept them and produce a SigV4 signature. The values themselves are meaningless: iron-proxy strips the placeholder signature and re-signs with the real credentials before the request leaves the proxy. Any syntactically valid pair works; using the AWS example values makes it obvious to the next reader that these aren't real.

Route the workload's outbound traffic through iron-proxy (DNS interception, explicit proxy config, or a SOCKS tunnel). The SDK signs with the placeholder credentials; the proxy reads the region and service from that signature, re-signs with the real credentials, and forwards upstream.

Workload Identity

On EKS, ECS, or EC2, you don't have to give iron-proxy a long-lived access key. Set credentials_provider instead and the proxy resolves credentials through the AWS SDK's default chain: IAM Roles for Service Accounts (IRSA), EKS Pod Identity, and IMDSv2.

- name: aws_auth
  config:
    credentials_provider:
      type: workload_identity
    allowed_services: ["bedrock"]
    rules:
      - host: "*.amazonaws.com"

The proxy holds the rotating pod credentials. The workload still runs with the AWS example placeholder credentials, so real keys never reach the agent even though the underlying credential is short-lived.

credentials_provider is mutually exclusive with access_key_id and secret_access_key: set one or the other, not both. The AWS SDK refreshes the credentials on its own schedule; iron-proxy picks up the rotation automatically.

The provider discovers the region the same way the AWS SDK does (AWS_REGION, instance metadata, container metadata). Set region on the provider block to override that discovery.

credentials_provider:
  type: workload_identity
  region: us-east-1

Scope Gating

Without allowed_regions or allowed_services, the proxy will sign for any region and service the workload requests. That's convenient but broad. Limit the scope explicitly when you can:

- name: aws_auth
  config:
    access_key_id:     {type: env, var: AWS_ACCESS_KEY_ID}
    secret_access_key: {type: env, var: AWS_SECRET_ACCESS_KEY}
    allowed_regions:   ["us-east-1", "eu-west-1"]
    allowed_services:  ["bedrock", "s3", "dynamodb"]
    rules:
      - host: "*.amazonaws.com"

A request with a credential scope outside the allowed sets is rejected with HTTP 403 and rejected: region_not_allowed or rejected: service_not_allowed. The proxy never reaches the real credentials in that case.

Body Handling

aws_auth has to know the request body to compute its SHA-256 (the payload hash AWS signs over). Two modes:

  • Default. iron-proxy reads the full body, hashes it, and replays it to the upstream. Bodies shorter than Content-Length are rejected with HTTP 413 (body_truncated). Chunked bodies are rejected with HTTP 400 (chunked_body_not_allowed) unless you opt in with allow_chunked_body: true. Raise proxy.max_request_body_bytes if your workload sends large bodies.
  • Unsigned payload. Set unsigned_payload: true to skip the body read entirely. The proxy sends the literal UNSIGNED-PAYLOAD sentinel as the payload hash. AWS accepts this for S3, Bedrock streaming, and other services that document support for it; check the service's signing reference before turning it on.

If you use allow_chunked_body: true, the proxy logs a warning per signed chunked request and signs the buffered body as-is. There is no way to verify it matches what the client sent.

Configuration Reference

FieldTypeDefaultDescription
access_key_idsecret sourceReal AWS access key ID. Required unless credentials_provider is set.
secret_access_keysecret sourceReal AWS secret access key. Required unless credentials_provider is set.
credentials_providerobjectResolves credentials through the AWS SDK default chain (IRSA, EKS Pod Identity, IMDSv2). Mutually exclusive with access_key_id/secret_access_key. See Workload Identity.
allowed_regionsstring[]any regionAllowlist of AWS regions the entry will sign for. Empty allows any.
allowed_servicesstring[]any serviceAllowlist of AWS services the entry will sign for. Empty allows any.
unsigned_payloadbooleanfalseSign with UNSIGNED-PAYLOAD instead of reading and hashing the body.
allow_chunked_bodybooleanfalseSign chunked-encoding bodies without length verification.
rulesobject[]requiredDestinations this transform applies to. Uses the allowlist rule format. At least one rule is required.

Set exactly one of access_key_id/secret_access_key or credentials_provider.

Each static credential field is a discrete secret source. env, aws_sm, aws_ssm, 1password_connect, and 1password all work.

credentials_provider accepts:

FieldTypeDefaultDescription
typestringrequiredCurrently only workload_identity.
regionstringdiscoveredOverride the region the SDK would otherwise pick up from AWS_REGION or instance metadata.

Failure Modes

ReasonStatusWhen
missing_sigv4400The inbound request has no SigV4 signature: no SigV4 Authorization header and no X-Amz-Credential query parameter.
region_not_allowed403The inbound credential scope's region isn't in allowed_regions.
service_not_allowed403The inbound credential scope's service isn't in allowed_services.
credential_unavailable502A configured secret source returned an error when the proxy asked for its value.
body_missing400The request declared Content-Length > 0 but the body was empty or absent.
body_truncated413The buffered body was shorter than Content-Length. Raise proxy.max_request_body_bytes.
chunked_body_not_allowed400The body arrived chunked and allow_chunked_body is not set.
body_read_failed400The proxy couldn't read the body off the inbound socket.
signing_failed500The AWS SDK signer returned an error.

Audit Log

A successful signing produces these annotations on the aws_auth trace entry:

  • injected: the headers that were set. Always includes header:Authorization, header:X-Amz-Date, and header:X-Amz-Content-Sha256.
  • service: the AWS service taken from the inbound credential scope.
  • region: the AWS region taken from the inbound credential scope.

Rejections annotate rejected with one of the reasons above, plus contextual fields (region, service, credential, content_length, buffered_length, error) as appropriate.

Limitations

  • MITM mode only. sni-only has no method, path, or body to sign.
  • SigV4 only. The inbound request must already be SigV4-signed. iron-proxy validates and rewrites the signature; it doesn't sign from scratch.
  • Re-signs at the egress boundary. The signing time is the time iron-proxy re-signs, not the time the workload first signed. Clock skew between the proxy and AWS still matters; clock skew between the workload and the proxy doesn't.
  • One credential set per entry. Use multiple aws_auth entries with non-overlapping rules to map different upstream credentials to different workloads or destinations.

Related