fix: LAM-C1/INF-H1 require IAM auth on Function URL, INF-H3 restrict OpenSearch to VPC

LAM-C1/INF-H1: Change Function URL authType from NONE to AWS_IAM and
grant invokeUrl permission to all four caller Lambdas (suggestions,
pdf-extract, pdf-generate, library-ingest). Lambda HTTP clients will
need SigV4 signing as a follow-up.

INF-H3: Create OpenSearch Serverless VPC endpoint in private subnets
and update network policy from AllowFromPublic to SourceVPCEs, removing
public internet access to the vector search collection.
This commit is contained in:
Adam Moussa 2026-05-27 17:21:02 -04:00
parent 67b4732395
commit a74ac4945f

View file

@ -58,6 +58,15 @@ export class ComputeStack extends cdk.Stack {
}),
});
// Fix: INF-H3 — restrict OpenSearch Serverless to VPC (was AllowFromPublic: true).
// Create a VPC endpoint so Lambdas in private subnets can reach the collection.
const ossVpcEndpoint = new opensearchserverless.CfnVpcEndpoint(this, 'OssVpcEndpoint', {
name: 'proposal-system-kb-vpce',
vpcId: props.vpc.vpcId,
subnetIds: props.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds,
securityGroupIds: [props.lambdaSecurityGroup.securityGroupId],
});
const ossNetworkPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OssNetworkPolicy', {
name: 'proposal-system-kb-net',
type: 'network',
@ -65,9 +74,11 @@ export class ComputeStack extends cdk.Stack {
Rules: [
{ ResourceType: 'collection', Resource: ['collection/proposal-system-kb'] },
],
AllowFromPublic: true,
AllowFromPublic: false,
SourceVPCEs: [ossVpcEndpoint.attrId],
}]),
});
ossNetworkPolicy.addDependency(ossVpcEndpoint);
const ossCollection = new opensearchserverless.CfnCollection(this, 'OssCollection', {
name: 'proposal-system-kb',
@ -237,9 +248,12 @@ export class ComputeStack extends cdk.Stack {
resources: [props.userPool.userPoolArn],
}));
// Function URL for internal Lambda-to-API calls (bypasses API Gateway JWT authorizer)
// Fix: LAM-C1/INF-H1 — require IAM auth on Function URL (was authType NONE).
// NOTE: Lambda HTTP clients (suggestions, pdf-extract, pdf-generate, library-ingest)
// must use SigV4 signing when calling this URL. The API key header alone is no longer
// sufficient for authentication at the transport layer.
const apiFunctionUrl = apiFunction.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
authType: lambda.FunctionUrlAuthType.AWS_IAM,
});
// API Gateway HTTP API
@ -338,6 +352,8 @@ export class ComputeStack extends cdk.Stack {
});
internalApiKeySecret.grantRead(suggestionsFunction);
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
apiFunctionUrl.grantInvokeUrl(suggestionsFunction);
suggestionsFunction.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-*`],
@ -369,6 +385,8 @@ export class ComputeStack extends cdk.Stack {
});
internalApiKeySecret.grantRead(pdfExtractFunction);
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
apiFunctionUrl.grantInvokeUrl(pdfExtractFunction);
props.uploadsBucket.grantRead(pdfExtractFunction);
pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock:InvokeModel'],
@ -396,6 +414,8 @@ export class ComputeStack extends cdk.Stack {
});
internalApiKeySecret.grantRead(pdfGenerateFunction);
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
apiFunctionUrl.grantInvokeUrl(pdfGenerateFunction);
props.generatedBucket.grantWrite(pdfGenerateFunction);
// Python Lambda: Library Ingest
@ -421,6 +441,8 @@ export class ComputeStack extends cdk.Stack {
});
internalApiKeySecret.grantRead(libraryIngestFunction);
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
apiFunctionUrl.grantInvokeUrl(libraryIngestFunction);
props.libraryBucket.grantWrite(libraryIngestFunction);
libraryIngestFunction.addToRolePolicy(new iam.PolicyStatement({
actions: ['bedrock:StartIngestionJob'],