From 0f502f5ef6f822a7bcdc83067497be7a8b29a183 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 26 Apr 2026 14:41:12 +0100 Subject: [PATCH 1/2] Add API Gateway endpoint security Co-authored-by: Arthur Frade de Araujo --- docs/events/apigateway.md | 61 +++++++ docs/guides/serverless.yml.md | 7 + .../events/api-gateway/lib/rest-api.js | 10 ++ lib/plugins/aws/provider.js | 10 ++ .../events/api-gateway/lib/rest-api.test.js | 153 ++++++++++++++++++ types/index.d.ts | 4 + 6 files changed, 245 insertions(+) diff --git a/docs/events/apigateway.md b/docs/events/apigateway.md index 4a5dfdd7c..cb2cd8393 100644 --- a/docs/events/apigateway.md +++ b/docs/events/apigateway.md @@ -24,6 +24,7 @@ Summary: - [Catching Exceptions In Your Lambda Function](#catching-exceptions-in-your-lambda-function) - [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api) - [Configuring endpoint types](#configuring-endpoint-types) + - [Security Policy](#security-policy) - [Request Parameters](#request-parameters) - [Request Schema Validators](#request-schema-validators) - [Setting source of API key for metering requests](#setting-source-of-api-key-for-metering-requests) @@ -752,6 +753,66 @@ provider: - vpce-456 ``` +### Security Policy + +You can configure the TLS security policy for the generated API Gateway REST API by setting `provider.apiGateway.endpoint.securityPolicy`. This maps to the [`SecurityPolicy`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-apigateway-restapi.html#cfn-apigateway-restapi-securitypolicy) property of the `AWS::ApiGateway::RestApi` CloudFormation resource. + +This applies only to REST APIs generated from `http` events. It does not apply to HTTP APIs generated from `httpApi` events, and it does not apply when you import an external REST API with `provider.apiGateway.restApiId`. + +For the default edge-optimized REST API endpoint, use an edge-compatible policy: + +```yml +provider: + name: aws + apiGateway: + endpoint: + securityPolicy: SecurityPolicy_TLS13_2025_EDGE + accessMode: strict +functions: + hello: + events: + - http: + path: user/create + method: get +``` + +For Regional or private REST APIs, set `provider.endpointType` and use a policy supported by that endpoint type: + +```yml +provider: + name: aws + endpointType: REGIONAL + apiGateway: + endpoint: + securityPolicy: SecurityPolicy_TLS13_1_3_2025_09 + accessMode: basic +functions: + hello: + events: + - http: + path: user/create + method: get +``` + +AWS treats policies that start with `SecurityPolicy_` as enhanced security policies. When using an enhanced policy, you must also set `provider.apiGateway.endpoint.accessMode` to `basic` or `strict`. + +`strict` adds additional host and endpoint-type checks. AWS recommends migrating by first using `basic`, validating traffic, and then switching to `strict`. + +When changing an API from an enhanced policy back to a legacy policy, AWS requires endpoint access mode to be unset with an empty string: + +```yml +provider: + name: aws + apiGateway: + endpoint: + securityPolicy: TLS_1_0 + accessMode: '' +``` + +For normal legacy policy usage, omit `accessMode`. + +Supported security policies differ by endpoint type. See the [AWS supported security policies documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-security-policies-list.html) for the current policy list. + ### Request Parameters To pass optional and required parameters to your functions, so you can use them in API Gateway tests and SDK generation, marking them as `true` will make them required, `false` will make them optional. diff --git a/docs/guides/serverless.yml.md b/docs/guides/serverless.yml.md index 2fe5ad42d..76887a0a7 100644 --- a/docs/guides/serverless.yml.md +++ b/docs/guides/serverless.yml.md @@ -256,6 +256,13 @@ provider: websocketApiId: xxxx # Disable the default 'execute-api' HTTP endpoint (default: false) disableDefaultEndpoint: true + # Optional REST API endpoint security settings + endpoint: + # TLS security policy for the generated REST API + securityPolicy: SecurityPolicy_TLS13_2025_EDGE + # Endpoint access mode for enhanced security policies: basic or strict + # Use "" only to unset access mode when reverting from enhanced to legacy policies + accessMode: strict # Source of API key for usage plan: HEADER or AUTHORIZER apiKeySourceType: HEADER # List of API keys for the REST API diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js index c61f693e9..6fdf6c2ca 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js @@ -5,6 +5,7 @@ const ServerlessError = require('../../../../../../../serverless-error'); module.exports = { compileRestApi() { const apiGateway = this.serverless.service.provider.apiGateway || {}; + const endpoint = apiGateway.endpoint || {}; // immediately return if we're using an external REST API id if (apiGateway.restApiId) { @@ -53,6 +54,15 @@ module.exports = { EndpointConfiguration, }; + if (endpoint.securityPolicy) { + properties.SecurityPolicy = endpoint.securityPolicy; + } + + if (endpoint.accessMode != null) { + properties.EndpointAccessMode = + endpoint.accessMode === '' ? '' : endpoint.accessMode.toUpperCase(); + } + // Tags if (this.serverless.service.provider.tags) { properties.Tags = Object.entries(this.serverless.service.provider.tags).map( diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index c919e8425..09f9a1e44 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -841,6 +841,16 @@ class AwsProvider { }, description: { type: 'string' }, disableDefaultEndpoint: { type: 'boolean' }, + endpoint: { + type: 'object', + properties: { + securityPolicy: { type: 'string' }, + accessMode: { + anyOf: [...['BASIC', 'STRICT'].map(caseInsensitive), { const: '' }], + }, + }, + additionalProperties: false, + }, metrics: { type: 'boolean' }, minimumCompressionSize: { type: 'integer', minimum: 0, maximum: 10485760 }, resourcePolicy: { $ref: '#/definitions/awsResourcePolicyStatements' }, diff --git a/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.test.js b/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.test.js index 5eae5bbda..690aae9f6 100644 --- a/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.test.js +++ b/test/unit/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.test.js @@ -184,6 +184,71 @@ describe('#compileRestApi()', () => { }); }); + it('should support `provider.apiGateway.endpoint.securityPolicy`', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + endpoint: { + securityPolicy: 'TLS_1_0', + }, + }; + + awsCompileApigEvents.compileRestApi(); + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayRestApi; + + expect(resource.Properties.SecurityPolicy).to.equal('TLS_1_0'); + expect(resource.Properties.EndpointAccessMode).to.equal(undefined); + }); + + it('should support `provider.apiGateway.endpoint.accessMode`', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + endpoint: { + securityPolicy: 'SecurityPolicy_TLS13_2025_EDGE', + accessMode: 'basic', + }, + }; + + awsCompileApigEvents.compileRestApi(); + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayRestApi; + + expect(resource.Properties.SecurityPolicy).to.equal('SecurityPolicy_TLS13_2025_EDGE'); + expect(resource.Properties.EndpointAccessMode).to.equal('BASIC'); + }); + + it('should support strict endpoint access mode', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + endpoint: { + securityPolicy: 'SecurityPolicy_TLS13_2025_EDGE', + accessMode: 'STRICT', + }, + }; + + awsCompileApigEvents.compileRestApi(); + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayRestApi; + + expect(resource.Properties.EndpointAccessMode).to.equal('STRICT'); + }); + + it('should preserve empty `provider.apiGateway.endpoint.accessMode`', () => { + awsCompileApigEvents.serverless.service.provider.apiGateway = { + endpoint: { + securityPolicy: 'TLS_1_0', + accessMode: '', + }, + }; + + awsCompileApigEvents.compileRestApi(); + const resource = + awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ApiGatewayRestApi; + + expect(resource.Properties).to.have.property('EndpointAccessMode', ''); + }); + it('should throw error if endpointType property is not PRIVATE and vpcEndpointIds property is [id1]', () => { awsCompileApigEvents.serverless.service.provider.endpointType = 'Testing'; awsCompileApigEvents.serverless.service.provider.vpcEndpointIds = ['id1']; @@ -219,6 +284,94 @@ describe('lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js' expect(resource.Properties.DisableExecuteApiEndpoint).to.equal(true); }); + it('should support `provider.apiGateway.endpoint.securityPolicy` and `accessMode`', async () => { + const { cfTemplate } = await runServerless({ + fixture: 'api-gateway', + command: 'package', + configExt: { + provider: { + apiGateway: { + endpoint: { + securityPolicy: 'SecurityPolicy_TLS13_2025_EDGE', + accessMode: 'strict', + }, + }, + }, + }, + }); + const resource = cfTemplate.Resources.ApiGatewayRestApi; + + expect(resource.Properties.SecurityPolicy).to.equal('SecurityPolicy_TLS13_2025_EDGE'); + expect(resource.Properties.EndpointAccessMode).to.equal('STRICT'); + }); + + it('should support empty `provider.apiGateway.endpoint.accessMode`', async () => { + const { cfTemplate } = await runServerless({ + fixture: 'api-gateway', + command: 'package', + configExt: { + provider: { + apiGateway: { + endpoint: { + securityPolicy: 'TLS_1_0', + accessMode: '', + }, + }, + }, + }, + }); + const resource = cfTemplate.Resources.ApiGatewayRestApi; + + expect(resource.Properties.SecurityPolicy).to.equal('TLS_1_0'); + expect(resource.Properties).to.have.property('EndpointAccessMode', ''); + }); + + it('should reject invalid `provider.apiGateway.endpoint.accessMode`', async () => { + let error; + try { + await runServerless({ + fixture: 'api-gateway', + command: 'package', + configExt: { + provider: { + apiGateway: { + endpoint: { + accessMode: 'invalid', + }, + }, + }, + }, + }); + } catch (caughtError) { + error = caughtError; + } + + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + + it('should reject unknown `provider.apiGateway.endpoint` properties', async () => { + let error; + try { + await runServerless({ + fixture: 'api-gateway', + command: 'package', + configExt: { + provider: { + apiGateway: { + endpoint: { + unknownProperty: true, + }, + }, + }, + }, + }); + } catch (caughtError) { + error = caughtError; + } + + expect(error).to.have.property('code', 'INVALID_NON_SCHEMA_COMPLIANT_CONFIGURATION'); + }); + it('should support `provider.apiGateway.resourcePolicy[].Principal.AWS with Fn::If`', async () => { const { cfTemplate } = await runServerless({ fixture: 'api-gateway', diff --git a/types/index.d.ts b/types/index.d.ts index e348d8f5f..a4eb20a54 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -821,6 +821,10 @@ export interface AWS { binaryMediaTypes?: string[]; description?: string; disableDefaultEndpoint?: boolean; + endpoint?: { + securityPolicy?: string; + accessMode?: string; + }; metrics?: boolean; minimumCompressionSize?: number; resourcePolicy?: AwsResourcePolicyStatements; From 7c0ff3bf8790152d43a45282f200644495177e8b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 26 Apr 2026 14:50:29 +0100 Subject: [PATCH 2/2] Simplify endpoint access mode compile --- .../aws/package/compile/events/api-gateway/lib/rest-api.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js b/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js index 6fdf6c2ca..cfa6d4404 100644 --- a/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js +++ b/lib/plugins/aws/package/compile/events/api-gateway/lib/rest-api.js @@ -59,8 +59,7 @@ module.exports = { } if (endpoint.accessMode != null) { - properties.EndpointAccessMode = - endpoint.accessMode === '' ? '' : endpoint.accessMode.toUpperCase(); + properties.EndpointAccessMode = endpoint.accessMode.toUpperCase(); } // Tags