diff --git a/infra/lib/compute-stack.ts b/infra/lib/compute-stack.ts index 4cca936..450050f 100644 --- a/infra/lib/compute-stack.ts +++ b/infra/lib/compute-stack.ts @@ -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'],