├── cdk ├── .npmignore ├── .gitignore ├── jest.config.js ├── cdk.json ├── bin │ ├── environment.template.ts │ └── cdk.ts ├── package.json ├── lib │ ├── cdk_nag_exceptions.ts │ ├── cross_account_provider_stack.ts │ ├── cross_account_peering_stack.ts │ ├── examples │ │ └── minimum_viable_test_environment.ts │ ├── consumer_allow_list_stack.ts │ └── cross_account_peering_provider.ts ├── test │ └── cdk.test.ts ├── tsconfig.json └── package-lock.json ├── img ├── end-state.png ├── initial.png └── initiation.png ├── .gitignore ├── CODE_OF_CONDUCT.md ├── custom_resource_handler ├── tsconfig.json ├── tests │ ├── vars.ts │ ├── handler.ts │ ├── ddb_lookup.ts │ ├── cross_account_service.ts │ └── vpc_control.ts ├── package.json └── src-ts │ ├── ddb_lookup.ts │ ├── logger.ts │ ├── index.ts │ ├── vpc_control.ts │ └── cross_account_service.ts ├── LICENSE ├── CONTRIBUTING.md └── README.md /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /img/end-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/vpc-cross-account-peering-automation/HEAD/img/end-state.png -------------------------------------------------------------------------------- /img/initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/vpc-cross-account-peering-automation/HEAD/img/initial.png -------------------------------------------------------------------------------- /img/initiation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/vpc-cross-account-peering-automation/HEAD/img/initiation.png -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | dist 3 | .env* 4 | *.map 5 | .npm 6 | cdk/bin/environment.ts 7 | .scannerwork 8 | sonar-project.properties 9 | interactive.ts -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "tsconfig.json", 12 | "package*.json", 13 | "yarn.lock", 14 | "node_modules", 15 | "test" 16 | ] 17 | }, 18 | "context": { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /custom_resource_handler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "esnext", 5 | "noImplicitAny": true, 6 | "preserveConstEnums": true, 7 | "outDir": "dist/src", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "lib": ["esnext.intl"], 12 | }, 13 | "include": ["src-ts/**/*", "tests/interactive.ts"], 14 | "exclude": ["node_modules", "**/*.spec.ts"] 15 | } -------------------------------------------------------------------------------- /cdk/bin/environment.template.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Provider setup 5 | export const PROVIDER_ACCOUNT = "012345678910"; 6 | export const CONSUMER_ACCOUNT = "012345678910"; 7 | 8 | // Consumer setup 9 | export const PROVIDER_VPC_ID = "vpc-xyz"; 10 | export const CONSUMER_CIDR = "192.168.1.0/24"; 11 | export const SERVICE_TOKEN = "arn:aws:lambda:....."; 12 | export const REGISTRATION_CODE = "testRegId"; 13 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^17.0.35", 15 | "aws-cdk-lib": "^2.49.0", 16 | "constructs": "^10.0.0", 17 | "typescript": "~3.9.0" 18 | }, 19 | "dependencies": { 20 | "cdk-nag": "^2.14.29", 21 | "constructs": "^10.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/lib/cdk_nag_exceptions.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | export const iam5ConditionSafeguard = { 5 | id: "AwsSolutions-IAM5", 6 | reason: "Remediated through condition.", 7 | }; 8 | 9 | export const iam4CDKProviderManaged = { 10 | id: "AwsSolutions-IAM4", 11 | reason: "CDK managed resource", 12 | }; 13 | 14 | export const l1CKDKProviderManaged = { 15 | id: "AwsSolutions-L1", 16 | reason: "CDK managed resource", 17 | }; -------------------------------------------------------------------------------- /cdk/test/cdk.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 5 | import * as cdk from '@aws-cdk/core'; 6 | import * as Cdk from '../lib/cdk-stack'; 7 | 8 | test('Empty Stack', () => { 9 | const app = new cdk.App(); 10 | // WHEN 11 | const stack = new Cdk.CdkStack(app, 'MyTestStack'); 12 | // THEN 13 | expectCDK(stack).to(matchTemplate({ 14 | "Resources": {} 15 | }, MatchStyle.EXACT)) 16 | }); 17 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /custom_resource_handler/tests/vars.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | export const TEST_PEER_ID = "pcx-018e70ea767738dea"; 5 | export const TEST_PROVIDER_VPC_ID = "vpc-ff7a5278"; 6 | export const TEST_CIDR = "10.255.255.0/24" 7 | export const TEST_ROUTE_TABLE = "rtb-0325d38c7edefffff" 8 | export const TEST_ROUTE_TABLES = ["rtb-0325d38c7edefffff", "rtb-0325d38c7edeffffa"]; 9 | export const TEST_SERVICE_CIDR = "10.1.0.0/24"; 10 | export const TEST_PORT = 443; 11 | export const TEST_PROTOCOL = "tcp"; 12 | export const TEST_SECURITY_GROUP_ID = "sg-abcd"; 13 | export const TEST_ACCOUNT_ID = "1234567890"; 14 | export const TEST_REGISTRATION_ID = "testReg"; 15 | export const TEST_TABLE_NAME = "PARTNER_DB"; 16 | export const TEST_SERVICE_TOKEN = "tata" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /cdk/lib/cross_account_provider_stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Stack, App, StackProps, aws_dynamodb as ddb } from "aws-cdk-lib"; 5 | import { NagSuppressions } from "cdk-nag"; 6 | import { iam5ConditionSafeguard, iam4CDKProviderManaged, l1CKDKProviderManaged } from "./cdk_nag_exceptions"; 7 | import { CrossAccountPeeringProvider } from "./cross_account_peering_provider"; 8 | 9 | interface CrossAccountDeploymentStackProps extends StackProps { 10 | vpcId: string; 11 | cidr: string; 12 | securityGroupId: string; 13 | routeTableIds: string; 14 | modifierTag: string; 15 | allowedAccounts?: string[]; 16 | } 17 | 18 | export class CrossAccountProviderStack extends Stack { 19 | serviceToken: string; 20 | connectionTable: ddb.Table; 21 | constructor(scope: App, id: string, props: CrossAccountDeploymentStackProps) { 22 | super(scope, id, props); 23 | 24 | const crossAccountProvider = new CrossAccountPeeringProvider(this, "provider", { 25 | vpcId: props.vpcId, 26 | cidr: props.cidr, 27 | securityGroupId: props.securityGroupId, 28 | routeTables: props.routeTableIds, 29 | allowedAccounts: props.allowedAccounts, 30 | modifierTag: props.modifierTag, 31 | }); 32 | 33 | this.serviceToken = crossAccountProvider.serviceToken; 34 | this.connectionTable = crossAccountProvider.connectionTable 35 | 36 | NagSuppressions.addResourceSuppressions( 37 | crossAccountProvider.providerRole, 38 | [iam5ConditionSafeguard] 39 | ) 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /custom_resource_handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cross_account_peering", 3 | "version": "1.0.0", 4 | "description": "Handle cross_account peering setup and teardown", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "env-cmd -f .env.test mocha --inspect=0.0.0.0:8080 --watch --extensions ts --watch-files src-ts,tests -r ts-node/register 'tests/**/*.ts' --watch --exclude tests/interactive.ts", 8 | "coverage": "nyc --reporter=text --reporter=html env-cmd -f .env.test mocha -r ts-node/register 'tests/**/*.ts'", 9 | "compile": "tsc", 10 | "build": "npm run compile && cp package.json dist/src && cd dist/src && npm install --production", 11 | "package": "npm run build && cd dist/src && zip -r ../code.zip ./" 12 | }, 13 | "dependencies": { 14 | "@aws-sdk/client-dynamodb": "^3.95.0", 15 | "@aws-sdk/client-ec2": "^3.98.0", 16 | "@aws-sdk/credential-providers": "^3.95.0", 17 | "winston": "^3.7.2" 18 | }, 19 | "devDependencies": { 20 | "@types/aws-lambda": "^8.10.88", 21 | "@types/chai": "^4.3.0", 22 | "@types/chai-as-promised": "^7.1.4", 23 | "@types/mocha": "^9.1.1", 24 | "@types/node": "^17.0.0", 25 | "@types/sinon": "^10.0.6", 26 | "@types/sinon-chai": "^3.2.6", 27 | "aws-sdk": "^2.1046.0", 28 | "aws-sdk-client-mock": "^0.6.2", 29 | "aws-sdk-mock": "^5.5.0", 30 | "chai": "^4.3.4", 31 | "chai-as-promised": "^7.1.1", 32 | "env-cmd": "^10.1.0", 33 | "faker": "^5.5.3", 34 | "mocha": "^9.1.3", 35 | "node-inspect": "^2.0.0", 36 | "nyc": "^15.1.0", 37 | "sinon": "^12.0.1", 38 | "sinon-chai": "^3.7.0", 39 | "ts-node": "^10.8.0", 40 | "typescript": "^4.5.4" 41 | }, 42 | "author": "Severin Gassauer-Fleissner sev@amazon.com", 43 | "license": "MIT-0" 44 | } 45 | -------------------------------------------------------------------------------- /custom_resource_handler/src-ts/ddb_lookup.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; 5 | import { logger } from "./logger"; 6 | 7 | export interface DDBLookupProps { 8 | tableName: string, 9 | client: DynamoDBClient, 10 | } 11 | 12 | export interface LookupPartnerProps { 13 | accountId: string, 14 | registrationId: string, 15 | } 16 | 17 | export class DDBLookup { 18 | tableName: string; 19 | client: DynamoDBClient; 20 | 21 | constructor(props: DDBLookupProps) { 22 | this.client = props.client; 23 | this.tableName = props.tableName; 24 | } 25 | 26 | public async lookupPartner(props: LookupPartnerProps) { 27 | const getItemCommand = new GetItemCommand({ 28 | TableName: this.tableName, 29 | Key: { 30 | pk: {S: props.accountId}, 31 | sk: {S: props.registrationId}, 32 | } 33 | }) 34 | 35 | logger.debug(`Looking up in DDB ${this.tableName} ${props.accountId} ${props.registrationId}`); 36 | const res = await this.client.send(getItemCommand); 37 | 38 | logger.debug(`Returned ${JSON.stringify(res)}`) 39 | 40 | if(!res.Item) { 41 | logger.error(`Failed to lookup entry for ${props.accountId}`); 42 | throw new Error("No valid registration found"); 43 | } 44 | 45 | logger.info(`Successful lookup for ${props.accountId}`); 46 | const parsedPeering = { 47 | pk: res.Item.pk.S, 48 | sk: res.Item.sk.S, 49 | cidr: res.Item.cidr.S, 50 | port: res.Item.port.N, 51 | protocol: res.Item.protocol.S, 52 | } 53 | return parsedPeering; 54 | } 55 | } -------------------------------------------------------------------------------- /custom_resource_handler/src-ts/logger.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import path = require("path"); 5 | import winston = require("winston"); 6 | 7 | const LOG_LEVEL = process.env.NODE_ENV === "prod" ? "info" : "debug"; 8 | 9 | const localTransport = new winston.transports.Console({ 10 | format: winston.format.combine( 11 | winston.format.colorize(), 12 | winston.format.splat(), 13 | winston.format.printf( 14 | (info) => `${info.timestamp} ${info.level} : ${info.message}` 15 | ) 16 | ), 17 | level: LOG_LEVEL, 18 | }); 19 | 20 | const lambdaTransport = new winston.transports.Console({ 21 | // 22 | // Possible to override the log method of the 23 | // internal transports of winston@3.0.0. 24 | // 25 | log(info, callback) { 26 | setImmediate(() => this.emit("logged", info)); 27 | 28 | if (this.stderrLevels[info["level"]]) { 29 | console.error(info["message"]); 30 | 31 | if (callback) { 32 | callback(); 33 | } 34 | return; 35 | } 36 | 37 | console.log(info["message"]); 38 | 39 | if (callback) { 40 | callback(); 41 | } 42 | }, 43 | }); 44 | 45 | // https://github.com/winstonjs/winston/issues/1305 46 | const winstonTransports: Array = []; 47 | if (process.env.LOCAL_RUN) { 48 | console.debug(`Using local logger transport`) 49 | winstonTransports.push(localTransport); 50 | } else { 51 | console.debug(`Using lambda logger transport`) 52 | winstonTransports.push(lambdaTransport); 53 | } 54 | 55 | export const logger = winston.createLogger({ 56 | level: LOG_LEVEL, 57 | format: winston.format.combine( 58 | //winston.format.label({ label: path.basename(process.mainModule.filename) }), 59 | winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 60 | // Format the metadata object 61 | winston.format.metadata({ 62 | fillExcept: ["message", "level", "timestamp", "label"], 63 | }) 64 | ), 65 | defaultMeta: { service: "cross-account-peering-service" }, 66 | transports: winstonTransports, 67 | }); -------------------------------------------------------------------------------- /custom_resource_handler/tests/handler.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as sinon from "sinon"; 5 | import * as sinonChai from "sinon-chai"; 6 | import { CrossAccountPeeringService } from "../src-ts/cross_account_service"; 7 | import { onEvent } from "../src-ts/index"; 8 | import { TEST_ACCOUNT_ID, TEST_PEER_ID, TEST_REGISTRATION_ID, TEST_ROUTE_TABLES, TEST_SECURITY_GROUP_ID, TEST_SERVICE_CIDR, TEST_SERVICE_TOKEN } from "./vars"; 9 | 10 | var chai = require("chai"); 11 | var chaiAsPromised = require("chai-as-promised"); 12 | 13 | chai.use(chaiAsPromised); 14 | chai.should(); 15 | chai.use(sinonChai); 16 | var expect = chai.expect; // we are using the "expect" style of Chai 17 | 18 | const sandbox = sinon.createSandbox(); 19 | 20 | describe("Handler", () => { 21 | 22 | afterEach( () => { 23 | sandbox.restore(); 24 | }) 25 | 26 | it("Should call create with the correct parameters", async () => { 27 | //@ts-ignore 28 | const stubbedCrossAccountPeeringService = sandbox.stub(CrossAccountPeeringService.prototype, "handleCreate").callsFake( () => { return } ); 29 | sandbox.stub(process.env, "ROUTE_TABLES").value(TEST_ROUTE_TABLES.toString()) 30 | sandbox.stub(process.env, "CIDR").value(TEST_SERVICE_CIDR) 31 | sandbox.stub(process.env, "SECURITY_GROUP").value(TEST_SECURITY_GROUP_ID) 32 | 33 | 34 | const res = await onEvent({ 35 | "RequestType": "Create", 36 | //"ResponseURL": "http://pre-signed-S3-url-for-response", 37 | "StackId": `arn:aws:cloudformation:ap-southeast-1:${TEST_ACCOUNT_ID}:stack/MyStack/guid`, 38 | //"RequestId": "unique id for this create request", 39 | //"ResourceType": "Custom::TestResource", 40 | //"LogicalResourceId": "MyTestResource", 41 | "ResourceProperties": { 42 | "PeeringId": TEST_PEER_ID, 43 | "RegistrationId": TEST_REGISTRATION_ID, 44 | "ServiceToken": TEST_SERVICE_TOKEN, 45 | } 46 | }); 47 | 48 | stubbedCrossAccountPeeringService.should.have.been.calledWith({ 49 | accountId: TEST_ACCOUNT_ID, 50 | registrationId: TEST_REGISTRATION_ID, 51 | peeringId: TEST_PEER_ID, 52 | }); 53 | 54 | }); 55 | }) -------------------------------------------------------------------------------- /cdk/lib/cross_account_peering_stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Stack, App, CustomResource } from "aws-cdk-lib"; 5 | import { 6 | AwsCustomResource, 7 | PhysicalResourceId, 8 | PhysicalResourceIdReference, 9 | AwsCustomResourcePolicy, 10 | } from "aws-cdk-lib/custom-resources"; 11 | import { NagSuppressions } from "cdk-nag"; 12 | import { 13 | iam5ConditionSafeguard, 14 | l1CKDKProviderManaged, 15 | iam4CDKProviderManaged, 16 | } from "./cdk_nag_exceptions"; 17 | import { ConsumerTestStackProps } from "./examples/minimum_viable_test_environment"; 18 | 19 | interface CrossAccountPeeringProps extends ConsumerTestStackProps { 20 | serviceToken: string; 21 | registrationCode: string; 22 | vpcId: string; 23 | peerVpcId: string; 24 | providerAccount: string; 25 | } 26 | 27 | export class CrossAccountPeeringStack extends Stack { 28 | constructor(scope: App, id: string, props: CrossAccountPeeringProps) { 29 | super(scope, id, props); 30 | 31 | const peeringConnection = new AwsCustomResource(this, "requestPeering", { 32 | onCreate: { 33 | service: "EC2", 34 | action: "createVpcPeeringConnection", 35 | parameters: { 36 | PeerOwnerId: props.providerAccount, 37 | PeerVpcId: props.peerVpcId, 38 | VpcId: props.vpcId, 39 | }, 40 | physicalResourceId: PhysicalResourceId.fromResponse( 41 | "VpcPeeringConnection.VpcPeeringConnectionId" 42 | ), 43 | }, 44 | onDelete: { 45 | service: "EC2", 46 | action: "deleteVpcPeeringConnection", 47 | parameters: { 48 | VpcPeeringConnectionId: new PhysicalResourceIdReference(), 49 | }, 50 | }, 51 | installLatestAwsSdk: false, 52 | policy: AwsCustomResourcePolicy.fromSdkCalls({ 53 | resources: AwsCustomResourcePolicy.ANY_RESOURCE, 54 | }), 55 | }); 56 | 57 | // This invokes the custom resource in the provider account and is the only bit that has to be done by CloudFormation from a consumer's side 58 | const crossAccountPeering = new CustomResource(this, "testCR", { 59 | serviceToken: props.serviceToken, 60 | properties: { 61 | RegistrationId: props.registrationCode, 62 | PeeringId: peeringConnection.getResponseField( 63 | "VpcPeeringConnection.VpcPeeringConnectionId" 64 | ), 65 | }, 66 | }); 67 | 68 | NagSuppressions.addResourceSuppressionsByPath( 69 | this, 70 | "/crossAccountPeering/requestPeering/CustomResourcePolicy/Resource", 71 | [iam5ConditionSafeguard] 72 | ); 73 | 74 | NagSuppressions.addResourceSuppressionsByPath( 75 | this, 76 | "/crossAccountPeering/AWS679f53fac002430cb0da5b7982bd2287/Resource", 77 | [l1CKDKProviderManaged] 78 | ); 79 | 80 | NagSuppressions.addResourceSuppressionsByPath( 81 | this, 82 | "/crossAccountPeering/AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/Resource", 83 | [iam4CDKProviderManaged] 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cdk/lib/examples/minimum_viable_test_environment.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Stack, App, StackProps, aws_logs, aws_iam } from "aws-cdk-lib"; 5 | import { Vpc, FlowLogDestination, FlowLogTrafficType, SecurityGroup, SubnetType } from "aws-cdk-lib/aws-ec2"; 6 | 7 | export interface ConsumerTestStackProps extends StackProps { 8 | cidr: string; 9 | } 10 | 11 | export class ProviderExampleEnvironmentStack extends Stack { 12 | vpcId: string; 13 | cidr: string; 14 | sgId: string; 15 | routeTableIds: string; 16 | constructor(scope: App, id: string, props?: StackProps) { 17 | super(scope, id, props); 18 | 19 | const logGroup = new aws_logs.LogGroup(this, "MyCustomLogGroup"); 20 | 21 | const role = new aws_iam.Role(this, "MyCustomRole", { 22 | assumedBy: new aws_iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"), 23 | }); 24 | 25 | const testVpc = new Vpc(this, "testVpc", { 26 | subnetConfiguration: [ 27 | { 28 | cidrMask: 27, 29 | name: 'provider', 30 | subnetType: SubnetType.PRIVATE_ISOLATED, 31 | }, 32 | ], 33 | flowLogs: { 34 | test: { 35 | destination: FlowLogDestination.toCloudWatchLogs(logGroup, role), 36 | trafficType: FlowLogTrafficType.ALL, 37 | }, 38 | }, 39 | 40 | }); 41 | 42 | const testSg = new SecurityGroup(this, "testSg", { 43 | vpc: testVpc, 44 | }); 45 | 46 | const routeTableIds = [ 47 | ... 48 | new Set( 49 | testVpc.isolatedSubnets.map((subnet) => { 50 | return subnet.routeTable.routeTableId; 51 | }) 52 | 53 | ), 54 | ]; 55 | const routeTableIdsString = routeTableIds.join(","); 56 | 57 | this.sgId = testSg.securityGroupId; 58 | this.vpcId = testVpc.vpcId; 59 | this.cidr = testVpc.vpcCidrBlock; 60 | this.routeTableIds = routeTableIdsString; 61 | } 62 | } 63 | 64 | export class ConsumerExampleEnvironmentStack extends Stack { 65 | vpcId: string; 66 | constructor(scope: App, id: string, props: ConsumerTestStackProps) { 67 | super(scope, id, props); 68 | 69 | const logGroup = new aws_logs.LogGroup(this, "CroasAccountLogGroup"); 70 | 71 | const role = new aws_iam.Role(this, "CrossAccountFlowLogRole", { 72 | assumedBy: new aws_iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"), 73 | }); 74 | 75 | const testVpc = new Vpc(this, "testVpc", { 76 | flowLogs: { 77 | test: { 78 | destination: FlowLogDestination.toCloudWatchLogs(logGroup, role), 79 | trafficType: FlowLogTrafficType.ALL, 80 | }, 81 | }, 82 | cidr: props.cidr, 83 | subnetConfiguration: [ 84 | { 85 | cidrMask: 27, 86 | name: "test", 87 | subnetType: SubnetType.PRIVATE_ISOLATED, 88 | }, 89 | ], 90 | }); 91 | 92 | this.vpcId = testVpc.vpcId; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /custom_resource_handler/tests/ddb_lookup.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as sinon from "sinon"; 5 | import * as sinonChai from "sinon-chai"; 6 | import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; 7 | import { mockClient } from "aws-sdk-client-mock"; 8 | import { 9 | TEST_ACCOUNT_ID, 10 | TEST_CIDR, 11 | TEST_PORT, 12 | TEST_PROTOCOL, 13 | TEST_REGISTRATION_ID, 14 | TEST_TABLE_NAME, 15 | } from "./vars"; 16 | import { DDBLookup } from "../src-ts/ddb_lookup"; 17 | 18 | var chai = require("chai"); 19 | var chaiAsPromised = require("chai-as-promised"); 20 | 21 | chai.use(sinonChai); 22 | chai.use(chaiAsPromised); 23 | chai.should(); 24 | 25 | var expect = chai.expect; // we are using the "expect" style of Chai 26 | 27 | const sandbox = sinon.createSandbox(); 28 | 29 | const ddbMock = mockClient(DynamoDBClient); 30 | 31 | let ddbLookupUnderTest = sandbox.spy( 32 | new DDBLookup({ tableName: TEST_TABLE_NAME, client: new DynamoDBClient({}) }) 33 | ); 34 | 35 | describe("Lookup input", () => { 36 | beforeEach(() => { 37 | ddbMock.reset(); 38 | }); 39 | 40 | it("Should return matching record if the RegistrationID and account id pair exists", async () => { 41 | ddbMock 42 | .on(GetItemCommand) 43 | .resolves({ Item: undefined }) 44 | .on(GetItemCommand, { 45 | TableName: TEST_TABLE_NAME, 46 | Key: { 47 | pk: { S: TEST_ACCOUNT_ID }, 48 | sk: { S: TEST_REGISTRATION_ID }, 49 | }, 50 | }) 51 | .resolves({ 52 | Item: { 53 | //@ts-ignore 54 | pk: {S: TEST_ACCOUNT_ID}, 55 | //@ts-ignore 56 | sk: {S: TEST_REGISTRATION_ID}, 57 | //@ts-ignore 58 | cidr: {S: TEST_CIDR}, 59 | //@ts-ignore 60 | port: {N: TEST_PORT.toString()}, 61 | //@ts-ignore 62 | protocol: {S: TEST_PROTOCOL}, 63 | }, 64 | }); 65 | const res = await ddbLookupUnderTest.lookupPartner({ 66 | registrationId: TEST_REGISTRATION_ID, 67 | accountId: TEST_ACCOUNT_ID, 68 | }); 69 | expect(res.pk).to.equal(TEST_ACCOUNT_ID); 70 | expect(res.sk).to.equal(TEST_REGISTRATION_ID); 71 | }); 72 | 73 | it("Should throw error if pair doesn't exist", async () => { 74 | ddbMock 75 | .on(GetItemCommand) 76 | .resolves({ Item: undefined }) 77 | .on(GetItemCommand, { 78 | TableName: TEST_TABLE_NAME, 79 | Key: { 80 | pk: { S: TEST_ACCOUNT_ID }, 81 | sk: { S: TEST_REGISTRATION_ID }, 82 | }, 83 | }) 84 | .resolves({ 85 | Item: { 86 | //@ts-ignore 87 | pk: TEST_ACCOUNT_ID, 88 | //@ts-ignore 89 | sk: TEST_REGISTRATION_ID, 90 | //@ts-ignore 91 | cidr: TEST_CIDR, 92 | //@ts-ignore 93 | port: TEST_PORT.toString(), 94 | //@ts-ignore 95 | protocol: TEST_PROTOCOL, 96 | }, 97 | }); 98 | 99 | return ddbLookupUnderTest.lookupPartner({ 100 | registrationId: "1", 101 | accountId: TEST_ACCOUNT_ID, 102 | }).should.be.rejectedWith("No valid registration found"); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // SPDX-License-Identifier: MIT-0 5 | 6 | import { 7 | App, 8 | Aspects, 9 | Tags, 10 | } from "aws-cdk-lib"; 11 | 12 | import { AwsSolutionsChecks } from "cdk-nag"; 13 | import { 14 | PROVIDER_ACCOUNT, 15 | CONSUMER_ACCOUNT, 16 | PROVIDER_VPC_ID, 17 | CONSUMER_CIDR, 18 | REGISTRATION_CODE, 19 | } from "./environment"; 20 | import { CrossAccountProviderStack } from "../lib/cross_account_provider_stack"; 21 | import { CrossAccountPeeringStack } from "../lib/cross_account_peering_stack"; 22 | import { ProviderExampleEnvironmentStack, ConsumerExampleEnvironmentStack } from "../lib/examples/minimum_viable_test_environment"; 23 | import { ConsumerAllowListStack } from "../lib/consumer_allow_list_stack"; 24 | 25 | const MODIFIER_TAG = "cross_account_modifiable"; 26 | const providerEnvironment = { 27 | account: PROVIDER_ACCOUNT, 28 | }; 29 | 30 | const consumerEnvironment = { 31 | account: CONSUMER_ACCOUNT, 32 | }; 33 | 34 | const app = new App(); 35 | Aspects.of(app).add(new AwsSolutionsChecks()); 36 | 37 | // This sets up a minimum viable test environment for a provider consisting of a VPC and a security group you would use your own VPCs in production 38 | const providerExampleEnvironment = new ProviderExampleEnvironmentStack(app, "providerExampleEnvironment", { 39 | env: providerEnvironment, 40 | }); 41 | 42 | // This sets up a minimum viable test environment for a consumer consisting of a VPC you would use your own VPCs in production 43 | const consumerExampleEnvironment = new ConsumerExampleEnvironmentStack(app, "consumerExampleEnvironment", { 44 | env: consumerEnvironment, 45 | cidr: CONSUMER_CIDR, 46 | }); 47 | 48 | // This marks the resources in the provider example as modifiable by the custom lambda resource 49 | Tags.of(providerExampleEnvironment).add(MODIFIER_TAG, "True"); 50 | 51 | // This deploys the provider VPC automation in the provider account 52 | const providerCrossAccountProvider = new CrossAccountProviderStack(app, "providerStack", { 53 | env: providerEnvironment, 54 | vpcId: providerExampleEnvironment.vpcId, 55 | cidr: providerExampleEnvironment.cidr, 56 | securityGroupId: providerExampleEnvironment.sgId, 57 | routeTableIds: providerExampleEnvironment.routeTableIds, 58 | allowedAccounts: [CONSUMER_ACCOUNT], 59 | modifierTag: MODIFIER_TAG, 60 | }); 61 | 62 | // Use this to maintain the list of allowed connections 63 | const consumerAllowList = new ConsumerAllowListStack(app, "consumerAllowList", { 64 | env: providerEnvironment, 65 | connectionTable: providerCrossAccountProvider.connectionTable, 66 | //allowList: [], 67 | allowList: [ 68 | { 69 | pk: CONSUMER_ACCOUNT, 70 | sk: REGISTRATION_CODE, 71 | cidr: CONSUMER_CIDR, 72 | protocol: "tcp", 73 | port: "443", 74 | }, 75 | // Add more entries here e.g.: 76 | // { 77 | // pk: `${CONSUMER_ACCOUNT}2`, 78 | // sk: REGISTRATION_CODE, 79 | // cidr: CONSUMER_CIDR, 80 | // protocol: "tcp", 81 | // port: "443", 82 | // } 83 | ] 84 | }) 85 | 86 | // This initiates the cross account peering automation request from the consumer side 87 | const crossAccountVPCPeering = new CrossAccountPeeringStack(app, "crossAccountPeering", { 88 | env: consumerEnvironment, 89 | peerVpcId: PROVIDER_VPC_ID, 90 | registrationCode: REGISTRATION_CODE, 91 | serviceToken: providerCrossAccountProvider.serviceToken, 92 | cidr: CONSUMER_CIDR, 93 | vpcId: consumerExampleEnvironment.vpcId, 94 | providerAccount: PROVIDER_ACCOUNT, 95 | }); 96 | -------------------------------------------------------------------------------- /custom_resource_handler/src-ts/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 5 | import { EC2Client } from "@aws-sdk/client-ec2"; 6 | import { DDBLookup } from "./ddb_lookup"; 7 | import { logger } from "./logger"; 8 | import { VpcController } from "./vpc_control"; 9 | import { CrossAccountPeeringService } from "./cross_account_service"; 10 | 11 | interface CrossAccountPeeringEvent { 12 | RequestType: string, 13 | StackId: string, 14 | ResourceProperties: { 15 | ServiceToken?: string, 16 | RegistrationId: string, 17 | PeeringId: string, 18 | } 19 | } 20 | 21 | function validateEvent(event: CrossAccountPeeringEvent) { 22 | const props = event.ResourceProperties; 23 | const isValid = props.RegistrationId && props.PeeringId; 24 | if(!isValid) { 25 | throw new Error("Invalid ResourceProperties"); 26 | } 27 | } 28 | 29 | export async function onEvent(event: CrossAccountPeeringEvent) { 30 | 31 | validateEvent(event); 32 | 33 | logger.debug(`Received Event ${JSON.stringify(event)}`) 34 | const registrationId = event.ResourceProperties.RegistrationId; 35 | // Extract calling account from stack id => "StackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/MyStack/guid" 36 | const requesterAccountId = event.StackId.split(":")[4]; 37 | const peeringId = event.ResourceProperties.PeeringId; 38 | 39 | switch (event.RequestType) { 40 | case 'Create': 41 | case 'Update': 42 | return await handleCreate(requesterAccountId, registrationId, peeringId); 43 | 44 | case 'Delete': 45 | return await handleDelete(requesterAccountId, registrationId, peeringId); 46 | } 47 | } 48 | 49 | const vpcController = new VpcController({ 50 | vpcId: process.env.VPC_ID, 51 | client: new EC2Client({}), 52 | }) 53 | 54 | const partnerLookup = new DDBLookup({ 55 | tableName: process.env.TABLE_NAME, 56 | client: new DynamoDBClient({}), 57 | }) 58 | 59 | const crossAccountPeeringService = new CrossAccountPeeringService({ 60 | vpcController: vpcController, 61 | routeTableIds: process.env.ROUTE_TABLES.split(","), 62 | securityGroupId: process.env.SECURITY_GROUP_ID, 63 | serviceCidr: process.env.CIDR, 64 | ddbLookup: partnerLookup, 65 | }); 66 | 67 | async function handleDelete(requesterAccountId:string, registrationId:string, peeringId:string) { 68 | try { 69 | const creation = await crossAccountPeeringService.handleDelete({ 70 | accountId: requesterAccountId, 71 | registrationId: registrationId, 72 | peeringId: peeringId, 73 | }) 74 | 75 | const response = { 76 | PhysicalResourceId: `${requesterAccountId}-${registrationId}`, 77 | } 78 | 79 | return response; 80 | 81 | } catch (error) { 82 | logger.error(JSON.stringify(error)); 83 | throw new Error("Cross account peering delete failed") 84 | } 85 | } 86 | 87 | async function handleCreate(requesterAccountId:string, registrationId:string, peeringId:string) { 88 | 89 | try { 90 | const creation = await crossAccountPeeringService.handleCreate({ 91 | accountId: requesterAccountId, 92 | registrationId: registrationId, 93 | peeringId: peeringId, 94 | }) 95 | 96 | const response = { 97 | PhysicalResourceId: `${requesterAccountId}-${registrationId}`, 98 | Data: { 99 | ServiceCidr: process.env.CIDR, 100 | } 101 | } 102 | 103 | return response; 104 | 105 | } catch (error) { 106 | logger.error(JSON.stringify(error)); 107 | throw new Error("Cross account peering creation failed") 108 | } 109 | } -------------------------------------------------------------------------------- /custom_resource_handler/tests/cross_account_service.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as sinon from "sinon"; 5 | import * as sinonChai from "sinon-chai"; 6 | 7 | import { VpcController } from "../src-ts/vpc_control"; 8 | import { CrossAccountPeeringService } from "../src-ts/cross_account_service"; 9 | import { 10 | TEST_PEER_ID, 11 | TEST_PROVIDER_VPC_ID, 12 | TEST_CIDR, 13 | TEST_ROUTE_TABLES, 14 | TEST_SERVICE_CIDR, 15 | TEST_PORT, 16 | TEST_PROTOCOL, 17 | TEST_REGISTRATION_ID, 18 | TEST_ACCOUNT_ID, 19 | TEST_TABLE_NAME, 20 | } from "./vars"; 21 | 22 | import { DDBLookup } from "../src-ts/ddb_lookup"; 23 | 24 | var chai = require("chai"); 25 | var chaiAsPromised = require("chai-as-promised"); 26 | 27 | 28 | chai.should(); 29 | chai.use(sinonChai); 30 | chai.use(chaiAsPromised); 31 | var expect = chai.expect; // we are using the "expect" style of Chai 32 | 33 | var sandbox: sinon.SinonSandbox; 34 | 35 | //@ts-ignore 36 | const vpcControlStub = new VpcController({ 37 | vpcId: TEST_PROVIDER_VPC_ID, 38 | //@ts-ignore 39 | client: {}, 40 | }); 41 | 42 | //@ts-ignore 43 | const ddbLookupStub = new DDBLookup({ tableName: TEST_TABLE_NAME, client: {} }); 44 | 45 | const crossAccountPeeringHandlerUnderTest = new CrossAccountPeeringService({ 46 | vpcController: vpcControlStub, 47 | ddbLookup: ddbLookupStub, 48 | serviceCidr: TEST_SERVICE_CIDR, 49 | routeTableIds: TEST_ROUTE_TABLES, 50 | }); 51 | 52 | describe("HandleCreate", () => { 53 | beforeEach(() => { 54 | sandbox = sinon.createSandbox(); 55 | sandbox.spy(crossAccountPeeringHandlerUnderTest); 56 | }); 57 | 58 | afterEach(() => { 59 | sandbox.restore(); 60 | }); 61 | 62 | it("Should initiatie end to end if the accountId and registration are valid", async () => { 63 | 64 | //sandbox.stub(vpcControlStub); 65 | sandbox.stub(vpcControlStub).lookupVpcPeeringConnectionCidr.resolves(TEST_CIDR); 66 | sandbox.stub(ddbLookupStub).lookupPartner.resolves({ 67 | //@ts-ignore 68 | pk: TEST_ACCOUNT_ID, 69 | //@ts-ignore 70 | sk: TEST_REGISTRATION_ID, 71 | //@ts-ignore 72 | cidr: TEST_CIDR, 73 | //@ts-ignore 74 | port: TEST_PORT, 75 | }); 76 | const res = await crossAccountPeeringHandlerUnderTest.handleCreate({ 77 | registrationId: TEST_REGISTRATION_ID, 78 | accountId: TEST_ACCOUNT_ID, 79 | peeringId: TEST_PEER_ID, 80 | }); 81 | 82 | expect(crossAccountPeeringHandlerUnderTest.establishEndToEndConnectivity).to.be 83 | .calledOnce; 84 | }); 85 | 86 | 87 | it("Should not initiatie end to end if the accountId and registration are valid and the requested CIDR is inappropriate", async () => { 88 | sandbox.stub(vpcControlStub).lookupVpcPeeringConnectionCidr.resolves("1.1.1.1/8"); 89 | 90 | sandbox.stub(ddbLookupStub).lookupPartner.resolves({ 91 | //@ts-ignore 92 | pk: TEST_ACCOUNT_ID, 93 | //@ts-ignore 94 | sk: TEST_REGISTRATION_ID, 95 | //@ts-ignore 96 | cidr: TEST_CIDR, 97 | //@ts-ignore 98 | port: TEST_PORT, 99 | }); 100 | 101 | await expect(crossAccountPeeringHandlerUnderTest.handleCreate({ 102 | registrationId: TEST_REGISTRATION_ID, 103 | accountId: TEST_ACCOUNT_ID, 104 | peeringId: TEST_PEER_ID, 105 | })).to.eventually.be.rejectedWith(Error); 106 | 107 | 108 | }); 109 | }); 110 | 111 | describe("Establish end-to-end", () => { 112 | beforeEach(() => { 113 | sandbox = sinon.createSandbox(); 114 | sandbox.spy(crossAccountPeeringHandlerUnderTest); 115 | }); 116 | 117 | afterEach(() => { 118 | sandbox.restore(); 119 | }); 120 | 121 | it("should accept the peering set the routes and permit the ingress", async () => { 122 | sandbox.stub(vpcControlStub); 123 | const res = await crossAccountPeeringHandlerUnderTest.establishEndToEndConnectivity({ 124 | cidr: TEST_CIDR, 125 | peeringId: TEST_PEER_ID, 126 | port: TEST_PORT, 127 | protocol: TEST_PROTOCOL, 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /cdk/lib/consumer_allow_list_stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { Stack, App, CustomResource, StackProps } from "aws-cdk-lib"; 5 | import { Table } from "aws-cdk-lib/aws-dynamodb"; 6 | import { PrincipalBase, Role } from "aws-cdk-lib/aws-iam"; 7 | import { 8 | AwsCustomResource, 9 | PhysicalResourceId, 10 | AwsCustomResourcePolicy, 11 | } from "aws-cdk-lib/custom-resources"; 12 | import { NagSuppressions } from "cdk-nag"; 13 | import * as iam from "aws-cdk-lib/aws-iam"; 14 | import { 15 | iam5ConditionSafeguard, 16 | l1CKDKProviderManaged, 17 | } from "./cdk_nag_exceptions"; 18 | 19 | interface AllowListEntryDefinition { 20 | pk: string; 21 | sk: string; 22 | cidr: string; 23 | protocol: string; 24 | port: string; 25 | } 26 | 27 | interface ConsumerAllowListProps extends StackProps { 28 | connectionTable: Table; 29 | allowList: Array; 30 | } 31 | 32 | export class ConsumerAllowListStack extends Stack { 33 | constructor(scope: App, id: string, props: ConsumerAllowListProps) { 34 | super(scope, id, props); 35 | 36 | const entryConstructs = []; 37 | 38 | const allowListUpdateRole = new Role(this, "allowlistrole", { 39 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com") 40 | }); 41 | 42 | props.connectionTable.grantReadWriteData(allowListUpdateRole); 43 | 44 | 45 | props.allowList.forEach((entry, index) => { 46 | const item = { 47 | pk: { S: entry.pk }, 48 | sk: { S: entry.sk }, 49 | cidr: { S: entry.cidr }, 50 | protocol: { S: entry.protocol }, 51 | port: { N: entry.port }, 52 | }; 53 | 54 | const primaryKey = { 55 | pk: item.pk, 56 | sk: item.sk, 57 | } 58 | 59 | const cidrId = entry.cidr.replace("/", "_").replace(".", "x") 60 | 61 | const id = `al${index}`; 62 | 63 | 64 | let tmp = new AwsCustomResource(this, id, { 65 | onCreate: { 66 | service: "DynamoDB", 67 | action: "putItem", 68 | parameters: { 69 | Item: item, 70 | TableName: props.connectionTable.tableName, 71 | }, 72 | physicalResourceId: PhysicalResourceId.of(id), 73 | }, 74 | onUpdate: { 75 | service: "DynamoDB", 76 | action: "updateItem", 77 | parameters: { 78 | TableName: props.connectionTable.tableName, 79 | Key: primaryKey, 80 | ExpressionAttributeNames: { 81 | "#cidr": "cidr", 82 | "#protocol": "protocol", 83 | "#port": "port", 84 | }, 85 | ExpressionAttributeValues: { 86 | ":c": { 87 | S: entry.cidr, 88 | }, 89 | ":p": { 90 | S: entry.protocol, 91 | }, 92 | ":po": { 93 | N: entry.port, 94 | } 95 | }, 96 | UpdateExpression: "SET #cidr = :c, #protocol = :p, #port = :po", 97 | }, 98 | physicalResourceId: PhysicalResourceId.of(id), 99 | }, 100 | onDelete: { 101 | service: "DynamoDB", 102 | action: "deleteItem", 103 | parameters: { 104 | Key: primaryKey, 105 | TableName: props.connectionTable.tableName, 106 | }, 107 | }, 108 | installLatestAwsSdk: false, 109 | role: allowListUpdateRole, 110 | }); 111 | 112 | entryConstructs.push(tmp); 113 | 114 | }); 115 | 116 | NagSuppressions.addResourceSuppressions( 117 | allowListUpdateRole, 118 | [ 119 | iam5ConditionSafeguard, 120 | l1CKDKProviderManaged, 121 | ], 122 | true 123 | ) 124 | 125 | if ( entryConstructs.length > 0 ) { 126 | NagSuppressions.addResourceSuppressionsByPath( 127 | this, 128 | "/consumerAllowList/AWS679f53fac002430cb0da5b7982bd2287/Resource", 129 | [l1CKDKProviderManaged] 130 | ); 131 | } 132 | 133 | 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /custom_resource_handler/tests/vpc_control.ts: -------------------------------------------------------------------------------- 1 | import { mockClient } from "aws-sdk-client-mock"; 2 | import * as sinon from "sinon"; 3 | import * as sinonChai from "sinon-chai"; 4 | import { 5 | EC2Client, 6 | AcceptVpcPeeringConnectionCommandOutput, 7 | AcceptVpcPeeringConnectionCommand, 8 | CreateRouteCommand, 9 | DeleteRouteCommand, 10 | DeleteVpcPeeringConnectionCommand, 11 | DescribeVpcPeeringConnectionsCommand, 12 | AuthorizeSecurityGroupIngressCommand, 13 | } from "@aws-sdk/client-ec2"; 14 | 15 | import { VpcController } from "../src-ts/vpc_control"; 16 | import { 17 | TEST_PEER_ID, 18 | TEST_PROVIDER_VPC_ID, 19 | TEST_CIDR, 20 | TEST_ROUTE_TABLE, 21 | TEST_PORT, 22 | TEST_PROTOCOL, 23 | TEST_SECURITY_GROUP_ID, 24 | } from "./vars"; 25 | 26 | var chai = require("chai"); 27 | var chaiAsPromised = require("chai-as-promised"); 28 | 29 | chai.use(chaiAsPromised); 30 | chai.should(); 31 | chai.use(sinonChai); 32 | var expect = chai.expect; // we are using the "expect" style of Chai 33 | 34 | const sandbox = sinon.createSandbox(); 35 | 36 | const ec2Mock = mockClient(EC2Client); 37 | 38 | let vpcControlUnderTest = sandbox.spy( 39 | new VpcController({ vpcId: TEST_PROVIDER_VPC_ID, client:new EC2Client({})}) 40 | ); 41 | 42 | describe("PeeringAcceptance", () => { 43 | beforeEach(() => { 44 | ec2Mock.reset(); 45 | }); 46 | 47 | it("Should lookup the peering if it exists", async () => { 48 | ec2Mock.on(DescribeVpcPeeringConnectionsCommand).resolves({ 49 | VpcPeeringConnections: [{ 50 | VpcPeeringConnectionId: TEST_PEER_ID, 51 | RequesterVpcInfo: { 52 | CidrBlock: TEST_CIDR 53 | } 54 | }], 55 | }); 56 | 57 | await vpcControlUnderTest.lookupVpcPeeringConnectionCidr( 58 | TEST_PEER_ID 59 | ); 60 | //expect(res.VpcPeeringConnection.VpcPeeringConnectionId).to.equal(TEST_PEER_ID); 61 | }); 62 | 63 | it("Should accept the peering if it exists", async () => { 64 | ec2Mock.on(AcceptVpcPeeringConnectionCommand).resolves({ 65 | VpcPeeringConnection: { 66 | VpcPeeringConnectionId: TEST_PEER_ID, 67 | }, 68 | }); 69 | await vpcControlUnderTest.acceptPeeringConnection({ 70 | peeringId: TEST_PEER_ID, 71 | }); 72 | //expect(res.VpcPeeringConnection.VpcPeeringConnectionId).to.equal(TEST_PEER_ID); 73 | }); 74 | 75 | it("should delete the peering if it exists", async () => { 76 | ec2Mock.on(DeleteVpcPeeringConnectionCommand).resolves({ 77 | Return: true 78 | }); 79 | ec2Mock.on(DescribeVpcPeeringConnectionsCommand).resolves({ 80 | VpcPeeringConnections: [ 81 | { 82 | VpcPeeringConnectionId: TEST_PEER_ID, 83 | Status: { 84 | Code: "deleted", 85 | } 86 | } 87 | ] 88 | }); 89 | 90 | await vpcControlUnderTest.deletePeeringConnection({ 91 | peeringId: TEST_PEER_ID, 92 | }); 93 | }) 94 | 95 | it("Should not accept the peering if it doesn't exist", async () => { 96 | ec2Mock.on(AcceptVpcPeeringConnectionCommand).rejects(); 97 | const promise = vpcControlUnderTest.acceptPeeringConnection({ 98 | peeringId: TEST_PEER_ID, 99 | }); 100 | expect(promise).to.be.rejected; 101 | }); 102 | }); 103 | 104 | describe("RouteManagement", () => { 105 | beforeEach(() => { 106 | ec2Mock.reset(); 107 | }); 108 | 109 | it("Should add routes to the specified route-tables via the pcx if it exists", async () => { 110 | ec2Mock.on(CreateRouteCommand).resolves({ 111 | Return: true, 112 | }); 113 | const promise = vpcControlUnderTest.createRoute({ 114 | peeringId: TEST_PEER_ID, 115 | cidr: TEST_CIDR, 116 | routeTable: TEST_ROUTE_TABLE, 117 | }); 118 | await promise; 119 | }); 120 | 121 | it("Should remove routes to the specified route-tables if it exists", async () => { 122 | ec2Mock.on(DeleteRouteCommand).resolves({}); 123 | const promise = vpcControlUnderTest.deleteRoute({ 124 | peeringId: TEST_PEER_ID, 125 | cidr: TEST_CIDR, 126 | routeTable: TEST_ROUTE_TABLE, 127 | }); 128 | await promise; 129 | }); 130 | }); 131 | 132 | describe("SecurityGroupUpdate", () => { 133 | beforeEach(() => { 134 | ec2Mock.reset(); 135 | }); 136 | it("should add the CIDR to the security group", async() => { 137 | ec2Mock.on(AuthorizeSecurityGroupIngressCommand).resolves({ 138 | Return: true 139 | }) 140 | 141 | const promise = vpcControlUnderTest.authorizeIngress({ 142 | securityGroupId: TEST_SECURITY_GROUP_ID, 143 | cidr: TEST_CIDR, 144 | port: TEST_PORT, 145 | protocol: TEST_PROTOCOL, 146 | }) 147 | 148 | const res = await promise; 149 | expect(res).to.equal(true); 150 | 151 | }); 152 | }) 153 | -------------------------------------------------------------------------------- /custom_resource_handler/src-ts/vpc_control.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { 5 | EC2Client, 6 | AcceptVpcPeeringConnectionCommand, 7 | waitUntilVpcPeeringConnectionExists, 8 | CreateRouteCommand, 9 | DeleteRouteCommand, 10 | DeleteVpcPeeringConnectionCommand, 11 | waitUntilVpcPeeringConnectionDeleted, 12 | EC2, 13 | CreateVpcPeeringConnectionCommand, 14 | AuthorizeSecurityGroupIngressCommand, 15 | RevokeSecurityGroupIngressCommand, 16 | DescribeVpcPeeringConnectionsCommand, 17 | } from "@aws-sdk/client-ec2"; 18 | import { json } from "stream/consumers"; 19 | import { logger } from "./logger"; 20 | 21 | export interface AcceptPeeringConnectionProps { 22 | peeringId: string; 23 | } 24 | 25 | export interface PeeringConnectionProps { 26 | peerVpc: string; 27 | peerAccount: string; 28 | } 29 | 30 | export interface RouteProps { 31 | peeringId: string; 32 | cidr: string; 33 | routeTable: string; 34 | } 35 | 36 | export interface VpcControllerProps { 37 | vpcId: string; 38 | client: EC2Client; 39 | } 40 | 41 | export interface IngressProps { 42 | cidr: string; 43 | protocol: string; 44 | port: number; 45 | securityGroupId: string; 46 | } 47 | 48 | export class VpcController { 49 | readonly vpcId: string; 50 | readonly ec2Client: EC2Client; 51 | 52 | constructor(props: VpcControllerProps) { 53 | this.vpcId = props.vpcId; 54 | this.ec2Client = props.client; 55 | } 56 | 57 | private async describePeeringConnection(peeringId: string) { 58 | const client = this.ec2Client; 59 | 60 | const describeCommand = new DescribeVpcPeeringConnectionsCommand({ 61 | VpcPeeringConnectionIds: [peeringId], 62 | }); 63 | 64 | const response = await this.ec2Client.send(describeCommand); 65 | logger.debug( 66 | `Peering connection desscribe response ${JSON.stringify(response)}` 67 | ); 68 | 69 | return response.VpcPeeringConnections; 70 | } 71 | 72 | public async lookupVpcPeeringConnectionCidr(peeringId: string) { 73 | const peeringConnections = await this.describePeeringConnection(peeringId); 74 | 75 | if (peeringConnections.length != 1) { 76 | throw new Error(`No peering found for ${peeringId}`); 77 | } 78 | 79 | return peeringConnections[0].RequesterVpcInfo.CidrBlock; 80 | } 81 | 82 | public async createPeeringConnection(props: PeeringConnectionProps) { 83 | const client = this.ec2Client; 84 | 85 | const createCommand = new CreateVpcPeeringConnectionCommand({ 86 | PeerVpcId: props.peerVpc, 87 | PeerOwnerId: props.peerAccount, 88 | VpcId: this.vpcId, 89 | }); 90 | 91 | const response = await this.ec2Client.send(createCommand); 92 | return response.VpcPeeringConnection.VpcPeeringConnectionId; 93 | } 94 | 95 | public async acceptPeeringConnection(props: AcceptPeeringConnectionProps) { 96 | const client = this.ec2Client; 97 | const acceptCommand = new AcceptVpcPeeringConnectionCommand({ 98 | VpcPeeringConnectionId: props.peeringId, 99 | }); 100 | 101 | const response = await this.ec2Client.send(acceptCommand); 102 | logger.debug( 103 | `Peering connection accept response ${JSON.stringify(response)}` 104 | ); 105 | 106 | await waitUntilVpcPeeringConnectionExists( 107 | { 108 | client, 109 | maxWaitTime: 60, 110 | }, 111 | { 112 | VpcPeeringConnectionIds: [props.peeringId], 113 | } 114 | ); 115 | logger.debug(`Peering connection established ${JSON.stringify(response)}`); 116 | 117 | return response; 118 | } 119 | 120 | public async deletePeeringConnection(props: AcceptPeeringConnectionProps) { 121 | const client = this.ec2Client; 122 | const acceptCommand = new DeleteVpcPeeringConnectionCommand({ 123 | VpcPeeringConnectionId: props.peeringId, 124 | }); 125 | 126 | const response = await this.ec2Client.send(acceptCommand); 127 | 128 | await waitUntilVpcPeeringConnectionDeleted( 129 | { 130 | client, 131 | maxWaitTime: 60, 132 | }, 133 | { 134 | VpcPeeringConnectionIds: [props.peeringId], 135 | } 136 | ); 137 | 138 | return response; 139 | } 140 | 141 | public async createRoute(props: RouteProps) { 142 | const client = new EC2Client({}); 143 | const addRouteCommand = new CreateRouteCommand({ 144 | DestinationCidrBlock: props.cidr, 145 | RouteTableId: props.routeTable, 146 | VpcPeeringConnectionId: props.peeringId, 147 | }); 148 | 149 | const res = await client.send(addRouteCommand); 150 | return res; 151 | } 152 | 153 | public async deleteRoute(props: RouteProps) { 154 | const deleteRouteCommand = new DeleteRouteCommand({ 155 | DestinationCidrBlock: props.cidr, 156 | RouteTableId: props.routeTable, 157 | }); 158 | 159 | return await this.ec2Client.send(deleteRouteCommand); 160 | } 161 | 162 | public async authorizeIngress(props: IngressProps) { 163 | const authorizeIngressCommand = new AuthorizeSecurityGroupIngressCommand({ 164 | CidrIp: props.cidr, 165 | IpProtocol: props.protocol, 166 | FromPort: props.port, 167 | ToPort: props.port, 168 | GroupId: props.securityGroupId, 169 | }); 170 | 171 | const res = await this.ec2Client.send(authorizeIngressCommand); 172 | return res.Return; 173 | } 174 | 175 | public async revokeIngress(props: IngressProps) { 176 | const revokeIngressCommand = new RevokeSecurityGroupIngressCommand({ 177 | CidrIp: props.cidr, 178 | IpProtocol: props.protocol, 179 | FromPort: props.port, 180 | ToPort: props.port, 181 | GroupId: props.securityGroupId, 182 | }); 183 | 184 | const res = await this.ec2Client.send(revokeIngressCommand); 185 | return res.Return; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cross-account VPC peering automation 2 | 3 | # Intro 4 | This repo contains the cdk, custom lambda logic, and worked example to setup a cross-account VPC peering without requiring cross-account IAM trust. It does this by using a CloudFormation custom resource provider that executes the remote side of the peering connection including routing and security group configuration updates. 5 | 6 | The cross-account provider setup is provided as a cdk construct and the key part of this repo. You are encouraged to re-use this construct as part of your own, more sophisticated, environments. The stacks provided in this repo exist to provide a worked example deployment for experimentation and demonstration purposes. 7 | 8 | # Sequence of events 9 | 10 | ## Initial setup 11 | 1. Initially the provider deploys the Amazon DynamoDB table and custom resource handler 12 | 2. The provider fills out the table with connectivity information for their consumer 13 | 3. provider shares wit the consumer 14 | * The provider account ID 15 | * The consumer registration code 16 | * The arn of the custom resource provider 17 | 18 | ![initial setup](./img/initial.png) 19 | 20 | ## Consumer invokes peering 21 | 1. consumer sets up a VPC peering with provider 22 | 2. consumer creates a CloudFormation custom resource targeting the pre-communicated custom resource handler arn and passing their registration ID 23 | 3. Custom resource invokes provider's Lambda handler 24 | 4. Custom resource handler looks-up provided information looking for a match for accountID and secret string 25 | 5. If there is a match custom resource handler invokes EC2 APIs to complete the end-to-end connectivity 26 | 27 | ![deployment invocation](./img/initiation.png) 28 | 29 | ## End state 30 | 31 | Once step 5, above, has completed the environment state is as follows 32 | * The VPC peering is accepted and established 33 | * The security group for the provider has been updated to allow ingress from the consumer CIDR on the specified port and protocol 34 | * The routing in the provider account route table is updated to enable return traffic flows to the consumer VPC via the VPC peering connection. 35 | 36 | ![end state](./img/end-state.png) 37 | 38 | 39 | # Getting started with and deploying the example 40 | The repo includes a fully worked example on how to deploy this. In a production use case you would likely use the cross-account cdk construct only instead of the whole example which includes setup of test VPCs 41 | 42 | ## Pre-reqs 43 | 1. Install nodejs v16+ 44 | 2. [Install cdk v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) 45 | 46 | ## Deploy provider stack 47 | As there are dependencies for the example we need to first deploy the provider stack 48 | 49 | To run the example see the `cdk` folder and find the `./bin/environment.template.ts` file. Copy and rename the file to `environment.ts` 50 | 51 | Within this file update the `PROVIDER_ACCOUNT` and `CONSUMER_ACCOUNT` (needed to allow cross-account lambda access) variable to match the account ID you wish to use for the provider. Ignore any other variables for now. 52 | 53 | Bootstrap cdk in the provider account if not done already 54 | `cdk bootstrap` 55 | 56 | You can now deploy the provider test framework using 57 | `cdk deploy providerStack` 58 | 59 | ### Setup a peering entry for the consumer 60 | The deployment of the stack created a DynamoDB table. In order for a consumer to make use of this you need to either manullay create an allow-list entry or use the cdk automation. 61 | 62 | ### Manually create an allow list entry 63 | 64 | Create an entry in this table with the following values (see diagram above for reference) 65 | * pk: the numeric account id of the consumer 66 | * sk: a random (hard to guess) **secret** string that you will communicate to the consumer 67 | * cidr: the allocated IP range for the consumer VPC to peer with - connection attempts with mismatching CIDR will be rejected 68 | * port: a TCP/UDP port which will be granted access from the consumer account CIDR 69 | * protocol: tcp | udp designating the protocol the consumer connection will use 70 | 71 | ### Automate allow list entries using a cdk Stack 72 | The sample code includes the `ConsumerAllowListStack` which can maintain entries in your allow list table. 73 | 74 | Update the `allowList` array to cover all consumer connections you want to allow then deploy the stack 75 | `cdk deploy consumerAllowList` 76 | 77 | ## Deploy the consumer stack 78 | Once you have deployed the provider stack, and updated the allow-list, you can use the outputs from the provider to setup the consumer variables in `environments.ts` based on the decisions and outputs from provider stack. 79 | 80 | Bootstrap cdk in the consumer account if not done already 81 | `cdk bootstrap` 82 | 83 | Then deploy the example peering 84 | `cdk deploy crossAccountPeering` 85 | 86 | You should now see that a VPC peering has been setup and the provider account has updated routing and security to provide full end-to-end connectivity 87 | 88 | To tear down you can simply run `cdk destroy crossAccountPeering`, and then you can re-run the previous step to recreate. 89 | 90 | ## Note: The use of CDK for consumers is optional (but recommended) - How to do it with Cfn or Terraform 91 | As the solution is based on a custom CloudFormation resource this is the only mandatory part for consumers. 92 | Consumers can use Cloudformation templates directly or embedded within terraform to invoke the cross-account automation 93 | 94 | ### CloudFormation and Terraform alternatives 95 | The consumer needs to define the following custom resource. Note that the PeeringId will be known/provided by the consumer once they request the peering to the provider VPC. 96 | 97 | **CloudFormation** 98 | CloudFormation natievly supports CustomResources 99 | 100 | ``` 101 | Type: AWS::CloudFormation::CustomResource 102 | Properties: 103 | ServiceToken: ARN_OF_THE_PROVIDER_LAMBDA_FUNCTION 104 | RegistrationId: SHARED_SECRET_YOU_COMMUNICATED_TO_THE_CONSUMER 105 | PeeringId: ID_OF_THE_PEERING_REQUEST 106 | ``` 107 | 108 | **Terraform** 109 | Terraform can be used to deploy CloudFormation stacks using embedded templates 110 | ``` 111 | resource "aws_cloudformation_stack" "cross-account_peering" { 112 | name = "cross-account-peering" 113 | parameters = { 114 | ServiceToken = as_provided 115 | PeeringId = reference_to_peering_id 116 | RegistrationId = as_provided 117 | } 118 | 119 | template_body = <, 24 | securityGroupId?: string, 25 | } 26 | 27 | export interface HandleCreateProps { 28 | accountId: string, 29 | registrationId: string, 30 | peeringId: string, 31 | } 32 | 33 | export interface VerifyPeeringOwenershipProps { 34 | assignedCidr: string, 35 | peeringId: string 36 | } 37 | 38 | export class CrossAccountPeeringService { 39 | readonly routeTableIds: string[]; 40 | readonly serviceCidr: string; 41 | readonly vpcController: VpcController; 42 | readonly securityGroupId: string; 43 | ddbLookup: DDBLookup; 44 | 45 | constructor(props: CrossAccountPeeringServiceProps) { 46 | this.vpcController = props.vpcController; 47 | this.serviceCidr = props.serviceCidr; 48 | this.routeTableIds = props.routeTableIds 49 | this.securityGroupId = props.securityGroupId 50 | this.ddbLookup = props.ddbLookup 51 | } 52 | 53 | public async handleDelete(props: HandleCreateProps) { 54 | try { 55 | logger.info(`Looking up peering entry for ${props.accountId}`); 56 | const peeringInformation = await this.ddbLookup.lookupPartner({ 57 | accountId: props.accountId, 58 | registrationId: props.registrationId, 59 | }); 60 | 61 | const res = await this.removeEndToEndConnectivity({ 62 | //@ts-ignore 63 | cidr: peeringInformation.cidr, 64 | //@ts-ignore 65 | port: peeringInformation.port, 66 | //@ts-ignore 67 | protocol: peeringInformation.protocol, 68 | peeringId: props.peeringId, 69 | }) 70 | 71 | logger.info(`Found peering entry for ${props.accountId}`) 72 | } catch(error) { 73 | logger.debug(`Error during delete ${error}`); 74 | logger.error(`Error during delete ${props.accountId}`); 75 | throw error; 76 | } 77 | 78 | } 79 | 80 | public async handleCreate(props: HandleCreateProps) { 81 | 82 | try { 83 | logger.info(`Looking up peering entry for ${props.accountId}`); 84 | const peeringInformation = await this.ddbLookup.lookupPartner({ 85 | accountId: props.accountId, 86 | registrationId: props.registrationId, 87 | }); 88 | 89 | logger.info(`Found peering entry for ${props.accountId}`) 90 | await this.verifyOwnedCidr({assignedCidr: peeringInformation.cidr, peeringId: props.peeringId}); 91 | 92 | 93 | const res = await this.establishEndToEndConnectivity({ 94 | //@ts-ignore 95 | cidr: peeringInformation.cidr, 96 | //@ts-ignore 97 | port: peeringInformation.port, 98 | //@ts-ignore 99 | protocol: peeringInformation.protocol, 100 | peeringId: props.peeringId, 101 | }) 102 | 103 | } catch(error) { 104 | logger.debug(`Error during create ${error}`); 105 | logger.error(`Error during create ${props.accountId}`); 106 | throw error; 107 | } 108 | } 109 | 110 | public async verifyOwnedCidr(props: VerifyPeeringOwenershipProps) { 111 | 112 | const requestedCidr = await this.vpcController.lookupVpcPeeringConnectionCidr(props.peeringId); 113 | 114 | if (props.assignedCidr != requestedCidr) { 115 | throw new Error(`Requested CIDR ${requestedCidr} != Assigned CIDR: ${props.assignedCidr}`); 116 | } 117 | } 118 | 119 | public async establishEndToEndConnectivity(props: EstablishEndToEndConnectivityProps) { 120 | try { 121 | logger.info(`Accepting peering ${props.peeringId}`); 122 | await this.vpcController.acceptPeeringConnection({ 123 | peeringId: props.peeringId, 124 | }); 125 | 126 | const vpcModificationPromises = new Array>(); 127 | 128 | this.routeTableIds.forEach(routeTableId => { 129 | logger.debug(`Building route addition promise ${routeTableId} ${props.cidr} ${props.peeringId}`); 130 | const routeAdditionPromise = this.vpcController.createRoute({ 131 | cidr: props.cidr, 132 | routeTable: routeTableId, 133 | peeringId: props.peeringId, 134 | }); 135 | vpcModificationPromises.push(routeAdditionPromise); 136 | }) 137 | 138 | logger.debug(`Building authorization promise ${props.cidr} ${props.protocol} ${props.port} to ${this.securityGroupId}`); 139 | const ingressPromise = this.vpcController.authorizeIngress({ 140 | cidr: props.cidr, 141 | protocol: props.protocol, 142 | port: props.port, 143 | securityGroupId: this.securityGroupId, 144 | }) 145 | vpcModificationPromises.push(ingressPromise); 146 | 147 | logger.info(`Adding routes and security ingress`) 148 | const res = await Promise.all(vpcModificationPromises); 149 | 150 | 151 | } catch (error:any) { 152 | logger.error(`Something went wrong end-to-end ${error}`); 153 | throw error; 154 | } 155 | 156 | } 157 | 158 | 159 | public async removeEndToEndConnectivity(props: EstablishEndToEndConnectivityProps) { 160 | try { 161 | // By default not deleting peering provider side because that breaks the IaC state on the consumer side 162 | if(props.deletePeeringProviderSide) { 163 | logger.info(`Deleting peering ${props.peeringId}`); 164 | await this.vpcController.deletePeeringConnection({ 165 | peeringId: props.peeringId, 166 | }); 167 | } 168 | 169 | const vpcModificationPromises = new Array>(); 170 | 171 | this.routeTableIds.forEach(routeTableId => { 172 | const routeAdditionPromise = this.vpcController.deleteRoute({ 173 | cidr: props.cidr, 174 | routeTable: routeTableId, 175 | peeringId: props.peeringId, 176 | }); 177 | vpcModificationPromises.push(routeAdditionPromise); 178 | }) 179 | 180 | logger.debug(`Revoking ${props.cidr} ${props.protocol} ${props.port} to ${this.securityGroupId}`); 181 | const ingressPromise = this.vpcController.revokeIngress({ 182 | cidr: props.cidr, 183 | protocol: props.protocol, 184 | port: props.port, 185 | securityGroupId: this.securityGroupId, 186 | }) 187 | vpcModificationPromises.push(ingressPromise); 188 | 189 | logger.info(`Removing routes and security ingress`) 190 | const res = await Promise.all(vpcModificationPromises); 191 | 192 | 193 | } catch (error:any) { 194 | logger.error(`Something went wrong removing end-to-end ${error}`); 195 | throw error; 196 | } 197 | 198 | } 199 | } -------------------------------------------------------------------------------- /cdk/lib/cross_account_peering_provider.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { AssetHashType, Stack, StackProps } from "aws-cdk-lib"; 5 | import * as cr from "aws-cdk-lib/custom-resources"; 6 | import * as lambda from "aws-cdk-lib/aws-lambda"; 7 | import * as kms from "aws-cdk-lib/aws-kms"; 8 | import * as ddb from "aws-cdk-lib/aws-dynamodb"; 9 | import * as iam from "aws-cdk-lib/aws-iam"; 10 | import * as path from "path"; 11 | import * as logs from "aws-cdk-lib/aws-logs"; 12 | import { Construct } from "constructs"; 13 | import { NagSuppressions } from "cdk-nag"; 14 | import { iam4CDKProviderManaged, iam5ConditionSafeguard, l1CKDKProviderManaged } from "./cdk_nag_exceptions"; 15 | 16 | export interface CrossAccountPeeringProps extends StackProps { 17 | readonly TableName: string; 18 | } 19 | 20 | export interface CrossAccountPeeringProviderProps { 21 | vpcId: string; 22 | cidr: string; 23 | securityGroupId: string; 24 | routeTables: string; 25 | allowedAccounts?: Array; 26 | modifierTag: string; 27 | 28 | } 29 | 30 | export class CrossAccountPeeringProvider extends Construct { 31 | serviceToken: string; 32 | providerRole: iam.Role; 33 | connectionTable: ddb.Table; 34 | public static getOrCreate(scope: Stack) { 35 | const stack = Stack.of(scope); 36 | const id = 37 | "com.amazonaws.cdk.custom-resources.crossaccount-peering-provider"; 38 | 39 | const singeletonProvider = 40 | //@ts-ignore 41 | (Node.of(stack).tryFindChild(id) as CrossAccountPeeringProvider) || 42 | //@ts-ignore 43 | new CrossAccountPeeringProvider(stack, id); 44 | return singeletonProvider.provider.serviceToken; 45 | } 46 | 47 | public readonly provider: cr.Provider; 48 | 49 | constructor( 50 | scope: Construct, 51 | id: string, 52 | props: CrossAccountPeeringProviderProps 53 | ) { 54 | super(scope, id); 55 | 56 | const partnerKey = new kms.Key(this, "partnerKey", { 57 | enableKeyRotation: true, 58 | }); 59 | 60 | const providerFunctionName = "crossAccountPeeringProvider"; 61 | 62 | this.connectionTable = new ddb.Table(this, "authorizedCrossAccountPeeringPartners", { 63 | partitionKey: { name: "pk", type: ddb.AttributeType.STRING }, 64 | sortKey: { name: "sk", type: ddb.AttributeType.STRING }, 65 | encryption: ddb.TableEncryption.CUSTOMER_MANAGED, 66 | encryptionKey: partnerKey, 67 | }); 68 | 69 | 70 | const onEventCode = lambda.Code.fromAsset( 71 | path.join(__dirname, "../../custom_resource_handler"), 72 | { 73 | assetHashType: AssetHashType.SOURCE, 74 | exclude: ["dist", ".npm"], 75 | bundling: { 76 | image: lambda.Runtime.NODEJS_16_X.bundlingImage, 77 | command: [ 78 | "bash", 79 | "-c", 80 | "mkdir -p .npm && export npm_config_cache=.npm && npm install && npm run build && cd dist/src && cp -aur . /asset-output", 81 | ], 82 | }, 83 | } 84 | ); 85 | 86 | this.providerRole = new iam.Role(this, "providerRole", { 87 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 88 | }); 89 | 90 | const handlerRole = new iam.Role(this, "handlerRole", { 91 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 92 | }); 93 | 94 | // https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/policies/arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole$jsonEditor 95 | const lambdaBasicExecutionPolicy = iam.PolicyDocument.fromJson({ 96 | Version: "2012-10-17", 97 | Statement: [ 98 | { 99 | Effect: "Allow", 100 | Action: [ 101 | "logs:CreateLogGroup", 102 | "logs:CreateLogStream", 103 | "logs:PutLogEvents", 104 | ], 105 | Resource: "*", 106 | }, 107 | ], 108 | }); 109 | 110 | const providerRolePolicy = new iam.ManagedPolicy(this, "crossAccountPeeringLambdaExecutionPolicy", { 111 | document: lambdaBasicExecutionPolicy, 112 | }); 113 | 114 | const ddbGetItemPolicy = new iam.Policy(this, "crossAccountddbDescribePolicy", { 115 | policyName: "ddbDescribe", 116 | }); 117 | ddbGetItemPolicy.addStatements( 118 | new iam.PolicyStatement({ 119 | effect: iam.Effect.ALLOW, 120 | actions: ["dynamodb:GetItem"], 121 | resources: [this.connectionTable.tableArn], 122 | }) 123 | ); 124 | 125 | const vpcModificationPolicy = new iam.Policy(this, "crossAccountVpcModificationPolicy", { 126 | policyName: "crossAccountvpcModificationPolicy", 127 | }); 128 | 129 | vpcModificationPolicy.addStatements( 130 | new iam.PolicyStatement({ 131 | effect: iam.Effect.ALLOW, 132 | actions: [ 133 | "ec2:AuthorizeSecurityGroupIngress", 134 | "ec2:RevokeSecurityGroupIngress", 135 | "ec2:UpdateSecurityGroupRuleDescriptionsIngress", 136 | "ec2:AuthorizeSecurityGroupEgress", 137 | "ec2:RevokeSecurityGroupEgress", 138 | "ec2:UpdateSecurityGroupRuleDescriptionsEgress", 139 | "ec2:ModifySecurityGroupRules", 140 | "ec2:CreateRoute", 141 | "ec2:ModifyRoute", 142 | "ec2:DeleteRoute", 143 | ], 144 | resources: ["*"], 145 | conditions: { 146 | StringEquals: { 147 | [`aws:ResourceTag/${props.modifierTag}`]: "True", 148 | }, 149 | }, 150 | }) 151 | ); 152 | 153 | vpcModificationPolicy.addStatements( 154 | new iam.PolicyStatement({ 155 | effect: iam.Effect.ALLOW, 156 | actions: [ 157 | "ec2:AcceptVpcPeeringConnection", 158 | "ec2:RejectVpcPeeringConnection", 159 | "ec2:DeleteVpcPeeringConnection", 160 | "ec2:ModifyVpcPeeringConnectionOptions", 161 | ], 162 | resources: [ 163 | `arn:aws:ec2:${Stack.of(this).region}:${Stack.of(this).account}:vpc/${ 164 | props.vpcId 165 | }`, 166 | `arn:aws:ec2:${Stack.of(this).region}:${ 167 | Stack.of(this).account 168 | }:vpc-peering-connection/pcx-*`, 169 | ], 170 | }), 171 | 172 | new iam.PolicyStatement({ 173 | effect: iam.Effect.ALLOW, 174 | actions: ["ec2:DescribeVpcPeeringConnections"], 175 | resources: ["*"], 176 | }) 177 | ); 178 | 179 | ddbGetItemPolicy.attachToRole(handlerRole); 180 | vpcModificationPolicy.attachToRole(handlerRole); 181 | 182 | handlerRole.addManagedPolicy(providerRolePolicy); 183 | this.providerRole.addManagedPolicy(providerRolePolicy); 184 | 185 | this.connectionTable.grantReadData(handlerRole); 186 | 187 | const onEvent = new lambda.Function(this, "CrossAccountPeeringHandler", { 188 | code: onEventCode, 189 | handler: "index.onEvent", 190 | runtime: lambda.Runtime.NODEJS_16_X, 191 | role: handlerRole, 192 | environment: { 193 | TABLE_NAME: this.connectionTable.tableName, 194 | VPC_ID: props.vpcId, 195 | VPC_CIDR: props.cidr, 196 | SECURITY_GROUP_ID: props.securityGroupId, 197 | ROUTE_TABLES: props.routeTables, 198 | NODE_ENV: "prod", 199 | }, 200 | }); 201 | 202 | this.provider = new cr.Provider(this, "CrossAccountPeeringProvider", { 203 | onEventHandler: onEvent, 204 | //logRetention: logs.RetentionDays.ONE_DAY, // default is INFINITE 205 | role: this.providerRole, 206 | providerFunctionName: providerFunctionName, 207 | }); 208 | 209 | this.serviceToken = this.provider.serviceToken; 210 | 211 | //@ts-ignore - see https://github.com/aws/aws-cdk/issues/20642 212 | const providerFunction = this.provider.entrypoint; 213 | 214 | props.allowedAccounts?.forEach((account) => { 215 | providerFunction.grantInvoke(new iam.AccountPrincipal(account)); 216 | }); 217 | 218 | this.serviceToken = this.provider.serviceToken; 219 | 220 | 221 | NagSuppressions.addResourceSuppressions( 222 | this.providerRole, 223 | [iam5ConditionSafeguard], 224 | true, 225 | ) 226 | 227 | NagSuppressions.addResourceSuppressions( 228 | providerRolePolicy, 229 | [iam5ConditionSafeguard], 230 | true, 231 | ) 232 | 233 | NagSuppressions.addResourceSuppressions( 234 | vpcModificationPolicy, 235 | [iam5ConditionSafeguard], 236 | true, 237 | ) 238 | 239 | NagSuppressions.addResourceSuppressions( 240 | this.provider, 241 | [l1CKDKProviderManaged], 242 | true, 243 | ) 244 | 245 | 246 | 247 | 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /cdk/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "cdk", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "cdk-nag": "^2.14.29", 12 | "constructs": "^10.0.0" 13 | }, 14 | "bin": { 15 | "cdk": "bin/cdk.js" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^17.0.35", 19 | "aws-cdk-lib": "^2.49.0", 20 | "constructs": "^10.0.0", 21 | "typescript": "~3.9.0" 22 | } 23 | }, 24 | "node_modules/@types/node": { 25 | "version": "17.0.45", 26 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", 27 | "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", 28 | "dev": true 29 | }, 30 | "node_modules/aws-cdk-lib": { 31 | "version": "2.49.0", 32 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.49.0.tgz", 33 | "integrity": "sha512-HMrV41VaYVLFhm5i35bXuxiiib8IXPwuspDF7W3LJ4WVwxQZWx4eGcvRAdaXuNM7dDwM52cF5BNl8lYa/xCt5Q==", 34 | "bundleDependencies": [ 35 | "@balena/dockerignore", 36 | "case", 37 | "fs-extra", 38 | "ignore", 39 | "jsonschema", 40 | "minimatch", 41 | "punycode", 42 | "semver", 43 | "yaml" 44 | ], 45 | "dependencies": { 46 | "@balena/dockerignore": "^1.0.2", 47 | "case": "1.6.3", 48 | "fs-extra": "^9.1.0", 49 | "ignore": "^5.2.0", 50 | "jsonschema": "^1.4.1", 51 | "minimatch": "^3.1.2", 52 | "punycode": "^2.1.1", 53 | "semver": "^7.3.8", 54 | "yaml": "1.10.2" 55 | }, 56 | "engines": { 57 | "node": ">= 14.15.0" 58 | }, 59 | "peerDependencies": { 60 | "constructs": "^10.0.0" 61 | } 62 | }, 63 | "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { 64 | "version": "1.0.2", 65 | "inBundle": true, 66 | "license": "Apache-2.0" 67 | }, 68 | "node_modules/aws-cdk-lib/node_modules/at-least-node": { 69 | "version": "1.0.0", 70 | "inBundle": true, 71 | "license": "ISC", 72 | "engines": { 73 | "node": ">= 4.0.0" 74 | } 75 | }, 76 | "node_modules/aws-cdk-lib/node_modules/balanced-match": { 77 | "version": "1.0.2", 78 | "inBundle": true, 79 | "license": "MIT" 80 | }, 81 | "node_modules/aws-cdk-lib/node_modules/brace-expansion": { 82 | "version": "1.1.11", 83 | "inBundle": true, 84 | "license": "MIT", 85 | "dependencies": { 86 | "balanced-match": "^1.0.0", 87 | "concat-map": "0.0.1" 88 | } 89 | }, 90 | "node_modules/aws-cdk-lib/node_modules/case": { 91 | "version": "1.6.3", 92 | "inBundle": true, 93 | "license": "(MIT OR GPL-3.0-or-later)", 94 | "engines": { 95 | "node": ">= 0.8.0" 96 | } 97 | }, 98 | "node_modules/aws-cdk-lib/node_modules/concat-map": { 99 | "version": "0.0.1", 100 | "inBundle": true, 101 | "license": "MIT" 102 | }, 103 | "node_modules/aws-cdk-lib/node_modules/fs-extra": { 104 | "version": "9.1.0", 105 | "inBundle": true, 106 | "license": "MIT", 107 | "dependencies": { 108 | "at-least-node": "^1.0.0", 109 | "graceful-fs": "^4.2.0", 110 | "jsonfile": "^6.0.1", 111 | "universalify": "^2.0.0" 112 | }, 113 | "engines": { 114 | "node": ">=10" 115 | } 116 | }, 117 | "node_modules/aws-cdk-lib/node_modules/graceful-fs": { 118 | "version": "4.2.10", 119 | "inBundle": true, 120 | "license": "ISC" 121 | }, 122 | "node_modules/aws-cdk-lib/node_modules/ignore": { 123 | "version": "5.2.0", 124 | "inBundle": true, 125 | "license": "MIT", 126 | "engines": { 127 | "node": ">= 4" 128 | } 129 | }, 130 | "node_modules/aws-cdk-lib/node_modules/jsonfile": { 131 | "version": "6.1.0", 132 | "inBundle": true, 133 | "license": "MIT", 134 | "dependencies": { 135 | "universalify": "^2.0.0" 136 | }, 137 | "optionalDependencies": { 138 | "graceful-fs": "^4.1.6" 139 | } 140 | }, 141 | "node_modules/aws-cdk-lib/node_modules/jsonschema": { 142 | "version": "1.4.1", 143 | "inBundle": true, 144 | "license": "MIT", 145 | "engines": { 146 | "node": "*" 147 | } 148 | }, 149 | "node_modules/aws-cdk-lib/node_modules/lru-cache": { 150 | "version": "6.0.0", 151 | "inBundle": true, 152 | "license": "ISC", 153 | "dependencies": { 154 | "yallist": "^4.0.0" 155 | }, 156 | "engines": { 157 | "node": ">=10" 158 | } 159 | }, 160 | "node_modules/aws-cdk-lib/node_modules/minimatch": { 161 | "version": "3.1.2", 162 | "inBundle": true, 163 | "license": "ISC", 164 | "dependencies": { 165 | "brace-expansion": "^1.1.7" 166 | }, 167 | "engines": { 168 | "node": "*" 169 | } 170 | }, 171 | "node_modules/aws-cdk-lib/node_modules/punycode": { 172 | "version": "2.1.1", 173 | "inBundle": true, 174 | "license": "MIT", 175 | "engines": { 176 | "node": ">=6" 177 | } 178 | }, 179 | "node_modules/aws-cdk-lib/node_modules/semver": { 180 | "version": "7.3.8", 181 | "inBundle": true, 182 | "license": "ISC", 183 | "dependencies": { 184 | "lru-cache": "^6.0.0" 185 | }, 186 | "bin": { 187 | "semver": "bin/semver.js" 188 | }, 189 | "engines": { 190 | "node": ">=10" 191 | } 192 | }, 193 | "node_modules/aws-cdk-lib/node_modules/universalify": { 194 | "version": "2.0.0", 195 | "inBundle": true, 196 | "license": "MIT", 197 | "engines": { 198 | "node": ">= 10.0.0" 199 | } 200 | }, 201 | "node_modules/aws-cdk-lib/node_modules/yallist": { 202 | "version": "4.0.0", 203 | "inBundle": true, 204 | "license": "ISC" 205 | }, 206 | "node_modules/aws-cdk-lib/node_modules/yaml": { 207 | "version": "1.10.2", 208 | "inBundle": true, 209 | "license": "ISC", 210 | "engines": { 211 | "node": ">= 6" 212 | } 213 | }, 214 | "node_modules/cdk-nag": { 215 | "version": "2.19.5", 216 | "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.19.5.tgz", 217 | "integrity": "sha512-sM15E108aBOPvoCUAlIVG3qigiHCOH4W0zgJdws6zrownv7FRAHdIjkT9ohfBGqTgfB9ig7PVwRH9u0VLKzBNw==", 218 | "peerDependencies": { 219 | "aws-cdk-lib": "^2.45.0", 220 | "constructs": "^10.0.5" 221 | } 222 | }, 223 | "node_modules/constructs": { 224 | "version": "10.1.142", 225 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.142.tgz", 226 | "integrity": "sha512-OMD0JB3vzBF5hxtfcIUuVjVh8g+Q0ndOdQKmdCoFmEnCLoShn0fLnUOtJb+6iPFTb6l2DgLZCOEJ6YBFISYTfA==", 227 | "engines": { 228 | "node": ">= 14.17.0" 229 | } 230 | }, 231 | "node_modules/typescript": { 232 | "version": "3.9.10", 233 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", 234 | "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", 235 | "dev": true, 236 | "bin": { 237 | "tsc": "bin/tsc", 238 | "tsserver": "bin/tsserver" 239 | }, 240 | "engines": { 241 | "node": ">=4.2.0" 242 | } 243 | } 244 | }, 245 | "dependencies": { 246 | "@types/node": { 247 | "version": "17.0.45", 248 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", 249 | "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", 250 | "dev": true 251 | }, 252 | "aws-cdk-lib": { 253 | "version": "2.49.0", 254 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.49.0.tgz", 255 | "integrity": "sha512-HMrV41VaYVLFhm5i35bXuxiiib8IXPwuspDF7W3LJ4WVwxQZWx4eGcvRAdaXuNM7dDwM52cF5BNl8lYa/xCt5Q==", 256 | "requires": { 257 | "@balena/dockerignore": "^1.0.2", 258 | "case": "1.6.3", 259 | "fs-extra": "^9.1.0", 260 | "ignore": "^5.2.0", 261 | "jsonschema": "^1.4.1", 262 | "minimatch": "^3.1.2", 263 | "punycode": "^2.1.1", 264 | "semver": "^7.3.8", 265 | "yaml": "1.10.2" 266 | }, 267 | "dependencies": { 268 | "@balena/dockerignore": { 269 | "version": "1.0.2", 270 | "bundled": true 271 | }, 272 | "at-least-node": { 273 | "version": "1.0.0", 274 | "bundled": true 275 | }, 276 | "balanced-match": { 277 | "version": "1.0.2", 278 | "bundled": true 279 | }, 280 | "brace-expansion": { 281 | "version": "1.1.11", 282 | "bundled": true, 283 | "requires": { 284 | "balanced-match": "^1.0.0", 285 | "concat-map": "0.0.1" 286 | } 287 | }, 288 | "case": { 289 | "version": "1.6.3", 290 | "bundled": true 291 | }, 292 | "concat-map": { 293 | "version": "0.0.1", 294 | "bundled": true 295 | }, 296 | "fs-extra": { 297 | "version": "9.1.0", 298 | "bundled": true, 299 | "requires": { 300 | "at-least-node": "^1.0.0", 301 | "graceful-fs": "^4.2.0", 302 | "jsonfile": "^6.0.1", 303 | "universalify": "^2.0.0" 304 | } 305 | }, 306 | "graceful-fs": { 307 | "version": "4.2.10", 308 | "bundled": true 309 | }, 310 | "ignore": { 311 | "version": "5.2.0", 312 | "bundled": true 313 | }, 314 | "jsonfile": { 315 | "version": "6.1.0", 316 | "bundled": true, 317 | "requires": { 318 | "graceful-fs": "^4.1.6", 319 | "universalify": "^2.0.0" 320 | } 321 | }, 322 | "jsonschema": { 323 | "version": "1.4.1", 324 | "bundled": true 325 | }, 326 | "lru-cache": { 327 | "version": "6.0.0", 328 | "bundled": true, 329 | "requires": { 330 | "yallist": "^4.0.0" 331 | } 332 | }, 333 | "minimatch": { 334 | "version": "3.1.2", 335 | "bundled": true, 336 | "requires": { 337 | "brace-expansion": "^1.1.7" 338 | } 339 | }, 340 | "punycode": { 341 | "version": "2.1.1", 342 | "bundled": true 343 | }, 344 | "semver": { 345 | "version": "7.3.8", 346 | "bundled": true, 347 | "requires": { 348 | "lru-cache": "^6.0.0" 349 | } 350 | }, 351 | "universalify": { 352 | "version": "2.0.0", 353 | "bundled": true 354 | }, 355 | "yallist": { 356 | "version": "4.0.0", 357 | "bundled": true 358 | }, 359 | "yaml": { 360 | "version": "1.10.2", 361 | "bundled": true 362 | } 363 | } 364 | }, 365 | "cdk-nag": { 366 | "version": "2.19.5", 367 | "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.19.5.tgz", 368 | "integrity": "sha512-sM15E108aBOPvoCUAlIVG3qigiHCOH4W0zgJdws6zrownv7FRAHdIjkT9ohfBGqTgfB9ig7PVwRH9u0VLKzBNw==", 369 | "requires": {} 370 | }, 371 | "constructs": { 372 | "version": "10.1.142", 373 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.1.142.tgz", 374 | "integrity": "sha512-OMD0JB3vzBF5hxtfcIUuVjVh8g+Q0ndOdQKmdCoFmEnCLoShn0fLnUOtJb+6iPFTb6l2DgLZCOEJ6YBFISYTfA==" 375 | }, 376 | "typescript": { 377 | "version": "3.9.10", 378 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", 379 | "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", 380 | "dev": true 381 | } 382 | } 383 | } 384 | --------------------------------------------------------------------------------