# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 AWSTemplateFormatVersion: '2010-09-09' Description: (SO0058) - The AWS CloudFormation template (Hub) for deployment of the %SOLUTION_NAME% Solution. Version %VERSION% Parameters: Principals: Type: CommaDelimitedList Description: AWS account numbers eg. 123456789012 (comma separated) OR the ARN of an Organization to share TGW with the principals eg. arn::organizations:::organization/ PrincipalType: Type: String Description: Either provide list of accounts (comma separated) or AWS Organizations ARN Default: 'AWS Organization ARN' AllowedValues: - 'AWS Organization ARN' - 'List of Accounts' ApprovalNotification: Description: Do you want to send notification to the Network Admin for approval? AllowedValues: - 'Yes' - 'No' Default: 'No' Type: String DeployWebUi: Description: Do you want to deploy a web user interface to manage and audit your network? If you select 'No', you can skip the Web UI settings. AllowedValues: - "Yes" - "No" Default: "Yes" Type: String AllowExternalPrincipals: Description: Do you want to share Transit Gateway with individual AWS accounts that are not in your organization? AllowedValues: - "Yes" - "No" Default: "Yes" Type: String ApprovalNotificationEmail: Type: 'String' Description: Email for the network administrator(s) AllowedPattern: '(^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)' UserPoolUsernameForAdminUser: Description: Cognito UserPool ADMIN username used in the solution. The ADMIN user has full read and write permissions. Default: adminuser Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' UserPoolUsernameForReadOnlyUser: Description: Cognito UserPool READONLY username used in the solution. The READONLY user has only read permission. Default: readonlyuser Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' ConsoleLoginInformationEmail: Description: Active email address to send temporary password for both admin and read-only usernames. Type: String AllowedPattern: '(^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)' MultiFactorAuthentication: Type: String Default: OPTIONAL AllowedValues: - 'ON' - OPTIONAL Description: Set to 'ON' or 'OPTIONAL' to enable multi factor authentication for Cognito User Pool. AttachmentTag: Description: Custom tag key name to create the VPC transit gateway attachment. Default: Attach-to-tgw Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' RoutingTag: Description: Custom tag key name to update route table associated with other subnets in same availability zone workflow. Default: Route-to-tgw Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' AssociationTag: Description: Custom tag key name to create the transit gateway route table association. Default: Associate-with Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' PropagationTag: Description: Custom tag key name to enable the transit gateway route table propagation. Default: Propagate-to Type: String MinLength: 0 MaxLength: 20 AllowedPattern: '^[a-zA-Z][a-zA-Z0-9-_]*$' TgwPeeringTag: Description: Custom tag key name to create the transit gateway peering attachment. The tag value must follow the format for eg. tgw-12345678_us-east-1/tgw-567890123_us-east-2 Default: TgwPeer Type: String MinLength: 0 MaxLength: 20 DefaultRoute: Description: Default/Static route(s) to Transit Gateway - applicable to spoke account route table associated with the tagged subnets. Default: "All-Traffic (0/0)" Type: String AllowedValues: - "All-Traffic (0/0)" - "RFC-1918 (10/8, 172.16/12, 192.168/16)" - "Custom-Destinations" - "Configure-Manually" ListOfCustomCidrBlocks: Type: String Description: Option to provide CIDR block(s) (OPTIONAL if providing prefix list id(s)) Example - 192.168.1.0/24, 192.168.2.0/24 Default: "" AllowedPattern: '(^$|^(([0-9]{1,3}\.){3}[0-9]{1,3}\/\d{1,2})(, (([0-9]{1,3}\.){3}[0-9]{1,3}\/\d{1,2}))*$)' CustomerManagedPrefixListIds: Type: String Description: Option to provide Customer-managed Prefix List Id(s) (OPTIONAL if providing CIDR blocks.) Example - pl-abcd1234, pl-efgh5678 Default: "" ExistingTransitGatewayId: Description: New transit gateway will be created if no value is provided. Example, tgw-a1b2c3d4e5 Type: String Default: "" AllowedPattern: '(^$|^tgw-[0-9a-z]{17}$)' ExistingGlobalNetworkId: Description: New global network will be created if no value is provided. Example, global-network-01231231231231231 Type: String Default: "" AllowedPattern: '(^$|^global-network-[0-9a-z]{17}$)' ListOfVpcTagsForAttachment: Description: Comma separated list of tag keys (do not include Name). If the VPC has these tag keys, the tag key/value is copied to any created TGW attachment. Default: "Associate-with,Propagate-to" Type: String OrganizationManagementAccountRoleArn: Description: > (Optional) To tag attachments with the account name, and use OU membership for auto-approvals, either configure this account to be an AWS Organization delegated admin for any service, or configure this parameter to include a role in the AWS Organizations Management account, that trusts this account, and has: organizations:DescribeAccount, organizations:ListParents and organizations:DescribeOrganizationalUnit Type: String AllowedPattern: ^((arn:(aws|aws-cn):iam::\d{12}:role\/.+)|)$ Default: "" CognitoDomainPrefixParameter: Type: String AllowedPattern: ^$|^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ Description: The prefix to the Cognito hosted domain name that will be associated with the user pool. Must be unique per region and must not contain the reserved word 'cognito'. CognitoSAMLProviderNameParameter: Type: String AllowedPattern: ^[a-zA-Z]*$ Description: (Optional) The identity provider name. MaxLength: 32 CognitoSAMLProviderMetadataUrlParameter: Type: String Description: (Optional) MetadataURL for the identity provider details. AllowListedRanges: Type: CommaDelimitedList Description: | Comma separated list of CIDR ranges that allow to console to access the API. To allow all the entire internet, use 0.0.0.0/1,128.0.0.0/1 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Account Structure Settings Parameters: - PrincipalType - Principals - AllowExternalPrincipals - OrganizationManagementAccountRoleArn - Label: default: Web UI Settings Parameters: - DeployWebUi - AllowListedRanges - CognitoDomainPrefixParameter - ConsoleLoginInformationEmail - UserPoolUsernameForAdminUser - UserPoolUsernameForReadOnlyUser - MultiFactorAuthentication - CognitoSAMLProviderNameParameter - CognitoSAMLProviderMetadataUrlParameter - Label: default: Transit Gateway Settings Parameters: - ExistingTransitGatewayId - ExistingGlobalNetworkId - Label: default: VPC Route Table Settings Parameters: - DefaultRoute - ListOfCustomCidrBlocks - CustomerManagedPrefixListIds - Label: default: Tag Settings Parameters: - AttachmentTag - RoutingTag - AssociationTag - PropagationTag - ListOfVpcTagsForAttachment - TgwPeeringTag - Label: default: Notification Settings Parameters: - ApprovalNotification - ApprovalNotificationEmail ParameterLabels: Principals: default: Account List or AWS Organizations ARN PrincipalType: default: Principal Type AllowExternalPrincipals: default: Allow External Principals ApprovalNotificationEmail: default: Approval Notification Email AllowListedRanges: default: Allow Listed Ranges ApprovalNotification: default: Receive Approval Notifications DeployWebUi: default: Web User Interface ConsoleLoginInformationEmail: default: Console Login Information Email UserPoolUsernameForAdminUser: default: Admin Username UserPoolUsernameForReadOnlyUser: default: Read-Only Username MultiFactorAuthentication: default: Set MFA for Cognito to 'ON' or 'OPTIONAL' DefaultRoute: default: Choose the type of destination for target Transit Gateway AttachmentTag: default: Tag key for subnets - Adds subnet to VPC attachment and add routes to route table associated with the tagged subnet. RoutingTag: default: Tag key for subnets - Only adds routes to route table associated with the tagged subnet. AssociationTag: default: Tag key for TGW Route Table Association with TGW Attachment PropagationTag: default: Tag key for Route Propagation to TGW Route Table(s) ExistingTransitGatewayId: default: (Optional) Provide the existing transit gateway id. TgwPeeringTag: default: Transit Gateway Peering Tag ExistingGlobalNetworkId: default: (Optional) Provide the existing global network id. ListOfCustomCidrBlocks: default: If selected 'Custom-Destinations', provide a comma separated list of CIDR Blocks. CustomerManagedPrefixListIds: default: If selected 'Custom-Destinations', provide a comma separated list of Customer-managed Prefix List IDs. ListOfVpcTagsForAttachment: default: (Optional) Comma separated list of VPC tag keys to copy from VPC to TGW Attachments OrganizationManagementAccountRoleArn: default: (Optional) IAM Role ARN of Management Account CognitoDomainPrefixParameter: default: Cognito Domain Prefix CognitoSAMLProviderNameParameter: default: SAML Provider Name CognitoSAMLProviderMetadataUrlParameter: default: SAML Provider Metadata URL Conditions: NotificationCondition: !Equals [!Ref ApprovalNotification, 'Yes'] DeployWebUiCondition: !Equals [!Ref DeployWebUi, "Yes" ] IsMemberOfOrganization: !Equals [!Ref PrincipalType, 'AWS Organization ARN'] IsNotMemberOfOrganization: !Equals [!Ref PrincipalType, 'List of Accounts'] CreateNewTransitGateway: !Equals [!Ref ExistingTransitGatewayId, ''] DeployIfNotChinaPartition: !Not [!Equals [ !Ref AWS::Partition, "aws-cn" ]] CreateNewGlobalNetwork: !And - !Equals [ !Ref ExistingGlobalNetworkId, '' ] - !Condition DeployIfNotChinaPartition CreateNewGlobalNetwork&CreateNewTransitGateway: !And - !Condition CreateNewGlobalNetwork - !Condition CreateNewTransitGateway ProvidedPrefixListIds: !Not [!Equals [!Ref CustomerManagedPrefixListIds, '']] OrganizationManagementAccountRoleArn: !Not [ !Equals [!Ref OrganizationManagementAccountRoleArn, '']] FrontEndCognitoSAMLCondition: !And - !Not [ !Equals [ !Ref CognitoSAMLProviderNameParameter, '' ] ] - !Not [ !Equals [ !Ref CognitoSAMLProviderMetadataUrlParameter, '' ] ] - !Condition CreateNewGlobalNetwork AllowExternalPrincipals: Fn::Equals: - Ref: AllowExternalPrincipals - 'Yes' Mappings: NetworkConfiguration: TransitGateway: AutoAcceptSharedAttachments: "enable" # the solution assumes this setting is set to 'enable'. DefaultRouteTableAssociation: "disable" # the solution assumes this setting is set to 'disable'. DefaultRouteTablePropagation: "disable" # the solution assumes this setting is set to 'disable'. DnsSupport: "enable" # this value can be changed based on customer preference VpnEcmpSupport: "enable" # this value can be changed based on customer preference us-east-1: AmazonSideAsn: 64526 # this must be changed by a network admin only us-east-2: AmazonSideAsn: 64527 # this must be changed by a network admin only us-west-1: AmazonSideAsn: 64528 # this must be changed by a network admin only us-west-2: AmazonSideAsn: 64529 # this must be changed by a network admin only eu-west-1: AmazonSideAsn: 64530 # this must be changed by a network admin only eu-west-2: AmazonSideAsn: 64531 # this must be changed by a network admin only eu-west-3: AmazonSideAsn: 64532 # this must be changed by a network admin only eu-central-1: AmazonSideAsn: 64533 # this must be changed by a network admin only ca-central-1: AmazonSideAsn: 64534 # this must be changed by a network admin only ap-northeast-1: AmazonSideAsn: 64535 # this must be changed by a network admin only ap-northeast-2: AmazonSideAsn: 64536 # this must be changed by a network admin only ap-south-1: AmazonSideAsn: 64537 # this must be changed by a network admin only ap-southeast-1: AmazonSideAsn: 64538 # this must be changed by a network admin only ap-southeast-2: AmazonSideAsn: 64539 # this must be changed by a network admin only sa-east-1: AmazonSideAsn: 64540 # this must be changed by a network admin only cn-north-1: AmazonSideAsn: 64541 # this must be changed by a network admin only cn-northwest-1: AmazonSideAsn: 64542 # this must be changed by a network admin only eu-north-1: AmazonSideAsn: 64543 # this must be changed by a network admin only NotificationConfiguration: SNS: DisplayName: "AWS Transit Network Change Approval Notification" TopicName: AWS-Transit-Network-Approval-Notifications LambdaFunction: Logging: Level: "info" EventBridge: Bus: Name: "Network-Orchestrator-Event-Bus" LogRetention: CloudWatchLogs: RetentionPeriod: 90 AuditTrail: RetentionPeriod: 90 SourceCode: General: S3Bucket: "%DIST_BUCKET_NAME%" KeyPrefix: "network-orchestration-for-aws-transit-gateway/%VERSION%" Version: "%VERSION%" Variables: WaitTime: "10" AllTraffic: "0.0.0.0/0" RFC1918Routes: "10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16" ApprovalTagKey: "ApprovalRequired" ApprovalTagValue: "No" Solution: Metrics: SolutionId: "SO0058" MetricsEndpoint: "https://metrics.awssolutionsbuilder.com/generic" Data: AppRegistryApplicationName: 'network-orchestration-for-aws-transit-gateway' SolutionName: 'Network Orchestration for AWS Transit Gateway' TagKey: 'CloudFoundations:NetworkOrchestrationForAWSTransitGateway' AnonymizedData: SendAnonymizedData: Data: "Yes" Resources: AWSTransitGateway: Type: AWS::EC2::TransitGateway DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewTransitGateway Properties: AmazonSideAsn: !FindInMap [NetworkConfiguration, !Ref "AWS::Region", AmazonSideAsn] Description: "This transit gateway was created by Network Orchestration for AWS Transit Gateway solution" AutoAcceptSharedAttachments: !FindInMap [NetworkConfiguration, TransitGateway, AutoAcceptSharedAttachments] DefaultRouteTableAssociation: !FindInMap [NetworkConfiguration, TransitGateway, DefaultRouteTableAssociation] DefaultRouteTablePropagation: !FindInMap [NetworkConfiguration, TransitGateway, DefaultRouteTablePropagation] DnsSupport: !FindInMap [NetworkConfiguration, TransitGateway, DnsSupport] VpnEcmpSupport: !FindInMap [NetworkConfiguration, TransitGateway, VpnEcmpSupport] Tags: - Key: Name Value: !Sub STNO-TGW-${AWS::Region} - Key: AWS Solutions Value: !Ref 'AWS::StackId' FlatTGWRouteTable: Type: AWS::EC2::TransitGatewayRouteTable DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewTransitGateway Properties: TransitGatewayId: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] Tags: - Key: Name Value: Flat - Key: StackId Value: !Ref AWS::StackId - Key: !FindInMap ["SourceCode", "Variables", "ApprovalTagKey"] Value: !FindInMap ["SourceCode", "Variables", "ApprovalTagValue"] OnPremTGWRouteTable: Type: AWS::EC2::TransitGatewayRouteTable DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewTransitGateway Properties: TransitGatewayId: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] Tags: - Key: Name Value: On-premises - Key: StackId Value: !Ref AWS::StackId - Key: !FindInMap ["SourceCode", "Variables", "ApprovalTagKey"] Value: !FindInMap ["SourceCode", "Variables", "ApprovalTagValue"] IsolatedTGWRouteTable: Type: AWS::EC2::TransitGatewayRouteTable DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewTransitGateway Properties: TransitGatewayId: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] Tags: - Key: Name Value: Isolated - Key: !FindInMap ["SourceCode", "Variables", "ApprovalTagKey"] Value: !FindInMap ["SourceCode", "Variables", "ApprovalTagValue"] InfrastructureTGWRouteTable: Type: AWS::EC2::TransitGatewayRouteTable DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewTransitGateway Properties: TransitGatewayId: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] Tags: - Key: Name Value: Infrastructure - Key: StackId Value: !Ref AWS::StackId - Key: !FindInMap ["SourceCode", "Variables", "ApprovalTagKey"] Value: !FindInMap ["SourceCode", "Variables", "ApprovalTagValue"] TGWResourceShare: Type: "AWS::RAM::ResourceShare" Properties: Name: "Transit Gateway Resource Share" ResourceArns: - !Sub - arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/${TransitGatewayId} - {TransitGatewayId: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId]} Principals: !Ref Principals AllowExternalPrincipals: Fn::If: - AllowExternalPrincipals - true - false Tags: - Key: Network Orchestration for AWS Transit Gateway Value: !Ref 'AWS::StackId' ConvertPrefixListIdsToPLArnList: Condition: ProvidedPrefixListIds Type: Custom::GetPrefixListArns Properties: PrefixListIds: !Ref CustomerManagedPrefixListIds AccountId: !Sub ${AWS::AccountId} ServiceToken: !GetAtt CustomResourceLambda.Arn CustomerManagedPrefixListResourceShare: Type: "AWS::RAM::ResourceShare" Condition: ProvidedPrefixListIds Properties: Name: "Customer-managed Prefix Lists Resource Share" ResourceArns: !GetAtt ConvertPrefixListIdsToPLArnList.PrefixListArns Principals: !Ref Principals Tags: - Key: Network Orchestration for AWS Transit Gateway Value: !Ref 'AWS::StackId' ApprovalTopic: Type: AWS::SNS::Topic Properties: DisplayName: !FindInMap [NotificationConfiguration ,SNS ,DisplayName] TopicName: !FindInMap [NotificationConfiguration ,SNS ,TopicName] KmsMasterKeyId: alias/aws/sns NetworkAdminEmailNotification: Condition: NotificationCondition Type: AWS::SNS::Subscription Properties: Endpoint: !Ref ApprovalNotificationEmail Protocol: email TopicArn: !Ref ApprovalTopic SNSNotificationPolicy: Type: AWS::SNS::TopicPolicy Properties: Topics: - !Ref ApprovalTopic PolicyDocument: Statement: - Sid: __default_statement_ID Effect: Allow Principal: AWS: !Sub ${AWS::AccountId} Action: - SNS:GetTopicAttributes - SNS:SetTopicAttributes - SNS:AddPermission - SNS:RemovePermission - SNS:DeleteTopic - SNS:Subscribe - SNS:ListSubscriptionsByTopic - SNS:Publish - SNS:Receive Resource: !Ref ApprovalTopic - Sid: TrustCWEToPublishEventsToMyTopic Effect: Allow Principal: Service: events.amazonaws.com Action: sns:Publish Resource: !Ref ApprovalTopic - Sid: AllowPublishThroughSSLOnly Effect: Deny Principal: "*" Action: - SNS:Publish Resource: !Ref ApprovalTopic Condition: Bool: aws:SecureTransport: False # For spoke accounts: LambdaEventRuleSpokeAccounts: Type: AWS::Events::Rule Properties: Description: Serverless Transit Network Orchestrator - Rule for tag changes in spokes EventBusName: !Ref STNOCustomEventBus EventPattern: { "account": !If [ IsNotMemberOfOrganization, !Ref Principals, !Ref "AWS::NoValue" ], "source": [ "aws.tag" ], "detail-type": [ "Tag Change on Resource" ], "detail": { "service": [ "ec2" ], "resource-type": [ "subnet", "vpc" ], "changed-tag-keys": [ !Ref "AttachmentTag", !Ref "RoutingTag", !Ref "AssociationTag", !Ref "PropagationTag" ] } } State: ENABLED Targets: - Arn: !GetAtt CustomResourceLambda.Arn Id: 'CustomResourceLambda' InputTransformer: InputPathsMap: "detail" : "$.detail" "source": "$.source" "account": "$.account" "resources": "$.resources" InputTemplate: !Sub - | { "state-machine": "${StateMachine}", "detail" : , "source" : , "account" : , "resources" : } - StateMachine: !Ref OrchestratorStateMachine PermissionForSpokeAccountRule: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "CustomResourceLambda" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !Sub ${LambdaEventRuleSpokeAccounts.Arn} # For hub account: LambdaEventRuleHubAccount: Type: AWS::Events::Rule Properties: Description: Serverless Transit Network Orchestrator - Rule for tag changes in the hub EventPattern: { "account": [ !Ref "AWS::AccountId" ], "source": [ "aws.tag" ], "detail-type": [ "Tag Change on Resource" ], "detail": { "service": [ "ec2" ], "resource-type": [ "subnet", "vpc" ], "changed-tag-keys": [ !Ref "AttachmentTag", !Ref "RoutingTag", !Ref "AssociationTag", !Ref "PropagationTag" ] } } State: ENABLED Targets: - Arn: !GetAtt CustomResourceLambda.Arn Id: 'CustomResourceLambda' InputTransformer: InputPathsMap: "detail" : "$.detail" "source": "$.source" "account": "$.account" "resources": "$.resources" InputTemplate: !Sub - | { "state-machine": "${StateMachine}", "detail" : , "source" : , "account" : , "resources" : } - StateMachine: !Ref OrchestratorStateMachine PermissionForHubAccountRule: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "CustomResourceLambda" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !Sub ${LambdaEventRuleHubAccount.Arn} LambdaEventRuleOnSubnetDeletion: Type: AWS::Events::Rule Properties: Description: Serverless Transit Network Orchestrator - Rule for detecting CFN subnet deletions EventBusName: !Ref STNOCustomEventBus EventPattern: { "account": !If [ IsNotMemberOfOrganization, !Ref Principals, !Ref "AWS::NoValue" ], "source": [ "aws.ec2" ], "detail-type": [ "AWS API Call via CloudTrail" ], "detail": { "eventSource": [ "ec2.amazonaws.com" ], "eventName": [ "DeleteSubnet" ], "errorCode": [ "Client.DependencyViolation" ], "sourceIPAddress": [ "cloudformation.amazonaws.com" ] } } State: ENABLED Targets: - Arn: !Sub ${CustomResourceLambda.Arn} Id: 'CustomResourceLambda' InputTransformer: InputPathsMap: "detail" : "$.detail" "source": "$.source" InputTemplate: !Sub - | { "state-machine": "${StateMachine}", "detail" : , "source" : } - StateMachine: !Ref OrchestratorStateMachine PermissionForSubnetDeletionRule: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "CustomResourceLambda" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !Sub ${LambdaEventRuleOnSubnetDeletion.Arn} STNOCustomEventBus: Type: AWS::Events::EventBus Properties: Name: !FindInMap [EventBridge, Bus, Name] EventBridgeBusPermission: Type: Custom::CWEventPermissions Properties: Principals: !Ref Principals EventBusName: !Ref STNOCustomEventBus ServiceToken: !Sub ${CustomResourceLambda.Arn} CustomResourceLambda: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W92 reason: "does not require concurrency reservation" - id: W89 reason: "not a valid use-case for vpc" - id: W58 reason: "log write permission added to CustomResourceLambdaFunctionRole" Properties: Environment: Variables: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] SEND_METRIC: !FindInMap [AnonymizedData, SendAnonymizedData, Data] SOLUTION_ID: !FindInMap [Solution, Metrics, SolutionId] SOLUTION_VERSION: !FindInMap [SourceCode, General, Version] METRICS_ENDPOINT: !FindInMap [Solution, Metrics, MetricsEndpoint] USER_AGENT_STRING: AwsSolution/SO0058/%VERSION% PARTITION: !Sub ${AWS::Partition} Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], !Ref "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],"custom_resource.zip"]] Description: Network Orchestration for AWS Transit Gateway - custom resource handler Handler: custom_resource.main.lambda_handler MemorySize: 1536 Role: !Sub ${CustomResourceLambdaFunctionRole.Arn} Runtime: python3.9 Timeout: 900 LambdaPermissionSpokeAccount: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt LambdaEventRuleSpokeAccounts.Arn FunctionName: !Ref CustomResourceLambda LambdaPermissionHubAccount: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt LambdaEventRuleHubAccount.Arn FunctionName: !Ref CustomResourceLambda LambdaPermissionOnSubnetDeletion: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt LambdaEventRuleOnSubnetDeletion.Arn FunctionName: !Ref CustomResourceLambda CustomResourceLambdaFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: STNO-CWLogs-Policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* - PolicyName: STNO-Events-Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - events:DescribeEventBus - events:RemovePermission - events:PutPermission Resource: !Sub ${STNOCustomEventBus.Arn} PolicyIAMEC2: Type: AWS::IAM::Policy Metadata: cfn_nag: rules_to_suppress: - id: W12 reason: given actions do not allow resource-level permissions or need read permissions on multiple resources Properties: PolicyName: STNO-MultipleResourcesPolicy Roles: - !Ref StateMachineLambdaFunctionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeSubnets - ec2:DescribeVpcs - ec2:DescribeTransitGatewayVpcAttachments - ec2:DescribeTransitGatewayAttachments - ec2:DescribeTransitGatewayRouteTables - ec2:GetTransitGatewayAttachmentPropagations - ec2:GetTransitGatewayRouteTableAssociations - ec2:GetTransitGatewayRouteTablePropagations - ec2:SearchTransitGatewayRoutes - iam:GetRole Resource: '*' - Effect: Allow Action: - ec2:CreateTransitGatewayRoute - ec2:DeleteTransitGatewayRoute - ec2:ModifyTransitGatewayVpcAttachment - ec2:CreateTransitGatewayVpcAttachment - ec2:DeleteTransitGatewayVpcAttachment - ec2:AssociateTransitGatewayRouteTable - ec2:DisableTransitGatewayRouteTablePropagation - ec2:DisassociateTransitGatewayRouteTable - ec2:EnableTransitGatewayRouteTablePropagation - ec2:CreateTags Resource: # use * for account id as the owner of the resource is spoke account - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:*:transit-gateway-route-table/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:*:transit-gateway-attachment/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:*:vpc/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:*:subnet/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:*:route-table/* PolicySNS: Type: AWS::IAM::Policy Properties: PolicyName: STNO-SNSPolicy Roles: - !Ref StateMachineLambdaFunctionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: sns:Publish Resource: - !Ref ApprovalTopic PolicyConsole: Type: AWS::IAM::Policy Condition: DeployWebUiCondition Properties: PolicyName: STNO-S3-Policy Roles: - !Ref CustomResourceLambdaFunctionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: "Allow" Action: - "s3:PutObject" Resource: - !Sub ${ConsoleBucket.Arn} - !Sub ${ConsoleBucket.Arn}/* - Effect: "Allow" Action: - "s3:GetObject" Resource: - !Join [ "", [ !Sub "arn:${AWS::Partition}:s3:::", !FindInMap [ "SourceCode", "General", "S3Bucket" ], "-", !Ref "AWS::Region" ] ] - !Join [ "", [ !Sub "arn:${AWS::Partition}:s3:::", !FindInMap [ "SourceCode", "General", "S3Bucket" ], "-", !Ref "AWS::Region", "/*" ] ] CreateUniqueID: Type: "Custom::CreateUUID" Properties: ServiceToken: !GetAtt CustomResourceLambda.Arn DynamoDbTable: Type: 'AWS::DynamoDB::Table' Properties: AttributeDefinitions: - AttributeName: SubnetId AttributeType: S - AttributeName: Version AttributeType: S KeySchema: - AttributeName: SubnetId KeyType: HASH - AttributeName: Version KeyType: RANGE TimeToLiveSpecification: AttributeName: TimeToLive Enabled: true BillingMode: PAY_PER_REQUEST SSESpecification: SSEEnabled: True SSEType: KMS PointInTimeRecoverySpecification: PointInTimeRecoveryEnabled: true StateMachineLambdaFunction: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W92 reason: "does not require concurrency reservation" - id: W89 reason: "not a valid use-case for vpc" Properties: Environment: Variables: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] APPROVAL_NOTIFICATION: !Ref ApprovalNotification APPROVAL_NOTIFICATION_ARN: !Ref ApprovalTopic TGW_ID: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] TABLE_NAME: !Ref DynamoDbTable ASSOCIATION_TAG: !Ref AssociationTag PROPAGATION_TAG: !Ref PropagationTag ATTACHMENT_TAG: !Ref AttachmentTag ROUTING_TAG: !Ref RoutingTag DEFAULT_ROUTE: !Ref DefaultRoute CIDR_BLOCKS : !Ref ListOfCustomCidrBlocks PREFIX_LISTS: !Ref CustomerManagedPrefixListIds ALL_TRAFFIC: !FindInMap ["SourceCode", "Variables", "AllTraffic"] RFC_1918_ROUTES: !FindInMap ["SourceCode", "Variables", "RFC1918Routes"] WAIT_TIME: !FindInMap ["SourceCode", "Variables", "WaitTime"] TTL: !FindInMap ["LogRetention", "AuditTrail", "RetentionPeriod"] APPROVAL_KEY: !FindInMap ["SourceCode", "Variables", "ApprovalTagKey"] FIRST_PRINCIPAL: !Select [ 0, !Ref Principals ] PARTITION: !Sub ${AWS::Partition} RESOURCE_SHARE_ARN: !GetAtt TGWResourceShare.Arn METRICS_FLAG: !FindInMap [AnonymizedData, SendAnonymizedData, Data] SOLUTION_VERSION: !FindInMap ["SourceCode", "General", "Version"] STNO_CONSOLE_LINK: !If [DeployWebUiCondition, !Sub "https://${ConsoleCloudFront.DomainName}", !Ref AWS::NoValue] VPC_TAGS_FOR_ATTACHMENT: !Ref ListOfVpcTagsForAttachment ORGANIZATION_ACCOUNT_ROLE_ARN: !Ref OrganizationManagementAccountRoleArn LOG_GROUP_FAILURES: !Ref CloudWatchLogFailures LOG_GROUP_ACTIONS: !Ref CloudWatchLogActions USER_AGENT_STRING: AwsSolution/SO0058/%VERSION% SOLUTION_UUID: !GetAtt CreateUniqueID.UUID Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],"tgw_vpc_attachment.zip"]] Description: Network Orchestration for AWS Transit Gateway - State Machine Handler Handler: tgw_vpc_attachment.main.lambda_handler MemorySize: 1536 Role: !GetAtt 'StateMachineLambdaFunctionRole.Arn' Runtime: python3.9 Timeout: 900 StateMachineRole: Type: "AWS::IAM::Role" Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "* needed for state machine logging configuration as highlighted here https://docs.aws.amazon.com/step-functions/latest/dg/cw-logs.html#monitoring-logging-configure" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - !Sub "states.${AWS::Region}.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: STNO-LambdaInvoke PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "lambda:InvokeFunction" Resource: !GetAtt StateMachineLambdaFunction.Arn - Effect: Allow Action: - "logs:CreateLogDelivery" - "logs:GetLogDelivery" - "logs:UpdateLogDelivery" - "logs:DeleteLogDelivery" - "logs:ListLogDeliveries" - "logs:PutResourcePolicy" - "logs:DescribeResourcePolicies" - "logs:DescribeLogGroups" Resource: - "*" StateMachineLambdaFunctionRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "The role name 'StateMachineLambdaFunction' has to be defined to allow cross account access from the hub account to make network changes." Properties: RoleName: !Join ["-", ["STNO-StateMachineLambdaFunctionRole", Ref: "AWS::Region"]] AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: STNO-StateMachine-Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/{AWS::Partition}/lambda/* - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogActions}* - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CloudWatchLogFailures}* - Effect: Allow Action: - sts:AssumeRole Resource: !Join ["-", ["Fn::Sub": "arn:${AWS::Partition}:iam::*:role/TransitNetworkExecutionRole", Ref: "AWS::Region"]] - Effect: Allow Action: - dynamodb:PutItem Resource: !GetAtt DynamoDbTable.Arn - !If - OrganizationManagementAccountRoleArn - Effect: Allow Action: - sts:AssumeRole Resource: !Ref OrganizationManagementAccountRoleArn - !Ref AWS::NoValue OrchestratorStateMachine: Type: 'AWS::StepFunctions::StateMachine' Properties: StateMachineName: STNO-StateMachine RoleArn: !GetAtt 'StateMachineRole.Arn' LoggingConfiguration: Destinations: - CloudWatchLogsLogGroup: LogGroupArn: !GetAtt CloudWatchLogStateMachine.Arn IncludeExecutionData: true Level: ALL TracingConfiguration: Enabled: true DefinitionSubstitutions: StateMachineLambdaArn: !GetAtt StateMachineLambdaFunction.Arn AWSPartition: !Ref AWS::Partition DefinitionString: |- { "Comment": "A state machine that orchestrates transit network changes.", "StartAt": "Check Event Type", "States": { "Check Event Type": { "Type": "Choice", "Choices": [ { "Comment": "Approval message", "Variable": "$.detail-type", "IsPresent": false, "Next": "Describe Resources" }, { "Comment": "Tag event", "Variable": "$.detail-type", "StringEquals": "Tag Change on Resource", "Next": "Describe Resources" }, { "Comment": "CloudFormation stack deletion", "Variable": "$.detail.eventName", "StringEquals": "DeleteSubnet", "Next": "Delete Transit Gateway Attachment" } ] }, "Delete Transit Gateway Attachment": { "Comment": "This deletes the relevant TGW VPC attachment, and gets triggered when there is an attempt to delete a subnet.", "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "subnet_deletion_event" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "End": true }, "Describe Resources": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "VPC", "FunctionName": "describe_resources" } } }, "Catch": [ { "ErrorEquals": ["ResourceNotFoundException"], "ResultPath": null, "Next": "Resource does not exist" } ], "Retry": [ { "ErrorEquals": ["ResourceNotFoundException"], "MaxAttempts":0 }, { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Next": "Describe Transit Gateway VPC Attachments" }, "Describe Transit Gateway VPC Attachments": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "describe_transit_gateway_vpc_attachments" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Next": "Describe TGW Route Tables" }, "Describe TGW Route Tables": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "describe_transit_gateway_route_tables" } } }, "Retry": [ { "ErrorEquals": ["RouteTableNotFoundException"], "MaxAttempts":0 }, { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Requires Approval?" }, "Requires Approval?": { "Type": "Choice", "Choices": [ { "And": [ { "Variable": "$.ApprovalRequired", "StringEquals": "yes" }, { "Not": { "Variable": "$.AdminAction", "StringEquals": "accept" } } ], "Next": "Yes" } ], "Default": "No" }, "Yes": { "Type": "Pass", "ResultPath": "$.params", "Next": "Send Approval Notification" }, "Send Approval Notification": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "ApprovalNotification", "FunctionName": "notify" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Log Event" }, "No": { "Type": "Pass", "ResultPath": "$.params", "Next": "Event Source?" }, "Event Source?": { "Type": "Choice", "Choices": [ { "Variable": "$.TagEventSource", "StringEquals": "subnet", "Next": "Resource Share Invitation" } ], "Default": "VPC Tagged?" }, "Resource Share Invitation": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "ResourceAccessManager", "FunctionName": "accept_resource_share_invitation" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Get Transit Gateway VPC Attachment State" }, "Get Transit Gateway VPC Attachment State": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "describe_transit_gateway_vpc_attachments" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "TGW Attachment Change In Progress?" }, "TGW Attachment Change In Progress?": { "Type": "Choice", "Choices": [ { "Or": [ { "Variable": "$.AttachmentState", "StringEquals": "modifying" }, { "Variable": "$.AttachmentState", "StringEquals": "pending" } ], "Next": "Wait for TGW Attachment Change To Finish" } ], "Default": "Check Route To Tgw Exists" }, "Wait for TGW Attachment Change To Finish": { "Type": "Wait", "Seconds": 15, "Next": "Get Transit Gateway VPC Attachment State" }, "Check Route To Tgw Exists": { "Type": "Choice", "Choices": [ { "Variable": "$.RouteToTgw", "IsPresent": true, "Next": "Route CRUD Operations" } ], "Default": "TGW Attachment CRUD Operations" }, "TGW Attachment CRUD Operations": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "tgw_attachment_crud_operations" } } }, "Retry": [ { "ErrorEquals": ["AttachmentCreationInProgressException"], "MaxAttempts":0 }, { "ErrorEquals": ["ResourceBusyException"], "MaxAttempts":10, "IntervalSeconds":5, "BackoffRate":1.5 }, { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["AttachmentCreationInProgressException"], "ResultPath": null, "Next": "Get Transit Gateway VPC Attachment State" }, { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "CRUD Operation Completed?" }, "CRUD Operation Completed?": { "Type": "Choice", "Choices": [ { "Variable": "$.AttachmentState", "StringEquals": "rejected", "Next": "Log Event" }, { "Variable": "$.AttachmentState", "StringEquals": "failed", "Next": "Log Event" }, { "Variable": "$.AttachmentState", "StringEquals": "deleted", "Next": "Route CRUD Operations" }, { "Variable": "$.AttachmentState", "StringEquals": "available", "Next": "Route CRUD Operations" } ], "Default": "Wait for CRUD operation to finish" }, "Wait for CRUD operation to finish": { "Type": "Wait", "Seconds": 15, "Next": "Get TGW Attachment State" }, "Get TGW Attachment State": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "get_transit_gateway_vpc_attachment_state" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "CRUD Operation Completed?" }, "VPC Tagged?": { "Type": "Choice", "Choices": [ { "And": [ { "Variable": "$.VpcTagFound", "StringEquals": "yes" }, { "Variable": "$.TgwAttachmentExist", "StringEquals": "yes" } ], "Next": "Association Changed?" } ], "Default": "Log Event" }, "Association Changed?": { "Type": "Choice", "Choices": [ { "Variable": "$.UpdateAssociationRouteTableId", "StringEquals": "yes", "Next": "Existing Association?" } ], "Default": "Get TGW Attachment Propagations" }, "Existing Association?": { "Type": "Choice", "Choices": [ { "Not": { "Variable": "$.ExistingAssociationRouteTableId", "StringEquals": "none" }, "Next": "Disassociate TGW Route Table" } ], "Default": "Associate TGW Route Table" }, "Disassociate TGW Route Table": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "disassociate_transit_gateway_route_table" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Associate TGW Route Table" }, "Associate TGW Route Table": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "associate_transit_gateway_route_table" } } }, "Retry": [ { "ErrorEquals": ["AlreadyConfiguredException"], "MaxAttempts":0 }, { "ErrorEquals": ["ResourceBusyException"], "MaxAttempts":10, "IntervalSeconds":5, "BackoffRate":1.5 }, { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":5, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["AlreadyConfiguredException"], "ResultPath": null, "Next": "TGW VPC Attachment Already Configured" }, { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Get TGW Attachment Propagations" }, "Get TGW Attachment Propagations": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "get_transit_gateway_attachment_propagations" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Enable TGW Attachment Propagations" }, "Enable TGW Attachment Propagations": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "enable_transit_gateway_route_table_propagation" } } }, "Retry": [ { "ErrorEquals": ["AlreadyConfiguredException"], "MaxAttempts":0 }, { "ErrorEquals": ["ResourceBusyException"], "MaxAttempts":10, "IntervalSeconds":5, "BackoffRate":1.5 }, { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":5, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["AlreadyConfiguredException"], "ResultPath": null, "Next": "TGW VPC Attachment Already Configured" }, { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Disable TGW Attachment Propagations" }, "Disable TGW Attachment Propagations": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "disable_transit_gateway_route_table_propagation" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":5, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Tag TGW Attachment" }, "Tag TGW Attachment": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "tag_transit_gateway_attachment" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "Log Event" }, "TGW VPC Attachment Already Configured": { "Type": "Pass", "ResultPath": "$.params", "Next": "Log Event" }, "Route CRUD Operations": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "VPC", "FunctionName": "default_route_crud_operations" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error-info", "Next": "Process Failure" } ], "Next": "VPC Tagged?" }, "Process Failure": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "GeneralFunctions", "FunctionName": "process_failure" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "Next": "Log Event" }, "Log Event": { "Type": "Parallel", "ResultPath": null, "Branches": [ { "StartAt": "Update DynamoDB", "States": { "Update DynamoDB": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "DynamoDb", "FunctionName": "put_item" } } }, "Retry": [ { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts":3, "IntervalSeconds":5, "BackoffRate":2 } ], "End": true } } }, { "StartAt": "Update Tags", "States": { "Update Tags": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "TransitGateway", "FunctionName": "update_spoke_resource_tags_if_failed" } } }, "End": true } } }, { "StartAt": "Log event in CloudWatch", "States": { "Log event in CloudWatch": { "Type": "Task", "Resource": "arn:${AWSPartition}:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "FunctionName": "${StateMachineLambdaArn}", "Payload": { "event.$": "$", "params": { "ClassName": "GeneralFunctions", "FunctionName": "log_in_cloudwatch" } } }, "End": true } } } ], "Next": "Check Status" }, "Check Status": { "Type": "Choice", "Choices": [ { "Variable": "$.error-info", "IsPresent": true, "Next": "Execution failed" } ], "Default": "Execution successful" }, "Resource does not exist": { "Type": "Pass", "End": true }, "Execution failed": { "Type": "Fail" }, "Execution successful": { "Type": "Pass", "End": true } } } OrchestratorExecutionPolicy: Type: AWS::IAM::Policy Properties: PolicyName: STNO-OrchestratorExecution-Policy Roles: - !Ref CustomResourceLambdaFunctionRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - states:StartExecution Resource: - !Ref OrchestratorStateMachine ################################################ # Transit Gateway Peering Attachment Resources # ################################################ TgwTagEventRule: Type: AWS::Events::Rule Properties: Description: Serverless Transit Network Orchestrator - Rule for tag on Transit Gateway from hub account EventPattern: { "account": [ !Ref "AWS::AccountId" ], "source": [ "aws.tag" ], "detail-type": [ "Tag Change on Resource" ], "detail": { "service": [ "ec2" ], "resource-type": [ "transit-gateway" ], "changed-tag-keys": [ !Ref TgwPeeringTag, ] } } State: ENABLED Targets: - Arn: !Sub ${TgwPeeringLambdaFunction.Arn} Id: 'TgwPeeringLambdaFunction' InputTransformer: InputPathsMap: "detail" : "$.detail" "source": "$.source" InputTemplate: | { "detail" : , "source" : } PermissionForTGWTagRule: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref "TgwPeeringLambdaFunction" Action: "lambda:InvokeFunction" Principal: "events.amazonaws.com" SourceArn: !Sub ${TgwTagEventRule.Arn} TgwPeeringLambdaFunctionRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "Allow * for describe calls" Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: !Sub TGWPeering PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* - Effect: Allow Action: - ec2:CreateTransitGatewayPeeringAttachment - ec2:DeleteTransitGatewayPeeringAttachment - ec2:CreateTags Resource: - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/* - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway-attachment/* - Effect: Allow Action: - ec2:AcceptTransitGatewayPeeringAttachment Resource: - !Sub arn:${AWS::Partition}:ec2:*:*:transit-gateway-attachment/* - Effect: Allow Action: - ec2:DescribeTransitGatewayPeeringAttachments Resource: "*" TgwPeeringLambdaFunction: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W92 reason: "does not require concurrency reservation" - id: W89 reason: "not a valid use-case for vpc" - id: W58 reason: "cw logs permission added to lambda role" Properties: Environment: Variables: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] TGW_PEERING_TAG: !Ref TgwPeeringTag TGW_ID: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] ATTACHMENT_TAG: !Ref AttachmentTag ROUTING_TAG: !Ref RoutingTag SOLUTION_VERSION: !FindInMap [SourceCode, General, Version] SOLUTION_ID: !FindInMap [Solution, Metrics, SolutionId] USER_AGENT_STRING: AwsSolution/SO0058/%VERSION% PARTITION: !Sub ${AWS::Partition} Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],"tgw_peering_attachment.zip"]] Description: Network Orchestration for AWS Transit Gateway - TGW peering attachment handler Handler: tgw_peering_attachment.main.lambda_handler MemorySize: 1536 Role: !Sub ${TgwPeeringLambdaFunctionRole.Arn} Runtime: python3.9 Timeout: 900 ######################################## # AWS Network Manager - Global Network # ######################################## GlobalNetworkSTNO: Type: AWS::NetworkManager::GlobalNetwork DeletionPolicy: Retain UpdateReplacePolicy: Retain Condition: CreateNewGlobalNetwork Properties: Description: !Sub Global Network created by Serverless Transit Network Solution deployed in ${AWS::Region} Tags: - Key: Name Value: STNO-Global-Network - Key: AWS Serverless Transit Network Orchestrator Value: !Ref 'AWS::StackId' GlobalNetworkRegistrationSTNO: Type: AWS::NetworkManager::TransitGatewayRegistration Condition: DeployIfNotChinaPartition Properties: GlobalNetworkId: !If [CreateNewGlobalNetwork, !Ref GlobalNetworkSTNO, !Ref ExistingGlobalNetworkId] TransitGatewayArn: !Sub - arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/${TransitGatewayId} - { TransitGatewayId: !If [ CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId ] } ############################ # AWS AppSync API # ############################ AppSyncPipelineApi: Type: AWS::AppSync::GraphQLApi Condition: DeployWebUiCondition Properties: Name: STNOGraphQLAPI AuthenticationType: AMAZON_COGNITO_USER_POOLS UserPoolConfig: UserPoolId: Ref: STNOUserPool AwsRegion: Ref: AWS::Region DefaultAction: DENY LogConfig: CloudWatchLogsRoleArn: !GetAtt AppSyncRole.Arn ExcludeVerboseContent: False FieldLogLevel: NONE XrayEnabled: true STNOGraphQLApiWebACLAssociation: Type: AWS::WAFv2::WebACLAssociation Condition: DeployWebUiCondition Properties: WebACLArn: !GetAtt STNOGraphQLApiWebACL.Arn ResourceArn: !GetAtt AppSyncPipelineApi.Arn GraphQLSchema: Type: "AWS::AppSync::GraphQLSchema" Condition: DeployWebUiCondition Properties: ApiId: Fn::GetAtt: - AppSyncPipelineApi - ApiId DefinitionS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/schema.graphql"]] ############################ # AWS AppSync Datasources # ############################ DynamoDBSTNOTableDatasource: Type: AWS::AppSync::DataSource Condition: DeployWebUiCondition Properties: Type: AMAZON_DYNAMODB Name: STNO_DynamoDB_Datasource ApiId: Fn::GetAtt: - "AppSyncPipelineApi" - ApiId ServiceRoleArn: !GetAtt AppSyncRole.Arn DynamoDBConfig: TableName: Ref: DynamoDbTable AwsRegion: Ref: AWS::Region UseCallerCredentials: FALSE STNOLambdaDatasource: Type: AWS::AppSync::DataSource Condition: DeployWebUiCondition Properties: Type: AWS_LAMBDA Name: STNO_Lambda_Datasource ApiId: Fn::GetAtt: - "AppSyncPipelineApi" - ApiId ServiceRoleArn: !GetAtt AppSyncRole.Arn LambdaConfig: LambdaFunctionArn: !GetAtt CustomResourceLambda.Arn ############################ # AWS AppSync Resolvers # ############################ GetActionItemsResolver: Type: "AWS::AppSync::Resolver" Condition: DeployWebUiCondition DependsOn: - GraphQLSchema Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId TypeName: "Query" FieldName: "getActionItemsFromTransitNetworkOrchestratorTables" DataSourceName: !GetAtt DynamoDBSTNOTableDatasource.Name RequestMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"], "/graphql/resolver/Query_GetActionItemsResolver_Request"]] ResponseMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"], "/graphql/resolver/Query_GetActionItemsResolver_Response"]] GetDashboardItemsResolver: Type: "AWS::AppSync::Resolver" Condition: DeployWebUiCondition DependsOn: - GraphQLSchema Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId TypeName: "Query" FieldName: "getDashboardItemsFromTransitNetworkOrchestratorTables" DataSourceName: !GetAtt DynamoDBSTNOTableDatasource.Name RequestMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/resolver/Query_GetDashboardItemsResolver_Request"]] ResponseMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/resolver/Query_GetDashboardItemsResolver_Response"]] GetVersionHistoryResolver: Type: "AWS::AppSync::Resolver" Condition: DeployWebUiCondition DependsOn: - GraphQLSchema Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId TypeName: "Query" FieldName: "getVersionHistoryForSubnetFromTransitNetworkOrchestratorTables" DataSourceName: !GetAtt DynamoDBSTNOTableDatasource.Name RequestMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/resolver/Query_GetVersionHistoryResolver_Request"]] ResponseMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/resolver/Query_GetVersionHistoryResolver_Response"]] UpdateDDBTableResolver: Type: AWS::AppSync::Resolver Condition: DeployWebUiCondition Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId TypeName: "Mutation" FieldName: "updateTransitNetworkOrchestratorTable" RequestMappingTemplate: | {} ResponseMappingTemplate: | $util.toJson($ctx.result) Kind: "PIPELINE" PipelineConfig: Functions: - !GetAtt UpdateDDBTableAppSyncFunction.FunctionId - !GetAtt LambdaAppSyncFunction.FunctionId UpdateDDBTableAppSyncFunction: Type: AWS::AppSync::FunctionConfiguration Condition: DeployWebUiCondition Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId Name: "Mutation_updateTransitNetworkOrchestratorTable_Function" Description: "Update DDB table: set status to processing" DataSourceName: !GetAtt DynamoDBSTNOTableDatasource.Name RequestMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/function/Mutation_UpdateDDBTableAppSyncFunction_Request"]] ResponseMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/function/Mutation_UpdateDDBTableAppSyncFunction_Response"]] FunctionVersion: "2018-05-29" LambdaAppSyncFunction: Type: AWS::AppSync::FunctionConfiguration Condition: DeployWebUiCondition Properties: ApiId: !GetAtt AppSyncPipelineApi.ApiId Name: "STNO_Lambda_Function" Description: "Function to receive data from appsync to invoke downstream actions" DataSourceName: !GetAtt STNOLambdaDatasource.Name RequestMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/function/LambdaAppSyncFunction_Request"]] ResponseMappingTemplateS3Location: !Join ["", ["s3://", !FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region", "/", !FindInMap ["SourceCode", "General", "KeyPrefix"],"/graphql/function/LambdaAppSyncFunction_Response"]] FunctionVersion: "2018-05-29" ############################ # Amazon Cognito User Pool # ############################ STNOUserPool: Type: AWS::Cognito::UserPool Condition: DeployWebUiCondition Properties: Schema: - AttributeDataType: String DeveloperOnlyAttribute: false Mutable: false Name: sub Required: false StringAttributeConstraints: MaxLength: 2048 MinLength: 1 - AttributeDataType: String DeveloperOnlyAttribute: false Mutable: false Name: custom:CognitoUserGr Required: false StringAttributeConstraints: MaxLength: 256 MinLength: 1 AdminCreateUserConfig: AllowAdminCreateUserOnly: true InviteMessageTemplate: EmailSubject: !Sub "Network Orchestration for AWS Transit Gateway Console Credentials (${AWS::Region})" EmailMessage: !Sub |

Here are your temporary login credentials for Serverless Transit Network Orchestrator Console: https://${ConsoleCloudFront.DomainName}

Region: ${AWS::Region}
Username: {username}
Temporary Password: {####}

AutoVerifiedAttributes: - email EmailVerificationMessage: The verification code to your new account is {####} EmailVerificationSubject: Verify your new account EnabledMfas: - SOFTWARE_TOKEN_MFA MfaConfiguration: Ref: MultiFactorAuthentication SmsVerificationMessage: The verification code to your new account is {####} AccountRecoverySetting: RecoveryMechanisms: - Name: verified_phone_number Priority: 1 - Name: verified_email Priority: 2 VerificationMessageTemplate: DefaultEmailOption: CONFIRM_WITH_CODE EmailMessage: The verification code to your new account is {####} EmailSubject: Verify your new account SmsMessage: The verification code to your new account is {####} Policies: PasswordPolicy: MinimumLength: 10 RequireLowercase: true RequireNumbers: true RequireSymbols: true RequireUppercase: true TemporaryPasswordValidityDays: 7 LambdaConfig: Fn::If: - FrontEndCognitoSAMLCondition - PostConfirmation: Fn::GetAtt: - FrontEndCognitoTriggerFunction - Arn - { } UserPoolAddOns: AdvancedSecurityMode: ENFORCED UserPoolClient: Type: AWS::Cognito::UserPoolClient Condition: DeployWebUiCondition Properties: UserPoolId: Ref: STNOUserPool ClientName: "stno-app-client" AllowedOAuthFlows: - code AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - phone - email - openid - profile - aws.cognito.signin.user.admin CallbackURLs: - !Sub "https://${ConsoleCloudFront.DomainName}/" GenerateSecret: false LogoutURLs: - !Sub "https://${ConsoleCloudFront.DomainName}/" PreventUserExistenceErrors: ENABLED RefreshTokenValidity: 1 SupportedIdentityProviders: Fn::If: - FrontEndCognitoSAMLCondition - - COGNITO - Ref: CognitoSAMLProviderNameParameter - - COGNITO UserPoolDomain: Type: AWS::Cognito::UserPoolDomain Condition: DeployWebUiCondition Properties: Domain: Ref: CognitoDomainPrefixParameter UserPoolId: Ref: STNOUserPool FrontEndCognitoSAMLProvider: Type: AWS::Cognito::UserPoolIdentityProvider Properties: ProviderName: Ref: CognitoSAMLProviderNameParameter ProviderType: SAML UserPoolId: Ref: STNOUserPool AttributeMapping: email: email ProviderDetails: MetadataURL: Ref: CognitoSAMLProviderMetadataUrlParameter Condition: FrontEndCognitoSAMLCondition ############################ # UserPool Cognito Trigger # ############################ FrontEndCognitoTriggerFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com Version: "2012-10-17" Path: / Policies: - PolicyDocument: Statement: - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/* Version: "2012-10-17" PolicyName: CloudWatchLogsPolicy - PolicyDocument: Statement: - Action: - cognito-idp:AdminAddUserToGroup Effect: Allow Resource: # referencing ReadOnlyUserPool instead of userpool/* creates a circular dependency - !Sub arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/* Version: "2012-10-17" PolicyName: AddUserToReadOnlyUserGroupPolicy Condition: FrontEndCognitoSAMLCondition FrontEndCognitoTriggerFunction: Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] S3Key: !Join ["/", [!FindInMap ["SourceCode", "General", "KeyPrefix"],"cognito-trigger.zip"]] Role: Fn::GetAtt: - FrontEndCognitoTriggerFunctionRole - Arn Description: "AWS STNO Solution (%VERSION%): Cognito Trigger. Used when a new user is confirmed in the user pool to allow for custom actions to be taken" Environment: Variables: LOG_LEVEL: !FindInMap [LambdaFunction, Logging, Level] SOLUTION_ID: !FindInMap [Solution, Metrics, SolutionId] SOLUTION_VERSION: !FindInMap [SourceCode, General, Version] Handler: index.handler Runtime: nodejs16.x Timeout: 60 DependsOn: - FrontEndCognitoTriggerFunctionRole Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: VPC for Lambda is not needed. This serverless architecture does not deploy a VPC. - id: W92 reason: ReservedConcurrentExecutions is not needed for this Lambda function. Condition: FrontEndCognitoSAMLCondition FrontEndCognitoTriggerFunctionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Ref: FrontEndCognitoTriggerFunction Principal: cognito-idp.amazonaws.com SourceArn: Fn::GetAtt: - STNOUserPool - Arn Condition: FrontEndCognitoSAMLCondition ############################ # UserPool Groups # ############################ UserPoolGroupAdmin: Type: AWS::Cognito::UserPoolGroup Condition: DeployWebUiCondition Properties: Description: "Admin can perform all read and write operations" GroupName: "AdminGroup" Precedence: 0 UserPoolId: Ref: STNOUserPool UserPoolGroupReadOnlyUsers: Type: AWS::Cognito::UserPoolGroup Condition: DeployWebUiCondition Properties: Description: "Users can only view requests" GroupName: "ReadOnlyUserGroup" Precedence: 1 UserPoolId: Ref: STNOUserPool ############################ # UserPool User - adminuser # ############################ UserPoolUserAdmin: Type: AWS::Cognito::UserPoolUser Condition: DeployWebUiCondition Properties: Username: !Ref UserPoolUsernameForAdminUser DesiredDeliveryMediums: - "EMAIL" UserAttributes: - Name: email_verified Value: 'true' - Name: email Value: Ref: ConsoleLoginInformationEmail UserPoolId: Ref: STNOUserPool UserPoolAdminUserToAdminGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Condition: DeployWebUiCondition Properties: GroupName: "AdminGroup" Username: !Ref UserPoolUsernameForAdminUser UserPoolId: Ref: STNOUserPool DependsOn: - UserPoolGroupAdmin - UserPoolUserAdmin ############################ # UserPool User - readonlyuser # ############################ UserPoolUserReadOnly: Type: AWS::Cognito::UserPoolUser Condition: DeployWebUiCondition Properties: Username: !Ref UserPoolUsernameForReadOnlyUser DesiredDeliveryMediums: - "EMAIL" UserAttributes: - Name: email_verified Value: 'true' - Name: email Value: Ref: ConsoleLoginInformationEmail UserPoolId: Ref: STNOUserPool UserPoolReadOnlyUserToReadOnlyGroup: Type: AWS::Cognito::UserPoolUserToGroupAttachment Condition: DeployWebUiCondition Properties: GroupName: "ReadOnlyUserGroup" Username: !Ref UserPoolUsernameForReadOnlyUser UserPoolId: Ref: STNOUserPool DependsOn: - UserPoolGroupReadOnlyUsers - UserPoolUserReadOnly ############################ # Amazon Cognito Identity Pool # ############################ CognitoAuthRole: Type: "AWS::IAM::Role" Condition: DeployWebUiCondition Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Federated: "cognito-identity.amazonaws.com" Action: - "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: cognito-identity.amazonaws.com:aud: !Ref IdentityPool ForAnyValue:StringLike: cognito-identity.amazonaws.com:amr: "authenticated" Path: "/" Policies: - PolicyName: STNOCognitoAuthRolePolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "cognito-identity:GetId" Resource: - !Join ["",[!Sub "arn:${AWS::Partition}:cognito-identity:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":identitypool/", !Ref IdentityPool]] IdentityPoolRoleAttachment: Type: "AWS::Cognito::IdentityPoolRoleAttachment" Condition: DeployWebUiCondition Properties: IdentityPoolId: !Ref IdentityPool Roles: authenticated: !GetAtt CognitoAuthRole.Arn IdentityPool: Type: "AWS::Cognito::IdentityPool" Condition: DeployWebUiCondition Properties: AllowUnauthenticatedIdentities: false CognitoIdentityProviders: - ClientId: !Ref UserPoolClient ProviderName: !GetAtt STNOUserPool.ProviderName ServerSideTokenCheck: false ########################## # Amazon DynamoDB Role # ########################## AppSyncRole: Type: AWS::IAM::Role Condition: DeployWebUiCondition Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: - appsync.amazonaws.com Path: / Policies: - PolicyName: STNO-Lambda-Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: - !Sub ${CustomResourceLambda.Arn} - PolicyName: STNO-Logs-Policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - PolicyName: STNO-DDB-Policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - dynamodb:UpdateItem - dynamodb:Scan - dynamodb:Query Resource: !GetAtt DynamoDbTable.Arn ########################## # S3 Bucket # ########################## ConsoleBucket: DeletionPolicy: Retain UpdateReplacePolicy: Retain Type: 'AWS::S3::Bucket' Condition: DeployWebUiCondition Properties: LoggingConfiguration: DestinationBucketName: !Ref LogsBucket LogFilePrefix: console_logs/ PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 VersioningConfiguration: Status: Enabled ConsoleBucketPolicy: Type: 'AWS::S3::BucketPolicy' Condition: DeployWebUiCondition Properties: Bucket: !Ref ConsoleBucket PolicyDocument: Statement: - Effect: Allow Principal: CanonicalUser: !GetAtt ConsoleOriginAccessIdentity.S3CanonicalUserId Action: 's3:GetObject' Resource: !Join ["/", [!GetAtt ConsoleBucket.Arn, "*"]] - Effect: Deny Action: s3:* Resource: - !GetAtt ConsoleBucket.Arn - !Join ["/", [!GetAtt ConsoleBucket.Arn, "*"]] Condition: Bool: aws:SecureTransport: False Principal: "*" LogsBucket: DeletionPolicy: Retain UpdateReplacePolicy: Retain Type: AWS::S3::Bucket Condition: DeployWebUiCondition Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "This is an access log bucket for the resource 'ConsoleBucket' and does not require access logging to be configured." Properties: AccessControl: LogDeliveryWrite OwnershipControls: Rules: - ObjectOwnership: ObjectWriter BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 PublicAccessBlockConfiguration: BlockPublicAcls: True BlockPublicPolicy: True IgnorePublicAcls: True RestrictPublicBuckets: True LoggingConfiguration: LogFilePrefix: logsbucket_logs/ VersioningConfiguration: Status: Enabled LogsBucketS3BucketPolicy: Type: AWS::S3::BucketPolicy Condition: DeployWebUiCondition Properties: Bucket: !Ref LogsBucket PolicyDocument: Statement: - Sid: DenyDeleteBucket Effect: Deny Principal: "*" Action: s3:DeleteBucket Resource: !Sub arn:${AWS::Partition}:s3:::${LogsBucket} ########################## # Cloud Front # ########################## ConsoleOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Condition: DeployWebUiCondition Properties: CloudFrontOriginAccessIdentityConfig: Comment: !Sub "access-identity-${ConsoleBucket}" AllowListedCidr: Condition: DeployWebUiCondition Type: AWS::WAFv2::IPSet Properties: Name: STNO-Api-Allowlist Scope: REGIONAL Description: CIDR ranges allowed access to STNO web UI IPAddressVersion: IPV4 Addresses: !Ref AllowListedRanges STNOGraphQLApiWebACL: Type: AWS::WAFv2::WebACL Condition: DeployWebUiCondition Properties: Name: "STNO-Api-WebACL" Scope: REGIONAL DefaultAction: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: "STNO-Api-WebACL-Metrics" Rules: - Name: "AllowlistRule" Priority: 0 Action: Allow: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: "STNO-Api-AllowlistRuleMetric" Statement: IPSetReferenceStatement: Arn: !GetAtt AllowListedCidr.Arn - Name: "AWSManagedRules" Priority: 1 OverrideAction: None: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: "STNO-Api-AWSManagedRulesMetric" Statement: ManagedRuleGroupStatement: VendorName: AWS Name: "AWSManagedRulesCommonRuleSet" ExcludedRules: [] SecurityHeaderFunction: Type: AWS::CloudFront::Function Condition: DeployWebUiCondition Properties: Name: !Sub "STNO-SecurityHeaderFunction-${AWS::Region}" AutoPublish: true FunctionCode: | function handler = (event) => { const response = event.response; const headers = response.headers; //Set new headers headers['strict-transport-security'] = [{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' }]; headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}]; headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}]; headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}]; headers['referrer-policy'] = [{key: 'Referrer-Policy', value: 'same-origin'}]; return response; }; FunctionConfig: Runtime: cloudfront-js-1.0 Comment: "Adds recommended security-related http headers to CloudFront responses" ConsoleCloudFront: Type: AWS::CloudFront::Distribution Condition: DeployWebUiCondition Metadata: cfn_nag: rules_to_suppress: - id: W70 reason: Minimum protocol version property is not necessary if the distribution uses the CloudFront domain name such as d111111abcdef8.cloudfront.net # Seems to be a bug with CFN Nag - adding this property didn't remove the warning. # Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-viewercertificate.html Properties: DistributionConfig: Comment: "Cloud Front distribution for Serverless Transit Network Orchestrator Console" Origins: - Id: S3-solution-website DomainName: !Sub "${ConsoleBucket}.s3.${AWS::Region}.amazonaws.com" OriginPath: /console S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${ConsoleOriginAccessIdentity}" DefaultCacheBehavior: TargetOriginId: S3-solution-website AllowedMethods: - GET - HEAD - OPTIONS - PUT - POST - PATCH - DELETE CachedMethods: - GET - HEAD - OPTIONS ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https IPV6Enabled: true DefaultRootObject: "index.html" CustomErrorResponses: - ErrorCode: 404 ResponsePagePath: "/index.html" ResponseCode: 200 - ErrorCode: 403 ResponsePagePath: "/index.html" ResponseCode: 200 ViewerCertificate: CloudFrontDefaultCertificate: true Enabled: true HttpVersion: 'http2' Logging: Bucket: !Join ['',[!Ref LogsBucket,'.s3.amazonaws.com']] IncludeCookies: false Prefix: cloudfront_logs/ ConsoleIAMPolicy: Type: "AWS::IAM::Policy" Condition: DeployWebUiCondition Properties: PolicyName: "STNOConsolePolicy" Roles: - Ref: "StateMachineLambdaFunctionRole" PolicyDocument: Version: 2012-10-17 Statement: - Effect: "Allow" Action: - s3:putObject - s3:deleteObject - s3:deleteBucket Resource: - !Join ["", [!Sub "arn:${AWS::Partition}:s3:::", Ref: "ConsoleBucket"]] - !Join ["", [!Sub "arn:${AWS::Partition}:s3:::", Ref: "ConsoleBucket", "/*" ]] - Effect: "Allow" Action: - s3:getObject Resource: - !Join ["", [!Sub "arn:${AWS::Partition}:s3:::",!FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region"]] - !Join ["", [!Sub "arn:${AWS::Partition}:s3:::",!FindInMap ["SourceCode", "General", "S3Bucket"], "-", Ref: "AWS::Region","/*"]] CopyWebConsoleArtifacts: Type: "Custom::ConsoleDeploy" Condition: DeployWebUiCondition Properties: ServiceToken: !Sub ${CustomResourceLambda.Arn} SrcBucket: !Join ["-", [!FindInMap ["SourceCode", "General", "S3Bucket"], Ref: "AWS::Region"]] SrcPath: !FindInMap ["SourceCode", "General", "KeyPrefix"] ConsoleBucket: !Ref ConsoleBucket AwsUserPoolsId: !Ref STNOUserPool AwsUserPoolsWebClientId: !Ref UserPoolClient AwsCognitoIdentityPoolId: !Ref IdentityPool AwsAppsyncGraphqlEndpoint: !GetAtt AppSyncPipelineApi.GraphQLUrl AwsContentDeliveryBucket: !Ref ConsoleBucket AwsContentDeliveryUrl: !Sub "https://${ConsoleCloudFront.DomainName}" AwsCognitoDomainPrefix: !Ref CognitoDomainPrefixParameter SendCloudFormationParameters: Type: Custom::SendCFNParameters Properties: Uuid: !GetAtt CreateUniqueID.UUID PrincipalType: !Ref PrincipalType ApprovalNotification: !Ref ApprovalNotification AuditTrailRetentionPeriod: !FindInMap ["LogRetention", "AuditTrail", "RetentionPeriod"] DefaultRoute: !Ref DefaultRoute CreatedNewTransitGateway: !If [CreateNewTransitGateway, 'Yes', 'No'] DeployWebUI: !Ref DeployWebUi ServiceToken: !Sub ${CustomResourceLambda.Arn} CloudWatchLogFailures: Type: AWS::Logs::LogGroup Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Using service default encryption https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-protection.html Properties: RetentionInDays: !FindInMap ["LogRetention", "CloudWatchLogs", "RetentionPeriod"] LogGroupName: STNO-Hub-Failures CloudWatchLogActions: Type: AWS::Logs::LogGroup Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Using service default encryption https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-protection.html Properties: RetentionInDays: !FindInMap ["LogRetention", "CloudWatchLogs", "RetentionPeriod"] LogGroupName: STNO-Hub-Actions CloudWatchLogStateMachine: Type: AWS::Logs::LogGroup Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Using service default encryption https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-protection.html Properties: RetentionInDays: !FindInMap [ "LogRetention", "CloudWatchLogs", "RetentionPeriod" ] LogGroupName: STNO-StateMachine # AppRegistry Application Application: Type: AWS::ServiceCatalogAppRegistry::Application Condition: DeployIfNotChinaPartition Properties: Description: Service Catalog application to track and manage all your resources. The SolutionID SO0058 is and SolutionVersion is %VERSION%. The ApplicationType is AWS-Solutions and SolutionDomain is CloudFoundations. Name: !Join - "-" - - !FindInMap [ Solution, Data, "AppRegistryApplicationName" ] - !Ref AWS::Region - !Ref AWS::AccountId Tags: { 'SolutionID': !FindInMap [ Solution, Metrics, SolutionId ], 'SolutionName': !FindInMap [ Solution, Data, SolutionName ], 'SolutionVersion': "%VERSION%", 'ApplicationType': 'AWS-Solutions', 'SolutionDomain': 'CloudFoundations', 'CloudFoundations:CostOptimizerForWorkspaces': !Ref "AWS::StackName" } AppRegistryApplicationStackAssociation: Type: AWS::ServiceCatalogAppRegistry::ResourceAssociation Condition: DeployIfNotChinaPartition Properties: Application: !GetAtt Application.Id Resource: !Ref AWS::StackId ResourceType: CFN_STACK DefaultApplicationAttributeGroup: Type: AWS::ServiceCatalogAppRegistry::AttributeGroup Condition: DeployIfNotChinaPartition Properties: Name: !Sub 'AttrGrp-${AWS::Region}-${AWS::StackName}' Description: Network Orchestration for AWS Transit Gateway. Attributes: { "ApplicationType": "AWS Solution", "Author": "CloudFoundations", "Team": "CloudFoundations", "Version": "%VERSION%", "SolutionID": !FindInMap [ Solution, Metrics, SolutionId ], "SolutionName": !FindInMap [ Solution, Data, SolutionName ], } Tags: { 'CloudFoundations:NetworkOrchestrationForAWSTransitGateway': !Ref "AWS::StackName" } AppRegistryApplicationAttributeAssociation: Type: AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation Condition: DeployIfNotChinaPartition Properties: Application: !GetAtt Application.Id AttributeGroup: !GetAtt DefaultApplicationAttributeGroup.Id ########################## # Outputs # ########################## Outputs: UUID: Value: !GetAtt CreateUniqueID.UUID TGWId: Value: !If [CreateNewTransitGateway, !Ref AWSTransitGateway, !Ref ExistingTransitGatewayId] GlobalNetworkId: Value: !If [CreateNewGlobalNetwork, !Ref GlobalNetworkSTNO, !Ref ExistingGlobalNetworkId] FlatTGWRT: Condition: CreateNewTransitGateway Value: !Ref FlatTGWRouteTable IsolatedTGWRT: Condition: CreateNewTransitGateway Value: !Ref IsolatedTGWRouteTable OnpremTGWRT: Condition: CreateNewTransitGateway Value: !Ref OnPremTGWRouteTable InfrastructureTGWRT: Condition: CreateNewTransitGateway Value: !Ref InfrastructureTGWRouteTable ResourceShareArn: Value: !GetAtt TGWResourceShare.Arn SolutionVersion: Description: Version Number Value: !FindInMap [SourceCode, General, Version] Console: Description: The link to the console Value: !Sub https://${ConsoleCloudFront.DomainName} Condition: DeployWebUiCondition ConsoleS3Bucket: Description: The s3 bucket that stores the contents of the website Value: !Ref ConsoleBucket Condition: DeployWebUiCondition UserPoolClientID: Description: The User Pool AppClient ID used when logging into the UserPool Value: !Ref UserPoolClient Condition: DeployWebUiCondition UserPoolID: Description: The User Pool ID Value: !Ref STNOUserPool Condition: DeployWebUiCondition CognitoDomain: Description: Cognito hosted domain Value: Fn::Join: - "" - - https:// - Ref: UserPoolDomain - .auth. - Ref: AWS::Region - .amazoncognito.com Condition: DeployWebUiCondition IdentityPoolID: Description: The Identity Pool ID. Value: !Ref IdentityPool Condition: DeployWebUiCondition DynamoDBTable: Description: The name of the Amazon DynamoDB table containing data of network requests Value: !Ref DynamoDbTable AppSyncAPI: Description: The AppSync API Value: !Ref AppSyncPipelineApi Condition: DeployWebUiCondition