Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mutual TLS authentication for regional APIs #505

Merged
merged 4 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ route53:ChangeResourceRecordSets hostedzone/{HostedZoneId}
route53:GetHostedZone *
route53:ListResourceRecordSets *
iam:CreateServiceLinkedRole arn:aws:iam::${AWS::AccountId}: role/aws-service-role/ops.apigateway.amazonaws.com/AWSServiceRoleForAPIGateway
s3:ListBucket *
s3:GetObject *
```
### CloudFormation
Alternatively you can generate an least privileged IAM Managed Policy for deployment with this:
Expand Down Expand Up @@ -153,6 +155,8 @@ custom:
| route53Region | `(none)` | Region to send Route53 services requests to (only applicable if also using route53Profile option) |
| endpointType | edge | Defines the endpoint type, accepts `regional` or `edge`. |
| apiType | rest | Defines the api type, accepts `rest`, `http` or `websocket`. |
| tlsTruststoreUri | `undefined` | An Amazon S3 url that specifies the truststore for mutual TLS authentication, for example `s3://bucket-name/key-name`. The truststore can contain certificates from public or private certificate authorities. Be aware mutual TLS is only available for `regional` APIs. |
| tlsTruststoreVersion | `undefined` | The version of the S3 object that contains your truststore. To specify a version, you must have versioning enabled for the S3 bucket. |
| hostedZoneId | | If hostedZoneId is set the route53 record set will be created in the matching zone, otherwise the hosted zone will be figured out from the domainName (hosted zone with matching domain). |
| hostedZonePrivate | | If hostedZonePrivate is set to `true` then only private hosted zones will be used for route 53 records. If it is set to `false` then only public hosted zones will be used for route53 records. Setting this parameter is specially useful if you have multiple hosted zones with the same domain name (e.g. a public and a private one) |
| enabled | true | Sometimes there are stages for which is not desired to have custom domain names. This flag allows the developer to disable the plugin for such cases. Accepts either `boolean` or `string` values and defaults to `true` for backwards compatibility. |
Expand Down
23 changes: 22 additions & 1 deletion src/aws/api-gateway-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class APIGatewayWrapper {

// For EDGE domain name or TLS 1.0, create with APIGateway (v1)
const isEdgeType = domain.endpointType === Globals.endpointTypes.edge;
const hasMutualTls = !!domain.tlsTruststoreUri;
if (isEdgeType || domain.securityPolicy === "TLS_1_0") {
// Set up parameters
const params = {
Expand All @@ -41,6 +42,16 @@ class APIGatewayWrapper {
tags: providerTags,
};

if (!isEdgeType && hasMutualTls) {
params.mutualTlsAuthentication = {
truststoreUri: domain.tlsTruststoreUri
};

if (domain.tlsTruststoreVersion) {
params.truststoreVersion = domain.tlsTruststoreVersion;
}
}

// Make API call to create domain
try {
// Creating EDGE domain so use APIGateway (v1) service
Expand All @@ -51,7 +62,7 @@ class APIGatewayWrapper {
}

} else { // For Regional domain name create with ApiGatewayV2
const params = {
const params: any = {
DomainName: domain.givenDomainName,
DomainNameConfigurations: [{
CertificateArn: domain.certificateArn,
Expand All @@ -61,6 +72,16 @@ class APIGatewayWrapper {
Tags: providerTags
};

if (!isEdgeType && hasMutualTls) {
params.MutualTlsAuthentication = {
TruststoreUri: domain.tlsTruststoreUri
};

if (domain.tlsTruststoreVersion) {
params.TruststoreVersion = domain.tlsTruststoreVersion;
}
}

// Make API call to create domain
try {
// Creating Regional domain so use ApiGatewayV2
Expand Down
45 changes: 45 additions & 0 deletions src/aws/s3-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {S3} from "aws-sdk";
import {throttledCall} from "../utils";
import DomainConfig = require("../domain-config");
import Globals from "../globals";

class S3Wrapper {
public s3: S3;

constructor(credentials: any) {
this.s3 = new S3(credentials);
}

/**
* * Checks whether the Mutual TLS certificate exists in S3 or not
*/
public async assertTlsCertObjectExists(domain: DomainConfig): Promise<void> {
try {
const {Bucket, Key} = this.extractBucketAndKey(domain.tlsTruststoreUri);
const params: S3.Types.HeadObjectRequest = {Bucket, Key};

if (domain.tlsTruststoreVersion) {
params.VersionId = domain.tlsTruststoreVersion;
}

await throttledCall(this.s3, "headObject", params);
} catch (err) {
if (err.code !== "AccessDenied") {
throw Error(`Could not head S3 object at ${domain.tlsTruststoreUri}.\n${err.message}`);
}

Globals.logWarning(`Unable to check existance of S3 object at ${domain.tlsTruststoreUri} due to\n${err.message}`);
}
}

/**
* * Extracts Bucket and Key from the given s3 uri
*/
private extractBucketAndKey(uri: string): { Bucket: string; Key: string } {
const { hostname, pathname } = new URL(uri);

return { Bucket: hostname, Key: pathname.substring(1) };
}
}

export = S3Wrapper;
21 changes: 21 additions & 0 deletions src/domain-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class DomainConfig {
public route53Region: string | undefined;
public endpointType: string | undefined;
public apiType: string | undefined;
public tlsTruststoreUri: string | undefined;
public tlsTruststoreVersion: string | undefined;
public hostedZoneId: string | undefined;
public hostedZonePrivate: boolean | undefined;
public enabled: boolean | string | undefined;
Expand Down Expand Up @@ -78,6 +80,17 @@ class DomainConfig {
}
this.apiType = apiTypeToUse;

const isEdgeType = this.endpointType === Globals.endpointTypes.edge;
const hasMutualTls = !!config.tlsTruststoreUri;
if (isEdgeType && hasMutualTls) {
throw new Error(`${this.endpointType} APIs do not support mutual TLS, remove tlsTruststoreUri or change to a regional API.`);
}
if (config.tlsTruststoreUri) {
this.validateS3Uri(config.tlsTruststoreUri);
}
this.tlsTruststoreUri = config.tlsTruststoreUri;
this.tlsTruststoreVersion = config.tlsTruststoreVersion;

const securityPolicyDefault = config.securityPolicy || Globals.tlsVersions.tls_1_2;
const tlsVersionToUse = Globals.tlsVersions[securityPolicyDefault.toLowerCase()];
if (!tlsVersionToUse) {
Expand Down Expand Up @@ -106,6 +119,14 @@ class DomainConfig {
healthCheckId: config.route53Params?.healthCheckId
}
}

private validateS3Uri(uri: string): void {
const { protocol, pathname } = new URL(uri);

if (protocol !== "s3:" && !pathname.substring(1).includes("/")) {
throw new Error(`${uri} is not a valid s3 uri, try something like s3://bucket-name/key-name.`);
}
}
}

export = DomainConfig;
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ACMWrapper = require("./aws/acm-wrapper");
import APIGatewayWrapper = require("./aws/api-gateway-wrapper");
import CloudFormationWrapper = require("./aws/cloud-formation-wrapper");
import Route53Wrapper = require("./aws/route53-wrapper");
import S3Wrapper = require("./aws/s3-wrapper");
import DomainConfig = require("./domain-config");
import Globals from "./globals";
import {CustomDomain, ServerlessInstance, ServerlessOptions, ServerlessUtils} from "./types";
Expand All @@ -14,6 +15,7 @@ class ServerlessCustomDomain {
// AWS SDK resources
public apiGatewayWrapper: APIGatewayWrapper;
public cloudFormationWrapper: CloudFormationWrapper;
public s3Wrapper: S3Wrapper;

// Serverless specific properties
public serverless: ServerlessInstance;
Expand Down Expand Up @@ -168,6 +170,7 @@ class ServerlessCustomDomain {

this.apiGatewayWrapper = new APIGatewayWrapper(credentials);
this.cloudFormationWrapper = new CloudFormationWrapper(credentials);
this.s3Wrapper = new S3Wrapper(credentials);
}

/**
Expand All @@ -190,6 +193,10 @@ class ServerlessCustomDomain {
const route53 = new Route53Wrapper(domain.route53Profile, domain.route53Region);
const acm = new ACMWrapper(domain.endpointType);
try {
if (domain.tlsTruststoreUri) {
await this.s3Wrapper.assertTlsCertObjectExists(domain);
}

if (!domain.domainInfo) {
if (!domain.certificateArn) {
const searchName = domain.certificateName || domain.givenDomainName;
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface CustomDomain { // tslint:disable-line
route53Region: string | undefined;
endpointType: string | undefined;
apiType: string | undefined;
tlsTruststoreUri: string | undefined;
tlsTruststoreVersion: string | undefined;
hostedZoneId: string | undefined;
hostedZonePrivate: boolean | undefined;
enabled: boolean | string | undefined;
Expand Down Expand Up @@ -52,6 +54,7 @@ export interface ServerlessInstance { // tslint:disable-line
Route53: any,
CloudFormation: any,
ACM: any,
S3: any,
config: {
httpOptions: HTTPOptions,
update(toUpdate: object): void,
Expand Down
Loading