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
- 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 theAuthorizationheader or, for pre-signed URLs, theX-Amz-Credentialquery parameter. - Scope gating. If
allowed_regionsorallowed_servicesis set, the inbound scope must match. Otherwise the request is rejected with HTTP 403 before any credential is resolved. - Credential resolution.
access_key_idandsecret_access_keyeach resolve through their own secret source, orcredentials_providerresolves them through the AWS SDK's default credential chain. - Body hashing. iron-proxy reads the body, computes its SHA-256, and uses that as the payload hash. With
unsigned_payload: truethe proxy skips the body read and uses the literalUNSIGNED-PAYLOADsentinel that S3 and Bedrock streaming accept. - 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. - Header injection. The proxy attaches the new
Authorization,X-Amz-Date, andX-Amz-Content-Sha256headers.
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-1Scope 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-Lengthare rejected with HTTP 413 (body_truncated). Chunked bodies are rejected with HTTP 400 (chunked_body_not_allowed) unless you opt in withallow_chunked_body: true. Raiseproxy.max_request_body_bytesif your workload sends large bodies. - Unsigned payload. Set
unsigned_payload: trueto skip the body read entirely. The proxy sends the literalUNSIGNED-PAYLOADsentinel 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
| Field | Type | Default | Description |
|---|---|---|---|
access_key_id | secret source | Real AWS access key ID. Required unless credentials_provider is set. | |
secret_access_key | secret source | Real AWS secret access key. Required unless credentials_provider is set. | |
credentials_provider | object | Resolves 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_regions | string[] | any region | Allowlist of AWS regions the entry will sign for. Empty allows any. |
allowed_services | string[] | any service | Allowlist of AWS services the entry will sign for. Empty allows any. |
unsigned_payload | boolean | false | Sign with UNSIGNED-PAYLOAD instead of reading and hashing the body. |
allow_chunked_body | boolean | false | Sign chunked-encoding bodies without length verification. |
rules | object[] | required | Destinations 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:
| Field | Type | Default | Description |
|---|---|---|---|
type | string | required | Currently only workload_identity. |
region | string | discovered | Override the region the SDK would otherwise pick up from AWS_REGION or instance metadata. |
Failure Modes
| Reason | Status | When |
|---|---|---|
missing_sigv4 | 400 | The inbound request has no SigV4 signature: no SigV4 Authorization header and no X-Amz-Credential query parameter. |
region_not_allowed | 403 | The inbound credential scope's region isn't in allowed_regions. |
service_not_allowed | 403 | The inbound credential scope's service isn't in allowed_services. |
credential_unavailable | 502 | A configured secret source returned an error when the proxy asked for its value. |
body_missing | 400 | The request declared Content-Length > 0 but the body was empty or absent. |
body_truncated | 413 | The buffered body was shorter than Content-Length. Raise proxy.max_request_body_bytes. |
chunked_body_not_allowed | 400 | The body arrived chunked and allow_chunked_body is not set. |
body_read_failed | 400 | The proxy couldn't read the body off the inbound socket. |
signing_failed | 500 | The 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 includesheader:Authorization,header:X-Amz-Date, andheader: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-onlyhas 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_authentries with non-overlappingrulesto map different upstream credentials to different workloads or destinations.
Related
- Static Secrets: inject a static API key when the upstream isn't AWS SigV4.
- HMAC Request Signing: the generic signing transform for non-AWS schemes.
- Configuration reference: the canonical schema for the
aws_authtransform.