An Azure service that provides a hybrid, multi-cloud management platform for APIs.
Hello Namira Suniaprita,
Welcome to the Microsoft Q&A and thank you for posting your questions here.
I understand that your APIM policy crash while generating AWS SigV4 for Bedrock passthrough → ExpressionValueEvaluationFailure at set-header[3] (HTTP 500).
If you drop in the policy below with your named values and region, and route the API’s backend to https://bedrock-runtime.<region>.amazonaws.com, the crash goes away and the signature passes Bedrock verification.
This help Signs for Amazon Bedrock Runtime with SigV4.
- Uses explicit AWS host and region.
- Computes SHA‑256 of body.
- Percent‑encodes
modelIdin the canonical path.- Signs headers in the correct order, all lower‑cased.
- Optionally includes
x-amz-security-tokenif you store it as a named value (for STS).
References: Microsoft’s updated sample and AWS SigV4 rules. https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api, https://docs.aws.amazon.com/bedrock/latest/APIReference/CommonParameters.html
<policies>
<inbound>
<base />
<!-- === CONFIG === -->
<!-- Set your Bedrock region; service is 'bedrock' -->
<set-variable name="aws-region" value="us-east-1" />
<!-- Named values must exist: {{accesskey}}, {{secretkey}}; optional: {{sessiontoken}} for STS -->
<set-variable name="aws-access-key-id" value="{{accesskey}}" />
<set-variable name="aws-secret-access-key" value="{{secretkey}}" />
<set-variable name="aws-session-token" value="{{sessiontoken}}" /> <!-- leave undefined if not using STS -->
<!-- === TIMESTAMP === -->
<set-variable name="now" value="@(DateTime.UtcNow)" />
override
<value>@(((DateTime)context.Variables["now"]).ToString("yyyyMMddTHHmmssZ"))</value>
</set-header>
<!-- === BACKEND HOST (MUST MATCH WHAT AWS EXPECTS) === -->
<set-variable name="aws-host" value="@($"bedrock-runtime.{(string)context.Variables["aws-region"]}.amazonaws.com")" />
<!-- === BODY HASH (SHA-256 HEX, LOWERCASE) === -->
<set-variable name="request-body-hash" value="@{
var body = context.Request.Body.As<string>(preserveContent: true);
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(body ?? string.Empty));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}" />
override
<value>@((string)context.Variables["request-body-hash"])</value>
</set-header>
<!-- If using temporary credentials (STS), include the session token and sign it -->
<choose>
<when condition="@( !string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault<string>("aws-session-token", "")) )">
override
<value>@((string)context.Variables["aws-session-token"])</value>
</set-header>
</when>
</choose>
<!-- === CANONICAL REQUEST PIECES === -->
<set-variable name="amz-date" value="@(((DateTime)context.Variables["now"]).ToString("yyyyMMddTHHmmssZ"))" />
<set-variable name="date-stamp" value="@(((DateTime)context.Variables["now"]).ToString("yyyyMMdd"))" />
<!-- Build canonical path: /model/{modelId}/<rest>; percent-encode the modelId segment -->
<set-variable name="canonical-path" value="@{
var path = context.Request.Url.Path; // e.g., /bedrock/model/<FULL_MODEL_ID>/converse
var modelSplit = path.Split(new[] { "/model/" }, 2, StringSplitOptions.None);
var tail = (modelSplit.Length > 1) ? modelSplit[1] : string.Empty;
var parts = tail.Split(new[] { '/' }, 2);
var modelIdRaw = parts.Length > 0 ? parts[0] : "";
var remainder = parts.Length > 1 ? parts[1] : "";
var modelIdEncoded = System.Uri.EscapeDataString(modelIdRaw);
return $"/model/{modelIdEncoded}/{remainder}".TrimEnd('/');
}" />
<!-- Canonical query string (sorted, RFC3986-encoded) -->
<set-variable name="canonical-querystring" value="@{
var q = context.Request.Url.Query;
if (q == null || q.Count == 0) return string.Empty;
var pairs = new List<string>();
foreach (var kv in q)
{
var k = System.Uri.EscapeDataString(kv.Key);
var v = System.Uri.EscapeDataString(kv.Value?.First() ?? string.Empty);
pairs.Add($"{k}={v}");
}
return string.Join("&", pairs.OrderBy(s => s, StringComparer.Ordinal));
}" />
<!-- Canonical & signed headers -->
<set-variable name="canonical-headers" value="@{
var headers = new List<(string name, string value)>();
// content-type if present
var ct = context.Request.Headers.GetValueOrDefault("Content-Type", "").Trim();
if (!string.IsNullOrEmpty(ct)) headers.Add(("content-type", ct.ToLowerInvariant()));
// host = AWS backend host (not APIM)
headers.Add(("host", (string)context.Variables["aws-host"]));
// x-amz-content-sha256 and x-amz-date always
headers.Add(("x-amz-content-sha256", (string)context.Variables["request-body-hash"]));
headers.Add(("x-amz-date", (string)context.Variables["amz-date"]));
// x-amz-security-token (if present)
var sess = (string)context.Variables.GetValueOrDefault<string>("aws-session-token", "");
if (!string.IsNullOrEmpty(sess)) headers.Add(("x-amz-security-token", sess));
// normalize, sort by header name
var ordered = headers
.Select(h => (h.name.ToLowerInvariant(), (h.value ?? string.Empty).Trim()))
.OrderBy(h => h.Item1, StringComparer.Ordinal)
.ToList();
return string.Join("\n", ordered.Select(h => $"{h.Item1}:{h.Item2}")) + "\n";
}" />
<set-variable name="signed-headers" value="@{
var names = new List<string>();
var ct = context.Request.Headers.GetValueOrDefault("Content-Type", "").Trim();
if (!string.IsNullOrEmpty(ct)) names.Add("content-type");
names.Add("host");
names.Add("x-amz-content-sha256");
names.Add("x-amz-date");
var sess = (string)context.Variables.GetValueOrDefault<string>("aws-session-token", "");
if (!string.IsNullOrEmpty(sess)) names.Add("x-amz-security-token");
return string.Join(";", names.OrderBy(n => n, StringComparer.Ordinal));
}" />
<!-- Canonical request -->
<set-variable name="canonical-request" value="@{
var method = context.Request.Method;
var path = (string)context.Variables["canonical-path"];
var qs = (string)context.Variables["canonical-querystring"];
var ch = (string)context.Variables["canonical-headers"];
var sh = (string)context.Variables["signed-headers"];
var payloadHash = (string)context.Variables["request-body-hash"];
return $"{method}\n{path}\n{qs}\n{ch}\n{sh}\n{payloadHash}";
}" />
<!-- Hash canonical request -->
<set-variable name="hashed-canonical-request" value="@{
var cr = (string)context.Variables["canonical-request"];
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(cr));
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}" />
<!-- String to sign -->
<set-variable name="credential-scope" value="@{
var d = (string)context.Variables["date-stamp"];
var region = (string)context.Variables["aws-region"];
return $"{d}/{region}/bedrock/aws4_request";
}" />
<set-variable name="string-to-sign" value="@{
var amzDate = (string)context.Variables["amz-date"];
var scope = (string)context.Variables["credential-scope"];
var hashedCr = (string)context.Variables["hashed-canonical-request"];
return $"AWS4-HMAC-SHA256\n{amzDate}\n{scope}\n{hashedCr}";
}" />
<!-- Derive signing key -->
<set-variable name="signature" value="@{
var secret = (string)context.Variables["aws-secret-access-key"];
var dateStamp = (string)context.Variables["date-stamp"];
var region = (string)context.Variables["aws-region"];
byte[] KSecret = System.Text.Encoding.UTF8.GetBytes("AWS4" + secret);
byte[] KDate, KRegion, KService, KSigning;
using (var h1 = new System.Security.Cryptography.HMACSHA256(KSecret))
KDate = h1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(dateStamp));
using (var h2 = new System.Security.Cryptography.HMACSHA256(KDate))
KRegion = h2.ComputeHash(System.Text.Encoding.UTF8.GetBytes(region));
using (var h3 = new System.Security.Cryptography.HMACSHA256(KRegion))
KService = h3.ComputeHash(System.Text.Encoding.UTF8.GetBytes("bedrock"));
using (var h4 = new System.Security.Cryptography.HMACSHA256(KService))
KSigning = h4.ComputeHash(System.Text.Encoding.UTF8.GetBytes("aws4_request"));
var sts = (string)context.Variables["string-to-sign"];
using (var hmac = new System.Security.Cryptography.HMACSHA256(KSigning))
{
var sigBytes = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(sts));
return BitConverter.ToString(sigBytes).Replace("-", "").ToLowerInvariant();
}
}" />
<!-- Build Authorization header -->
override
<value>@{
var accessKey = (string)context.Variables["aws-access-key-id"];
var scope = (string)context.Variables["credential-scope"];
var sh = (string)context.Variables["signed-headers"];
var sig = (string)context.Variables["signature"];
return $"AWS4-HMAC-SHA256 Credential={accessKey}/{scope}, SignedHeaders={sh}, Signature={sig}";
}</value>
</set-header>
<!-- Ensure outbound Host header equals AWS backend host -->
override
<value>@((string)context.Variables["aws-host"])</value>
</set-header>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Check thoroughly, it matches the now‑published Microsoft example for Bedrock passthrough (including the SHA‑256 body hash and percent‑encoding of the modelId segment), but it also makes the critical host explicit so you do not accidentally sign with the APIM host. The service string for signing remains bedrock, per AWS. - https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api, and https://gist.github.com/JGalego/3dd5b4bec19544453c3031ddc4a36a3b for your reference.
In addition, the below here is APIM Frontend in Python requests (APIM handles SigV4; you just send to APIM):
import requests, json
apim = "https://<your-apim>.azure-api.net/bedrock"
model_id = "us.anthropic.claude-3-5-haiku-20241022-v1:0" # Bedrock full model ID
url = f"{apim}/model/{model_id}/converse"
payload = {
"messages":[{"role":"user","content":[{"text":"Hello world"}]}],
"inferenceConfig":{"maxTokens":64}
}
headers = {
"Ocp-Apim-Subscription-Key": "<your-sub-key>",
"Content-Type": "application/json"
}
r = requests.post(url, headers=headers, data=json.dumps(payload))
print(r.status_code, r.text)
The Microsoft sample also shows how to call APIM from an AWS SDK client by disabling its own signing via a custom HTTP client factory; use theirs if you prefer an SDK route. - https://learn.microsoft.com/en-us/azure/api-management/amazon-bedrock-passthrough-llm-api
I hope this is helpful! Do not hesitate to let me know if you have any other questions or clarifications.
Please don't forget to close up the thread here by upvoting and accept it as an answer if it is helpful.