You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Only reproducible when querying an Amazon OpenSearchServerless collection with a private network policy (accessibly only via VPCE). I am unable to reproduce on a collection with a public network policy.
The following requirements apply when signing requests to OpenSearch Serverless collections when you construct HTTP requests with another clients.
You must specify the service name as aoss.
The x-amz-content-sha256 header is required for all AWS Signature Version 4 requests. It provides a hash of the request payload. If there's a request payload, set the value to its Secure Hash Algorithm (SHA) cryptographic hash (SHA256). If there's no request payload, set the value to e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, which is the hash of an empty string.
Create an API Gateway with a Lambda proxy integration.
Via the Lambda Service Console:
Create a Lambda function
Via the OpenSearch Service Console:
Create an Amazon OpenSearchServerless (AOSS) collection. When creating...
Configure the collection with a "private" network policy.
Create VPCE to access the private instance. Ensure the VPCE and Lambda are in the same subnet.
Add an Inbound / Outbound rule for the Lambda SG
Add an Inbound / Outbound rule for the AOSS SG
Configure the collection with a data access policy. Add the Lambda execution role to the policy.
Sample Lambda Code
using System.Net;using System.Net.Security;using System.Security.Authentication;using System.Text;using System.Text.Json;using Amazon.Lambda.APIGatewayEvents;using Amazon.Lambda.Core;using Amazon.Lambda.Serialization.SystemTextJson;using Amazon.Runtime;using AWS.Lambda.Powertools.Logging;using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Logging;using EnvironmentVariables = Insights.Shared.Variables.EnvironmentVariables;using HttpMethod = System.Net.Http.HttpMethod;// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.[assembly:LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]namespace Functions;//You can simplify to just query aoss directly in the lambda. This function is a generic proxy.publicclassFunction{privateconststringServiceKey="awsService";privateconststringServiceUriKey="awsUri";privatestaticreadonlystringRegion="us-west-2";/// <summary>/// Filter out query parameters used to configure the proxy request as these may cause the receiving service to fail. e.g. OpenSearch returns BadRequest when there are excess parameters/// </summary>privatestaticreadonlyIReadOnlySet<string>ReservedQueryParameterKeys=newHashSet<string>([ServiceKey, ServiceUriKey]);/// <summary>/// Headers which should not be forwarded./// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection/// See https://www.rfc-editor.org/rfc/rfc7230#section-6.1/// </summary>privatestaticreadonlyIReadOnlySet<string>HopByHopHeaders=newHashSet<string>([//sigv4"authorization",//standard"host","connection","keep-alive","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]);privatestaticreadonlyTimeSpanTimeout= TimeSpan.FromSeconds(30);privatestatic HttpClient CreateHttpClient(){varhandler=new SocketsHttpHandler
{PooledConnectionLifetime=Timeout*2,ConnectTimeout=Timeout,SslOptions=new SslClientAuthenticationOptions
{EnabledSslProtocols= SslProtocols.None
}};returnnew HttpClient(handler){Timeout=Timeout};}publicHttpClientHttpClient{get;init;}= CreateHttpClient();publicILoggerLogger{get;init;}= AWS.Lambda.Powertools.Logging.Logger.Create<Function>();publicFunc<AWSCredentials> CredentialProvider {get;init;}= FallbackCredentialsFactory.GetCredentials;//Absurd levels of indirection to mock things outpublicFunc<HttpClient,HttpRequestMessage,string,string,AWSCredentials,Task<HttpResponseMessage>> SignAndSendAsyncFunc {get;init;}= SignAndSendAsync;[Logging(LogEvent =true)]publicasyncTask<APIGatewayHttpApiV2ProxyResponse>HandleRequestAsync(APIGatewayHttpApiV2ProxyRequestrequest,ILambdaContextcontext){try{usingHttpResponseMessageresponse=awaitthis.SendRequestAsync(request);stringcontent=await response.Content.ReadAsStringAsync();if(response.IsSuccessStatusCode ==false){stringrequestContent=await(response.RequestMessage?.Content?.ReadAsStringAsync()?? Task.FromResult(string.Empty));this.Logger.LogError("'{Request}' with '{RequestContent}' failed with '{Response}' '{Content}'", response.RequestMessage, requestContent, response, content);}returnnew APIGatewayHttpApiV2ProxyResponse
{StatusCode=(int) response.StatusCode,Headers= response.Headers.ToDictionary(kvp => kvp.Key,kvp =>string.Join(",", kvp.Value)),Body=content,IsBase64Encoded=false};}catch(HttpRequestExceptione){this.Logger.LogError(e,"Failed to proxy request to AWS service");returnnew APIGatewayHttpApiV2ProxyResponse
{StatusCode=(int)(e.StatusCode ?? HttpStatusCode.InternalServerError),Headers=newDictionary<string,string>{{"Content-Type","application/json; charset=utf-8"}},Body= JsonSerializer.Serialize(new ErrorResponse(e.StatusCode ?? HttpStatusCode.InternalServerError, e.Message, request.RequestContext.RequestId),new JsonSerializerOptions().ConfigureDefaults()),IsBase64Encoded=false};}catch(Exceptione){this.Logger.LogError(e,"Failed to proxy request to AWS service");returnnew APIGatewayHttpApiV2ProxyResponse
{StatusCode= StatusCodes.Status400BadRequest,Headers=newDictionary<string,string>{{"Content-Type","application/json; charset=utf-8"}},Body= JsonSerializer.Serialize(new ErrorResponse(HttpStatusCode.BadRequest, e.Message, request.RequestContext.RequestId),new JsonSerializerOptions().ConfigureDefaults()),IsBase64Encoded=false};}}privateasyncTask<HttpResponseMessage>SendRequestAsync(APIGatewayHttpApiV2ProxyRequestapiRequest){varheaders= apiRequest.Headers
.Select(kvp =>newKeyValuePair<string,string>(kvp.Key.ToLowerInvariant(), kvp.Value))//Remove 'x-amz' and 'authorization' SigV4 headers as these will cause the signing to fail.Where(kvp => kvp.Key.StartsWith("x-amz")==false).Where(kvp => HopByHopHeaders.Contains(kvp.Key)==false).GroupBy(kvp => kvp.Key).ToDictionary(group => group.Key,group =>string.Join(',', group.Select(kvp => kvp.Value)));if(string.IsNullOrWhiteSpace(apiRequest.RequestContext.Http.Method)){thrownew InvalidOperationException("APIGatewayHttpApiV2ProxyRequest.RequestContext.Http.Method is empty / not set");}varmethod= HttpMethod.Parse(apiRequest.RequestContext.Http.Method);boolisContentTypeRequired= HttpMethod.Put.Equals(method)|| HttpMethod.Post.Equals(method)|| HttpMethod.Patch.Equals(method);if(headers.TryGetValue("content-type",outstring? contentType)==false||string.IsNullOrWhiteSpace(contentType)){if(isContentTypeRequired){thrownew InvalidOperationException($"APIGatewayProxyRequest.Header 'content-type' is empty / not set and required for method '{method}'");}}if(apiRequest.PathParameters.TryGetValue("proxy",outstring? path)==false||string.IsNullOrWhiteSpace(path)){thrownew InvalidOperationException("APIGatewayHttpApiV2ProxyRequest.PathParameters 'proxy' is empty / not set. Cannot forward request");}if(apiRequest.QueryStringParameters.TryGetValue(ServiceKey,outstring? service)==false||string.IsNullOrWhiteSpace(service)){thrownew InvalidOperationException($"APIGatewayHttpApiV2ProxyRequest.QueryStringParameters '{ServiceKey}' is empty / not set. Cannot forward request. Expect service authorization token. "+$"e.g. 'aoss'. See https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html");}if(apiRequest.QueryStringParameters.TryGetValue(ServiceUriKey,outstring? baseUri)==false||string.IsNullOrWhiteSpace(baseUri)){thrownew InvalidOperationException($"APIGatewayHttpApiV2ProxyRequest.QueryStringParameters '{ServiceUriKey}' is empty / not set. Cannot forward request. Expect absolute Uri. e.g. 'https://domain-id.us-west-2.aoss.amazonaws.com'");}varparameterList= apiRequest.QueryStringParameters
.Where(kvp => ReservedQueryParameterKeys.Contains(kvp.Key)==false).Select(kvp =>$"{kvp.Key}={kvp.Value}").ToList();stringparameters= parameterList.Count ==0?string.Empty :"?"+string.Join("&", parameterList);//We need to add '/' because the proxy path parameter does not include itvarrequestUri=new Uri($"{baseUri}/{path}{parameters}", UriKind.Absolute);stringbody=(apiRequest.IsBase64Encoded ? Encoding.UTF8.GetString(Convert.FromBase64String(apiRequest.Body)): apiRequest.Body)??string.Empty;varrequest=new HttpRequestMessage(method, requestUri){Content=new StringContent(body)};foreach(KeyValuePair<string,string> header in headers){//Content headers must go into the content headers else it throwsif(header.Key.StartsWith("content")){_= request.Content.Headers.Remove(header.Key);_= request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);}else{//Remove request defaults because .NET hates humanity_= request.Headers.Remove(header.Key);_= request.Headers.TryAddWithoutValidation(header.Key, header.Value);}}this.Logger.LogInformation("Sending {HttpRequestMessage} to {Service} at {Region}", request, service, Region);returnawaitthis.SignAndSendAsyncFunc.Invoke(this.HttpClient, request, Region, service,this.CredentialProvider.Invoke());}privatestaticasyncTask<HttpResponseMessage>SignAndSendAsync(HttpClientclient,HttpRequestMessagerequest,stringregion,stringservice,AWSCredentialscredentials)=>await client.SendAsync(request, region, service, credentials);privatesealedrecordErrorResponse(HttpStatusCodeStatusCode,stringMessage,stringRequestId);}
Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.
Describe the bug
Only reproducible when querying an Amazon OpenSearchServerless collection with a private network policy (accessibly only via VPCE). I am unable to reproduce on a collection with a public network policy.
--
Based on the documentation,
https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-clients.html#serverless-signing
Expected Behavior
X-Amz-Content-SHA256 should be present when service identifier is 'aoss'
Current Behavior
X-Amz-Content-SHA256 should be present when querying Amazon OpenSearch Serverless
Reproduction Steps
Infra
Client -> APIG -> Lambda -> VPCE -> AOSS (private)
Via the APIGateway Service Console:
Via the Lambda Service Console:
Via the OpenSearch Service Console:
Sample Lambda Code
Possible Solution
Update https://github.com/FantasticFiasco/aws-signature-version-4/blob/master/src/Private/Signer.cs#L138
Additional Information/Context
Context
I am attempting to proxy requests through API Gateway to our private AOSS collection via VPCE.
Infra
Client -> APIG -> Lambda -> VPCE -> AOSS
Security
1. Allows Task Ingress / Egress
2. Allows Lambda Ingress / Egress
3. Allows AOSS Ingress / Egress
AWS .NET SDK and/or Package version used
3.7.303.38
Targeted .NET Platform
.NET 8
Operating System and version
MacOS 13.6.1 (22G313), Amazon Linux
The text was updated successfully, but these errors were encountered: