diff --git a/internal/pkg/cli/deploy/env.go b/internal/pkg/cli/deploy/env.go index bee9c1e96b4..7d3cda08a2f 100644 --- a/internal/pkg/cli/deploy/env.go +++ b/internal/pkg/cli/deploy/env.go @@ -270,6 +270,15 @@ type DeployEnvironmentInput struct { Detach bool } +// AdditionalAssumeRolePermissions helper method to export the additional assume role permissions of environment roles +// from the manifest. +func (dei DeployEnvironmentInput) AdditionalAssumeRolePermissions() []string { + if dei.Manifest == nil { + return []string{} + } + return dei.Manifest.AdditionalAssumeRolePermissions +} + // GenerateCloudFormationTemplate returns the environment stack's template and parameter configuration. func (d *envDeployer) GenerateCloudFormationTemplate(in *DeployEnvironmentInput) (*GenerateCloudFormationTemplateOutput, error) { stackInput, err := d.buildStackInput(in) @@ -420,18 +429,19 @@ func (d *envDeployer) buildStackInput(in *DeployEnvironmentInput) (*cfnstack.Env RootDomainHostedZoneId: d.app.DomainHostedZoneID, AppDomainHostedZoneId: appHostedZoneID, }, - AdditionalTags: d.app.Tags, - Addons: addons, - CustomResourcesURLs: in.CustomResourcesURLs, - ArtifactBucketARN: awss3.FormatARN(partition.ID(), resources.S3Bucket), - ArtifactBucketKeyARN: resources.KMSKeyARN, - CIDRPrefixListIDs: cidrPrefixListIDs, - PublicALBSourceIPs: d.publicALBSourceIPs(in), - Mft: in.Manifest, - ForceUpdate: in.ForceNewUpdate, - RawMft: in.RawManifest, - PermissionsBoundary: in.PermissionsBoundary, - Version: in.Version, + AdditionalTags: d.app.Tags, + Addons: addons, + CustomResourcesURLs: in.CustomResourcesURLs, + ArtifactBucketARN: awss3.FormatARN(partition.ID(), resources.S3Bucket), + ArtifactBucketKeyARN: resources.KMSKeyARN, + CIDRPrefixListIDs: cidrPrefixListIDs, + PublicALBSourceIPs: d.publicALBSourceIPs(in), + Mft: in.Manifest, + AdditionalAssumeRolePermissions: in.AdditionalAssumeRolePermissions(), + ForceUpdate: in.ForceNewUpdate, + RawMft: in.RawManifest, + PermissionsBoundary: in.PermissionsBoundary, + Version: in.Version, }, nil } diff --git a/internal/pkg/cli/env_init.go b/internal/pkg/cli/env_init.go index 10d26863a68..fb5a1326c31 100644 --- a/internal/pkg/cli/env_init.go +++ b/internal/pkg/cli/env_init.go @@ -151,6 +151,7 @@ type initEnvVars struct { importCerts []string // Additional existing ACM certificates to use. internalALBSubnets []string // Subnets to be used for internal ALB placement. allowVPCIngress bool // True means the env stack will create ingress to the internal ALB from ports 80/443. + federatedSession bool // True means, that the following additional permissions are added to the environment manager role trust policy: sts:SetSourceIdentity, sts:TagSession. tempCreds tempCredsVars // Temporary credentials to initialize the environment. Mutually exclusive with the profile. region string // The region to create the environment in. @@ -747,10 +748,11 @@ func (o *initEnvOpts) deployEnv(app *config.Application) error { Domain: app.Domain, AccountPrincipalARN: caller.RootUserARN, }, - AdditionalTags: app.Tags, - ArtifactBucketARN: artifactBucketARN, - ArtifactBucketKeyARN: resources.KMSKeyARN, - PermissionsBoundary: app.PermissionsBoundary, + AdditionalTags: app.Tags, + ArtifactBucketARN: artifactBucketARN, + ArtifactBucketKeyARN: resources.KMSKeyARN, + PermissionsBoundary: app.PermissionsBoundary, + AdditionalAssumeRolePermissions: o.additionalAssumeRolePermissions(), } if err := o.cleanUpDanglingRoles(o.appName, o.name); err != nil { @@ -770,6 +772,13 @@ func (o *initEnvOpts) deployEnv(app *config.Application) error { return nil } +func (o *initEnvOpts) additionalAssumeRolePermissions() (permissions []string) { + if o.federatedSession { + permissions = append(permissions, "sts:SetSourceIdentity", "sts:TagSession") + } + return +} + func (o *initEnvOpts) addToStackset(opts *deploycfn.AddEnvToAppOpts) error { if err := o.appDeployer.AddEnvToApp(opts); err != nil { return fmt.Errorf("add env %s to application %s: %w", opts.EnvName, opts.App.Name, err) @@ -867,11 +876,12 @@ func (o *initEnvOpts) tryDeletingEnvRoles(app, env string) { func (o *initEnvOpts) writeManifest() (string, error) { customizedEnv := &config.CustomizeEnv{ - ImportVPC: o.importVPCConfig(), - VPCConfig: o.adjustVPCConfig(), - ImportCertARNs: o.importCerts, - InternalALBSubnets: o.internalALBSubnets, - EnableInternalALBVPCIngress: o.allowVPCIngress, + ImportVPC: o.importVPCConfig(), + VPCConfig: o.adjustVPCConfig(), + ImportCertARNs: o.importCerts, + InternalALBSubnets: o.internalALBSubnets, + EnableInternalALBVPCIngress: o.allowVPCIngress, + AdditionalAssumeRolePermissions: o.additionalAssumeRolePermissions(), } if customizedEnv.IsEmpty() { customizedEnv = nil @@ -973,6 +983,7 @@ func buildEnvInitCmd() *cobra.Command { cmd.Flags().StringSliceVar(&vars.internalALBSubnets, internalALBSubnetsFlag, nil, internalALBSubnetsFlagDescription) cmd.Flags().BoolVar(&vars.allowVPCIngress, allowVPCIngressFlag, false, allowVPCIngressFlagDescription) cmd.Flags().BoolVar(&vars.defaultConfig, defaultConfigFlag, false, defaultConfigFlagDescription) + cmd.Flags().BoolVar(&vars.federatedSession, allowFederatedSessionFlag, false, allowFederatedSessionFlagDescription) flags := pflag.NewFlagSet("Common", pflag.ContinueOnError) flags.AddFlag(cmd.Flags().Lookup(appFlag)) @@ -984,6 +995,7 @@ func buildEnvInitCmd() *cobra.Command { flags.AddFlag(cmd.Flags().Lookup(regionFlag)) flags.AddFlag(cmd.Flags().Lookup(defaultConfigFlag)) flags.AddFlag(cmd.Flags().Lookup(allowDowngradeFlag)) + flags.AddFlag(cmd.Flags().Lookup(allowFederatedSessionFlag)) resourcesImportFlags := pflag.NewFlagSet("Import Existing Resources", pflag.ContinueOnError) resourcesImportFlags.AddFlag(cmd.Flags().Lookup(vpcIDFlag)) diff --git a/internal/pkg/cli/env_init_test.go b/internal/pkg/cli/env_init_test.go index 55719053d12..af7b722df20 100644 --- a/internal/pkg/cli/env_init_test.go +++ b/internal/pkg/cli/env_init_test.go @@ -1023,6 +1023,7 @@ func TestInitEnvOpts_Execute(t *testing.T) { testCases := map[string]struct { enableContainerInsights bool allowDowngrade bool + allowFederatedSession bool setupMocks func(m *initEnvExecuteMocks) wantedErrorS string }{ @@ -1154,6 +1155,7 @@ func TestInitEnvOpts_Execute(t *testing.T) { "success": { enableContainerInsights: true, allowDowngrade: true, + allowFederatedSession: true, setupMocks: func(m *initEnvExecuteMocks) { m.store.EXPECT().GetApplication("phonetool").Return(&config.Application{Name: "phonetool"}, nil) m.store.EXPECT().CreateEnvironment(&config.Environment{ @@ -1338,6 +1340,7 @@ func TestInitEnvOpts_Execute(t *testing.T) { EnableContainerInsights: tc.enableContainerInsights, }, allowAppDowngrade: tc.allowDowngrade, + federatedSession: tc.allowFederatedSession, }, store: m.store, envDeployer: m.deployer, diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go index 060c04dd0c8..2e188917dd1 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -132,6 +132,7 @@ const ( enableContainerInsightsFlag = "container-insights" defaultConfigFlag = "default-config" + allowFederatedSessionFlag = "federated-session" accessKeyIDFlag = "aws-access-key-id" secretAccessKeyFlag = "aws-secret-access-key" @@ -406,6 +407,7 @@ Cannot be specified with --default-config or any of the --override flags.` enableContainerInsightsFlagDescription = "Optional. Enable CloudWatch Container Insights." defaultConfigFlagDescription = "Optional. Skip prompting and use default environment configuration." + allowFederatedSessionFlagDescription = "Optional. Shorthand to add additional permissions to the Assume Role policy required for federated sessions with a source identity or transitive session tags." profileFlagDescription = "Name of the profile for the environment account." accessKeyIDFlagDescription = "Optional. An AWS access key for the environment account." diff --git a/internal/pkg/cli/init.go b/internal/pkg/cli/init.go index 770030f4a6b..057b6c16c49 100644 --- a/internal/pkg/cli/init.go +++ b/internal/pkg/cli/init.go @@ -485,6 +485,7 @@ func (o *initOpts) deployEnv() error { // Set the application name from app init to the env init command, and check whether a flag has been passed for envName. initEnvCmd.appName = *o.appName initEnvCmd.name = o.initVars.envName + initEnvCmd.federatedSession = true } if err := o.askEnvNameAndMaybeInit(); err != nil { diff --git a/internal/pkg/config/env.go b/internal/pkg/config/env.go index 1e45b529737..389aeca587a 100644 --- a/internal/pkg/config/env.go +++ b/internal/pkg/config/env.go @@ -30,11 +30,12 @@ type Environment struct { // CustomizeEnv represents the custom environment config. type CustomizeEnv struct { - ImportVPC *ImportVPC `json:"importVPC,omitempty"` - VPCConfig *AdjustVPC `json:"adjustVPC,omitempty"` - ImportCertARNs []string `json:"importCertARNs,omitempty"` - InternalALBSubnets []string `json:"internalALBSubnets,omitempty"` - EnableInternalALBVPCIngress bool `json:"enableInternalALBVPCIngress,omitempty"` + ImportVPC *ImportVPC `json:"importVPC,omitempty"` + VPCConfig *AdjustVPC `json:"adjustVPC,omitempty"` + ImportCertARNs []string `json:"importCertARNs,omitempty"` + InternalALBSubnets []string `json:"internalALBSubnets,omitempty"` + EnableInternalALBVPCIngress bool `json:"enableInternalALBVPCIngress,omitempty"` + AdditionalAssumeRolePermissions []string `json:"additionalAssumeRolePermissions,omitempty"` } // IsEmpty returns true if CustomizeEnv is an empty struct. @@ -42,7 +43,7 @@ func (c *CustomizeEnv) IsEmpty() bool { if c == nil { return true } - return c.ImportVPC == nil && c.VPCConfig == nil && len(c.ImportCertARNs) == 0 && len(c.InternalALBSubnets) == 0 && !c.EnableInternalALBVPCIngress + return c.ImportVPC == nil && c.VPCConfig == nil && len(c.ImportCertARNs) == 0 && len(c.InternalALBSubnets) == 0 && !c.EnableInternalALBVPCIngress && len(c.AdditionalAssumeRolePermissions) == 0 } // ImportVPC holds the fields to import VPC resources. diff --git a/internal/pkg/deploy/cloudformation/stack/env.go b/internal/pkg/deploy/cloudformation/stack/env.go index 8b82e5f978d..a21243584ad 100644 --- a/internal/pkg/deploy/cloudformation/stack/env.go +++ b/internal/pkg/deploy/cloudformation/stack/env.go @@ -102,18 +102,19 @@ type EnvConfig struct { CustomResourcesURLs map[string]string // Mapping of Custom Resource Function Name to the S3 URL where the function zip file is stored. // User inputs. - ImportVPCConfig *config.ImportVPC // Optional configuration if users have an existing VPC. - AdjustVPCConfig *config.AdjustVPC // Optional configuration if users want to override default VPC configuration. - ImportCertARNs []string // Optional configuration if users want to import certificates. - InternalALBSubnets []string // Optional configuration if users want to specify internal ALB placement. - AllowVPCIngress bool // Optional configuration to allow access to internal ALB from ports 80/443. - CIDRPrefixListIDs []string // Optional configuration to specify public security group ingress based on prefix lists. - PublicALBSourceIPs []string // Optional configuration to specify public security group ingress based on customer given source IPs. - InternalLBSourceIPs []string // Optional configuration to specify private security group ingress based on customer given source IPs. - Telemetry *config.Telemetry // Optional observability and monitoring configuration. - Mft *manifest.Environment // Unmarshaled and interpolated manifest object. - RawMft string // Content of the environment manifest with env var interpolation only. - ForceUpdate bool + ImportVPCConfig *config.ImportVPC // Optional configuration if users have an existing VPC. + AdjustVPCConfig *config.AdjustVPC // Optional configuration if users want to override default VPC configuration. + ImportCertARNs []string // Optional configuration if users want to import certificates. + InternalALBSubnets []string // Optional configuration if users want to specify internal ALB placement. + AllowVPCIngress bool // Optional configuration to allow access to internal ALB from ports 80/443. + CIDRPrefixListIDs []string // Optional configuration to specify public security group ingress based on prefix lists. + PublicALBSourceIPs []string // Optional configuration to specify public security group ingress based on customer given source IPs. + InternalLBSourceIPs []string // Optional configuration to specify private security group ingress based on customer given source IPs. + Telemetry *config.Telemetry // Optional observability and monitoring configuration. + AdditionalAssumeRolePermissions []string // Optional configuration to specify additional permissions to put into the Environment Manager Role for that environment. + Mft *manifest.Environment // Unmarshaled and interpolated manifest object. + RawMft string // Content of the environment manifest with env var interpolation only. + ForceUpdate bool } func (cfg *EnvConfig) loadCustomResourceURLs(crs []uploadable) error { @@ -202,18 +203,19 @@ func (e *Env) Template() (string, error) { forceUpdateID = id.String() } content, err := e.parser.ParseEnv(&template.EnvOpts{ - AppName: e.in.App.Name, - EnvName: e.in.Name, - CustomResources: crs, - Addons: addons, - ArtifactBucketARN: e.in.ArtifactBucketARN, - ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, - PermissionsBoundary: e.in.PermissionsBoundary, - PublicHTTPConfig: e.publicHTTPConfig(), - VPCConfig: vpcConfig, - PrivateHTTPConfig: e.privateHTTPConfig(), - Telemetry: e.telemetryConfig(), - CDNConfig: e.cdnConfig(), + AppName: e.in.App.Name, + EnvName: e.in.Name, + CustomResources: crs, + Addons: addons, + ArtifactBucketARN: e.in.ArtifactBucketARN, + ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, + PermissionsBoundary: e.in.PermissionsBoundary, + PublicHTTPConfig: e.publicHTTPConfig(), + VPCConfig: vpcConfig, + PrivateHTTPConfig: e.privateHTTPConfig(), + Telemetry: e.telemetryConfig(), + CDNConfig: e.cdnConfig(), + AdditionalAssumeRolePermissions: e.in.AdditionalAssumeRolePermissions, LatestVersion: e.in.Version, SerializedManifest: string(e.in.RawMft), @@ -414,9 +416,10 @@ type BootstrapEnv Env // Template returns the CloudFormation template to bootstrap environment resources. func (e *BootstrapEnv) Template() (string, error) { content, err := e.parser.ParseEnvBootstrap(&template.EnvOpts{ - ArtifactBucketARN: e.in.ArtifactBucketARN, - ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, - PermissionsBoundary: e.in.PermissionsBoundary, + ArtifactBucketARN: e.in.ArtifactBucketARN, + ArtifactBucketKeyARN: e.in.ArtifactBucketKeyARN, + PermissionsBoundary: e.in.PermissionsBoundary, + AdditionalAssumeRolePermissions: e.in.AdditionalAssumeRolePermissions, }) if err != nil { return "", err diff --git a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go index ed16c91c200..411d47eb8e6 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_integration_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_integration_test.go @@ -263,6 +263,33 @@ network: }(), wantedFileName: "template-with-importedvpc-flowlogs.yml", }, + "generate template with additional assume role permissions": { + input: func() *stack.EnvConfig { + rawMft := `name: test +type: Environment + +additionalAssumeRolePermissions: + - sts:SetSourceIdentity + - sts:TagSession` + var mft manifest.Environment + err := yaml.Unmarshal([]byte(rawMft), &mft) + require.NoError(t, err) + return &stack.EnvConfig{ + Version: "1.x", + App: deploy.AppInformation{ + AccountPrincipalARN: "arn:aws:iam::000000000:root", + Name: "demo", + }, + Name: "test", + ArtifactBucketARN: "arn:aws:s3:::mockbucket", + ArtifactBucketKeyARN: "arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab", + Mft: &mft, + AdditionalAssumeRolePermissions: mft.AdditionalAssumeRolePermissions, + RawMft: rawMft, + } + }(), + wantedFileName: "template-with-additional-assume-role-permissions.yml", + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/internal/pkg/deploy/cloudformation/stack/env_test.go b/internal/pkg/deploy/cloudformation/stack/env_test.go index ddfac11066f..8b2927f5282 100644 --- a/internal/pkg/deploy/cloudformation/stack/env_test.go +++ b/internal/pkg/deploy/cloudformation/stack/env_test.go @@ -170,10 +170,11 @@ func TestEnv_Template(t *testing.T) { Telemetry: &template.Telemetry{ EnableContainerInsights: false, }, - ArtifactBucketARN: "arn:aws:s3:::mockbucket", - SerializedManifest: "name: env\ntype: Environment\n", - ForceUpdateID: "mockPreviousForceUpdateID", - DelegateDNS: true, + ArtifactBucketARN: "arn:aws:s3:::mockbucket", + SerializedManifest: "name: env\ntype: Environment\n", + ForceUpdateID: "mockPreviousForceUpdateID", + DelegateDNS: true, + AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"}, HostedZones: &template.HostedZones{ RootDomainHostedZoneId: "Z00ABC", AppDomainHostedZoneId: "Z00DEF", @@ -1090,6 +1091,20 @@ func TestBootstrapEnv_Template(t *testing.T) { }, expectedOutput: "mockTemplate", }, + "should contain additional permissions": { + in: &EnvConfig{ + AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"}, + }, + setupMock: func(m *mocks.MockenvReadParser) { + m.EXPECT().ParseEnvBootstrap(gomock.Any(), gomock.Any()).DoAndReturn(func(data *template.EnvOpts, options ...template.ParseOption) (*template.Content, error) { + require.Equal(t, &template.EnvOpts{ + AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"}, + }, data) + return &template.Content{Buffer: bytes.NewBufferString("mockTemplate")}, nil + }) + }, + expectedOutput: "mockTemplate", + }, } for name, tc := range testCases { @@ -1275,7 +1290,8 @@ func mockDeployEnvironmentInput() *EnvConfig { "DNSDelegationFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/mockkey2", "CustomDomainFunction": "https://mockbucket.s3-us-west-2.amazonaws.com/mockkey4", }, - ArtifactBucketARN: "arn:aws:s3:::mockbucket", + ArtifactBucketARN: "arn:aws:s3:::mockbucket", + AdditionalAssumeRolePermissions: []string{"sts:TagSession", "sts:SetSourceIdentity"}, Mft: &manifest.Environment{ Workload: manifest.Workload{ Name: aws.String("env"), diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-additional-assume-role-permissions.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-additional-assume-role-permissions.yml new file mode 100644 index 00000000000..2204223c0ee --- /dev/null +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-additional-assume-role-permissions.yml @@ -0,0 +1,1221 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +Description: CloudFormation environment template for infrastructure shared among Copilot workloads. +Metadata: + Manifest: | + name: test + type: Environment + + additionalAssumeRolePermissions: + - sts:SetSourceIdentity + - sts:TagSession +Parameters: + AppName: + Type: String + EnvironmentName: + Type: String + ALBWorkloads: + Type: String + InternalALBWorkloads: + Type: String + EFSWorkloads: + Type: String + NATWorkloads: + Type: String + AppRunnerPrivateWorkloads: + Type: String + ToolsAccountPrincipalARN: + Type: String + AppDNSName: + Type: String + AppDNSDelegationRole: + Type: String + Aliases: + Type: String + CreateHTTPSListener: + Type: String + AllowedValues: [true, false] + CreateInternalHTTPSListener: + Type: String + AllowedValues: [true, false] + ServiceDiscoveryEndpoint: + Type: String +Conditions: + CreateALB: + !Not [!Equals [ !Ref ALBWorkloads, "" ]] + CreateInternalALB: + !Not [!Equals [ !Ref InternalALBWorkloads, "" ]] + DelegateDNS: + !Not [!Equals [ !Ref AppDNSName, "" ]] + ExportHTTPSListener: !And + - !Condition CreateALB + - !Equals [ !Ref CreateHTTPSListener, true ] + ExportInternalHTTPSListener: !And + - !Condition CreateInternalALB + - !Equals [ !Ref CreateInternalHTTPSListener, true ] + CreateEFS: + !Not [!Equals [ !Ref EFSWorkloads, ""]] + CreateNATGateways: + !Not [!Equals [ !Ref NATWorkloads, ""]] + CreateAppRunnerVPCEndpoint: + !Not [!Equals [ !Ref AppRunnerPrivateWorkloads, ""]] + ManagedAliases: !And + - !Condition DelegateDNS + - !Not [!Equals [ !Ref Aliases, "" ]] +Resources: + # The CloudformationExecutionRole definition must be immediately followed with DeletionPolicy: Retain. + # See #1533. + CloudformationExecutionRole: + Metadata: + 'aws:copilot:description': 'An IAM Role for AWS CloudFormation to manage resources' + DeletionPolicy: Retain + Type: AWS::IAM::Role + Properties: + RoleName: !Sub ${AWS::StackName}-CFNExecutionRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - 'cloudformation.amazonaws.com' + Action: sts:AssumeRole + Path: / + Policies: + - PolicyName: executeCfn + # This policy is more permissive than the managed PowerUserAccess + # since it allows arbitrary role creation, which is needed for the + # ECS task role specified by the customers. + PolicyDocument: + Version: '2012-10-17' + Statement: + - + Effect: Allow + NotAction: + - 'organizations:*' + - 'account:*' + Resource: '*' + - + Effect: Allow + Action: + - 'organizations:DescribeOrganization' + - 'account:ListRegions' + Resource: '*' + + EnvironmentManagerRole: + Metadata: + 'aws:copilot:description': 'An IAM Role to describe resources in your environment' + DeletionPolicy: Retain + Type: AWS::IAM::Role + DependsOn: CloudformationExecutionRole + Properties: + RoleName: !Sub ${AWS::StackName}-EnvManagerRole + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: !Sub ${ToolsAccountPrincipalARN} + Action: + - sts:AssumeRole + - sts:SetSourceIdentity + - sts:TagSession + Path: / + Policies: + - PolicyName: root + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: CloudwatchLogs + Effect: Allow + Action: [ + "logs:GetLogRecord", + "logs:GetQueryResults", + "logs:StartQuery", + "logs:GetLogEvents", + "logs:DescribeLogStreams", + "logs:StopQuery", + "logs:TestMetricFilter", + "logs:FilterLogEvents", + "logs:GetLogGroupFields", + "logs:GetLogDelivery" + ] + Resource: "*" + - Sid: Cloudwatch + Effect: Allow + Action: [ + "cloudwatch:DescribeAlarms" + ] + Resource: "*" + - Sid: ECS + Effect: Allow + Action: [ + "ecs:ListAttributes", + "ecs:ListTasks", + "ecs:DescribeServices", + "ecs:DescribeTaskSets", + "ecs:ListContainerInstances", + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks", + "ecs:DescribeClusters", + "ecs:UpdateService", + "ecs:PutAttributes", + "ecs:StartTelemetrySession", + "ecs:StartTask", + "ecs:StopTask", + "ecs:ListServices", + "ecs:ListTaskDefinitionFamilies", + "ecs:DescribeTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:ListClusters", + "ecs:RunTask", + "ecs:ListServicesByNamespace" + ] + Resource: "*" + - Sid: ExecuteCommand + Effect: Allow + Action: [ + "ecs:ExecuteCommand" + ] + Resource: "*" + Condition: + StringEquals: + 'aws:ResourceTag/copilot-application': !Sub '${AppName}' + 'aws:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: StartStateMachine + Effect: Allow + Action: + - "states:StartExecution" + - "states:DescribeStateMachine" + Resource: + - !Sub "arn:${AWS::Partition}:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${AppName}-${EnvironmentName}-*" + - Sid: CloudFormation + Effect: Allow + Action: [ + "cloudformation:CancelUpdateStack", + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:DeleteChangeSet", + "cloudformation:DeleteStack", + "cloudformation:Describe*", + "cloudformation:DetectStackDrift", + "cloudformation:DetectStackResourceDrift", + "cloudformation:ExecuteChangeSet", + "cloudformation:GetTemplate", + "cloudformation:GetTemplateSummary", + "cloudformation:UpdateStack", + "cloudformation:UpdateTerminationProtection" + ] + Resource: "*" + - Sid: GetAndPassCopilotRoles + Effect: Allow + Action: [ + "iam:GetRole", + "iam:PassRole" + ] + Resource: "*" + Condition: + StringEquals: + 'iam:ResourceTag/copilot-application': !Sub '${AppName}' + 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: ECR + Effect: Allow + Action: [ + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:ListImages", + "ecr:ListTagsForResource", + "ecr:PutImage", + "ecr:UploadLayerPart", + "ecr:GetAuthorizationToken" + ] + Resource: "*" + - Sid: ResourceGroups + Effect: Allow + Action: [ + "resource-groups:GetGroup", + "resource-groups:GetGroupQuery", + "resource-groups:GetTags", + "resource-groups:ListGroupResources", + "resource-groups:ListGroups", + "resource-groups:SearchResources" + ] + Resource: "*" + - Sid: SSM + Effect: Allow + Action: [ + "ssm:DeleteParameter", + "ssm:DeleteParameters", + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParametersByPath" + ] + Resource: "*" + - Sid: SSMSecret + Effect: Allow + Action: [ + "ssm:PutParameter", + "ssm:AddTagsToResource" + ] + Resource: + - !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/copilot/${AppName}/${EnvironmentName}/secrets/*' + - Sid: ELBv2 + Effect: Allow + Action: [ + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeSSLPolicies", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeTargetGroups", + "elasticloadbalancing:DescribeRules" + ] + Resource: "*" + - Sid: BuiltArtifactAccess + Effect: Allow + Action: [ + "s3:ListBucketByTags", + "s3:GetLifecycleConfiguration", + "s3:GetBucketTagging", + "s3:GetInventoryConfiguration", + "s3:GetObjectVersionTagging", + "s3:ListBucketVersions", + "s3:GetBucketLogging", + "s3:ListBucket", + "s3:GetAccelerateConfiguration", + "s3:GetBucketPolicy", + "s3:GetObjectVersionTorrent", + "s3:GetObjectAcl", + "s3:GetEncryptionConfiguration", + "s3:GetBucketRequestPayment", + "s3:GetObjectVersionAcl", + "s3:GetObjectTagging", + "s3:GetMetricsConfiguration", + "s3:HeadBucket", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketPolicyStatus", + "s3:ListBucketMultipartUploads", + "s3:GetBucketWebsite", + "s3:ListJobs", + "s3:GetBucketVersioning", + "s3:GetBucketAcl", + "s3:GetBucketNotification", + "s3:GetReplicationConfiguration", + "s3:ListMultipartUploadParts", + "s3:GetObject", + "s3:GetObjectTorrent", + "s3:GetAccountPublicAccessBlock", + "s3:ListAllMyBuckets", + "s3:DescribeJob", + "s3:GetBucketCORS", + "s3:GetAnalyticsConfiguration", + "s3:GetObjectVersionForReplication", + "s3:GetBucketLocation", + "s3:GetObjectVersion", + "kms:Decrypt" + ] + Resource: "*" + - Sid: PutObjectsToArtifactBucket + Effect: Allow + Action: + - s3:PutObject + - s3:PutObjectAcl + Resource: + - arn:aws:s3:::mockbucket + - arn:aws:s3:::mockbucket/* + - Sid: EncryptObjectsInArtifactBucket + Effect: Allow + Action: + - kms:GenerateDataKey + Resource: arn:aws:kms:us-west-2:000000000:key/1234abcd-12ab-34cd-56ef-1234567890ab + - Sid: EC2 + Effect: Allow + Action: [ + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeRouteTables" + ] + Resource: "*" + - Sid: AppRunner + Effect: Allow + Action: [ + "apprunner:DescribeService", + "apprunner:ListOperations", + "apprunner:ListServices", + "apprunner:PauseService", + "apprunner:ResumeService", + "apprunner:StartDeployment", + "apprunner:DescribeObservabilityConfiguration", + "apprunner:DescribeVpcIngressConnection" + ] + Resource: "*" + - Sid: Tags + Effect: Allow + Action: [ + "tag:GetResources" + ] + Resource: "*" + - Sid: ApplicationAutoscaling + Effect: Allow + Action: [ + "application-autoscaling:DescribeScalingPolicies" + ] + Resource: "*" + - Sid: DeleteRoles + Effect: Allow + Action: [ + "iam:DeleteRole", + "iam:ListRolePolicies", + "iam:DeleteRolePolicy" + ] + Resource: + - !GetAtt CloudformationExecutionRole.Arn + - !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${AWS::StackName}-EnvManagerRole" + - Sid: DeleteEnvStack + Effect: Allow + Action: + - 'cloudformation:DescribeStacks' + - 'cloudformation:DeleteStack' + Resource: + - !Sub 'arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*' + + VPC: + Metadata: + 'aws:copilot:description': 'A Virtual Private Cloud to control networking of your AWS resources' + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + InstanceTenancy: default + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + PublicRouteTable: + Metadata: + 'aws:copilot:description': "A custom route table that directs network traffic for the public subnets" + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + DefaultPublicRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + InternetGateway: + Metadata: + 'aws:copilot:description': 'An Internet Gateway to connect to the public internet' + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}' + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref InternetGateway + VpcId: !Ref VPC + PublicSubnet1: + Metadata: + 'aws:copilot:description': 'Public subnet 1 for resources that can access the internet' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.0.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub0' + PublicSubnet2: + Metadata: + 'aws:copilot:description': 'Public subnet 2 for resources that can access the internet' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.1.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-pub1' + PrivateSubnet1: + Metadata: + 'aws:copilot:description': 'Private subnet 1 for resources with no internet access' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.2.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv0' + PrivateSubnet2: + Metadata: + 'aws:copilot:description': 'Private subnet 2 for resources with no internet access' + Type: AWS::EC2::Subnet + Properties: + CidrBlock: 10.0.3.0/24 + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-priv1' + PublicSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet1 + PublicSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet2 + + NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' + Type: AWS::EC2::EIP + Condition: CreateNATGateways + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + NatGateway1: + Metadata: + 'aws:copilot:description': 'NAT Gateway 1 enabling workloads placed in private subnet 1 to reach the internet' + Type: AWS::EC2::NatGateway + Condition: CreateNATGateways + Properties: + AllocationId: !GetAtt NatGateway1Attachment.AllocationId + SubnetId: !Ref PublicSubnet1 + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-0' + PrivateRouteTable1: + Type: AWS::EC2::RouteTable + Condition: CreateNATGateways + Properties: + VpcId: !Ref 'VPC' + PrivateRoute1: + Type: AWS::EC2::Route + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable1 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway1 + PrivateRouteTable1Association: + Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable1 + SubnetId: !Ref PrivateSubnet1 + NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' + Type: AWS::EC2::EIP + Condition: CreateNATGateways + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + NatGateway2: + Metadata: + 'aws:copilot:description': 'NAT Gateway 2 enabling workloads placed in private subnet 2 to reach the internet' + Type: AWS::EC2::NatGateway + Condition: CreateNATGateways + Properties: + AllocationId: !GetAtt NatGateway2Attachment.AllocationId + SubnetId: !Ref PublicSubnet2 + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-1' + PrivateRouteTable2: + Type: AWS::EC2::RouteTable + Condition: CreateNATGateways + Properties: + VpcId: !Ref 'VPC' + PrivateRoute2: + Type: AWS::EC2::Route + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable2 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway2 + PrivateRouteTable2Association: + Type: AWS::EC2::SubnetRouteTableAssociation + Condition: CreateNATGateways + Properties: + RouteTableId: !Ref PrivateRouteTable2 + SubnetId: !Ref PrivateSubnet2 + # Creates a service discovery namespace with the form provided in the parameter. + # For new environments after 1.5.0, this is "env.app.local". For upgraded environments from + # before 1.5.0, this is app.local. + ServiceDiscoveryNamespace: + Metadata: + 'aws:copilot:description': 'A private DNS namespace for discovering services within the environment' + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !Ref ServiceDiscoveryEndpoint + Vpc: !Ref VPC + Cluster: + Metadata: + 'aws:copilot:description': 'An ECS cluster to group your services' + Type: AWS::ECS::Cluster + Properties: + CapacityProviders: ['FARGATE', 'FARGATE_SPOT'] + Configuration: + ExecuteCommandConfiguration: + Logging: DEFAULT + ClusterSettings: + - Name: containerInsights + Value: disabled + PublicHTTPLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your load balancer allowing HTTP traffic' + Condition: CreateALB + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: HTTP access to the public facing load balancer + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 80 + FromPort: 80 + IpProtocol: tcp + ToPort: 80 + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb-http' + PublicHTTPSLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your load balancer allowing HTTPS traffic' + Condition: ExportHTTPSListener + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: HTTPS access to the public facing load balancer + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + Description: Allow from anyone on port 443 + FromPort: 443 + IpProtocol: tcp + ToPort: 443 + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-lb-https' + InternalLoadBalancerSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for your internal load balancer allowing HTTP traffic from within the VPC' + Condition: CreateInternalALB + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Access to the internal load balancer + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-internal-lb' + # Only accept requests coming from the public ALB, internal ALB, or other containers in the same security group. + EnvironmentSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to each other' + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EnvironmentSecurityGroup]] + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-env' + EnvironmentHTTPSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateALB + Properties: + Description: HTTP ingress from the public ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicHTTPLoadBalancerSecurityGroup + EnvironmentHTTPSSecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: ExportHTTPSListener + Properties: + Description: HTTPS ingress from the public ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicHTTPSLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromInternalALB: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateInternalALB + Properties: + Description: Ingress from the internal ALB + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref InternalLoadBalancerSecurityGroup + EnvironmentSecurityGroupIngressFromSelf: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from other containers in the same security group + GroupId: !Ref EnvironmentSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + InternalALBIngressFromEnvironmentSecurityGroup: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateInternalALB + Properties: + Description: Ingress from the env security group + GroupId: !Ref InternalLoadBalancerSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + PublicLoadBalancer: + Metadata: + 'aws:copilot:description': 'An Application Load Balancer to distribute public traffic to your services' + Condition: CreateALB + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + SecurityGroups: + - !GetAtt PublicHTTPLoadBalancerSecurityGroup.GroupId + - !If [ExportHTTPSListener, !GetAtt PublicHTTPSLoadBalancerSecurityGroup.GroupId, !Ref "AWS::NoValue"] + Subnets: [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] + Type: application + # Assign a dummy target group that with no real services as targets, so that we can create + # the listeners for the services. + DefaultHTTPTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: CreateALB + Properties: + # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. + HealthCheckIntervalSeconds: 10 # Default is 30. + HealthyThresholdCount: 2 # Default is 5. + HealthCheckTimeoutSeconds: 5 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # Default is 300. + TargetType: ip + VpcId: !Ref VPC + HTTPListener: + Metadata: + 'aws:copilot:description': 'A load balancer listener to route HTTP traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: CreateALB + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 80 + Protocol: HTTP + HTTPSListener: + Metadata: + 'aws:copilot:description': 'A load balancer listener to route HTTPS traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: ExportHTTPSListener + Properties: + Certificates: + - CertificateArn: !Ref HTTPSCert + DefaultActions: + - TargetGroupArn: !Ref DefaultHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 443 + Protocol: HTTPS + InternalLoadBalancer: + Metadata: + 'aws:copilot:description': 'An internal Application Load Balancer to distribute private traffic from within the VPC to your services' + Condition: CreateInternalALB + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internal + SecurityGroups: [ !GetAtt InternalLoadBalancerSecurityGroup.GroupId ] + Subnets: [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] + Type: application + # Assign a dummy target group that with no real services as targets, so that we can create + # the listeners for the services. + DefaultInternalHTTPTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Condition: CreateInternalALB + Properties: + # Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds. + HealthCheckIntervalSeconds: 10 # Default is 30. + HealthyThresholdCount: 2 # Default is 5. + HealthCheckTimeoutSeconds: 5 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 60 # Default is 300. + TargetType: ip + VpcId: !Ref VPC + InternalHTTPListener: + Metadata: + 'aws:copilot:description': 'An internal load balancer listener to route HTTP traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: CreateInternalALB + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref InternalLoadBalancer + Port: 80 + Protocol: HTTP + InternalHTTPSListener: + Metadata: + 'aws:copilot:description': 'An internal load balancer listener to route HTTPS traffic' + Type: AWS::ElasticLoadBalancingV2::Listener + Condition: ExportInternalHTTPSListener + Properties: + DefaultActions: + - TargetGroupArn: !Ref DefaultInternalHTTPTargetGroup + Type: forward + LoadBalancerArn: !Ref InternalLoadBalancer + Port: 443 + Protocol: HTTPS + InternalWorkloadsHostedZone: + Metadata: + 'aws:copilot:description': 'A hosted zone named test.demo.internal for backends behind a private load balancer' + Condition: CreateInternalALB + Type: AWS::Route53::HostedZone + Properties: + Name: !Sub ${EnvironmentName}.${AppName}.internal + VPCs: + - VPCId: !Ref VPC + VPCRegion: !Ref AWS::Region + FileSystem: + Condition: CreateEFS + Type: AWS::EFS::FileSystem + Metadata: + 'aws:copilot:description': 'An EFS filesystem for persistent task storage' + Properties: + BackupPolicy: + Status: ENABLED + Encrypted: true + FileSystemPolicy: + Version: "2012-10-17" + Id: CopilotEFSPolicy + Statement: + - Sid: AllowIAMFromTaggedRoles + Effect: Allow + Principal: + AWS: '*' + Action: + - elasticfilesystem:ClientWrite + - elasticfilesystem:ClientMount + Condition: + Bool: + 'elasticfilesystem:AccessedViaMountTarget': true + StringEquals: + 'iam:ResourceTag/copilot-application': !Sub '${AppName}' + 'iam:ResourceTag/copilot-environment': !Sub '${EnvironmentName}' + - Sid: DenyUnencryptedAccess + Effect: Deny + Principal: '*' + Action: 'elasticfilesystem:*' + Condition: + Bool: + 'aws:SecureTransport': false + LifecyclePolicies: + - TransitionToIA: AFTER_30_DAYS + PerformanceMode: generalPurpose + ThroughputMode: bursting + EFSSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group to allow your containers to talk to EFS storage' + Type: AWS::EC2::SecurityGroup + Condition: CreateEFS + Properties: + GroupDescription: !Join ['', [!Ref AppName, '-', !Ref EnvironmentName, EFSSecurityGroup]] + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub 'copilot-${AppName}-${EnvironmentName}-efs' + EFSSecurityGroupIngressFromEnvironment: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateEFS + Properties: + Description: Ingress from containers in the Environment Security Group. + GroupId: !Ref EFSSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + MountTarget1: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet1 + SecurityGroups: + - !Ref EFSSecurityGroup + MountTarget2: + Type: AWS::EFS::MountTarget + Condition: CreateEFS + Properties: + FileSystemId: !Ref FileSystem + SubnetId: !Ref PrivateSubnet2 + SecurityGroups: + - !Ref EFSSecurityGroup + + CustomResourceRole: + Metadata: + 'aws:copilot:description': 'An IAM role to manage certificates and Route53 hosted zones' + Type: AWS::IAM::Role + Condition: DelegateDNS + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - + Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: "DNSandACMAccess" + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "acm:ListCertificates" + - "acm:RequestCertificate" + - "acm:DescribeCertificate" + - "acm:GetCertificate" + - "acm:DeleteCertificate" + - "acm:AddTagsToCertificate" + - "sts:AssumeRole" + - "logs:*" + - "route53:ChangeResourceRecordSets" + - "route53:Get*" + - "route53:Describe*" + - "route53:ListResourceRecordSets" + - "route53:ListHostedZonesByName" + Resource: + - "*" + EnvironmentHostedZone: + Metadata: + 'aws:copilot:description': "A Route 53 Hosted Zone for the environment's subdomain" + Type: "AWS::Route53::HostedZone" + Condition: DelegateDNS + Properties: + HostedZoneConfig: + Comment: !Sub "HostedZone for environment ${EnvironmentName} - ${EnvironmentName}.${AppName}.${AppDNSName}" + Name: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} + CertificateValidationFunction: + Type: AWS::Lambda::Function + Condition: DelegateDNS + Properties: + Handler: "index.certificateRequestHandler" + Timeout: 900 + MemorySize: 512 + Role: !GetAtt 'CustomResourceRole.Arn' + Runtime: nodejs16.x + + CustomDomainFunction: + Condition: ManagedAliases + Type: AWS::Lambda::Function + Properties: + Handler: "index.handler" + Timeout: 600 + MemorySize: 512 + Role: !GetAtt 'CustomResourceRole.Arn' + Runtime: nodejs16.x + + DNSDelegationFunction: + Type: AWS::Lambda::Function + Condition: DelegateDNS + Properties: + Handler: "index.domainDelegationHandler" + Timeout: 600 + MemorySize: 512 + Role: !GetAtt 'CustomResourceRole.Arn' + Runtime: nodejs16.x + DelegateDNSAction: + Metadata: + 'aws:copilot:description': 'Delegate DNS for environment subdomain' + Condition: DelegateDNS + Type: Custom::DNSDelegationFunction + DependsOn: + - DNSDelegationFunction + - EnvironmentHostedZone + Properties: + ServiceToken: !GetAtt DNSDelegationFunction.Arn + DomainName: !Sub ${AppName}.${AppDNSName} + SubdomainName: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} + NameServers: !GetAtt EnvironmentHostedZone.NameServers + RootDNSRole: !Ref AppDNSDelegationRole + EnvHostedZoneId: !Ref EnvironmentHostedZone + + HTTPSCert: + Metadata: + 'aws:copilot:description': 'Request and validate an ACM certificate for your domain' + Condition: DelegateDNS + Type: Custom::CertificateValidationFunction + DependsOn: + - CertificateValidationFunction + - EnvironmentHostedZone + - DelegateDNSAction + Properties: + ServiceToken: !GetAtt CertificateValidationFunction.Arn + AppName: !Ref AppName + EnvName: !Ref EnvironmentName + DomainName: !Ref AppDNSName + Aliases: !Ref Aliases + EnvHostedZoneId: !Ref EnvironmentHostedZone + Region: !Ref AWS::Region + RootDNSRole: !Ref AppDNSDelegationRole + + CustomDomainAction: + Metadata: + 'aws:copilot:description': 'Add an A-record to the hosted zone for the domain alias' + Condition: ManagedAliases + Type: Custom::CustomDomainFunction + Properties: + ServiceToken: !GetAtt CustomDomainFunction.Arn + AppName: !Ref AppName + EnvName: !Ref EnvironmentName + Aliases: !Ref Aliases + AppDNSRole: !Ref AppDNSDelegationRole + DomainName: !Ref AppDNSName + EnvHostedZoneId: !Ref EnvironmentHostedZone + PublicAccessDNS: !GetAtt PublicLoadBalancer.DNSName + PublicAccessHostedZone: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID + AppRunnerVpcEndpointSecurityGroup: + Metadata: + 'aws:copilot:description': 'A security group for App Runner private services' + Type: AWS::EC2::SecurityGroup + Condition: CreateAppRunnerVPCEndpoint + Properties: + GroupDescription: demo-test-AppRunnerVpcEndpointSecurityGroup + VpcId: !Ref VPC + Tags: + - Key: Name + Value: copilot-demo-test-app-runner-vpc-endpoint + + AppRunnerVpcEndpointSecurityGroupIngressFromEnvironment: + Type: AWS::EC2::SecurityGroupIngress + Condition: CreateAppRunnerVPCEndpoint + Properties: + Description: Ingress from services in the environment + GroupId: !Ref AppRunnerVpcEndpointSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref EnvironmentSecurityGroup + + AppRunnerVpcEndpoint: + Metadata: + 'aws:copilot:description': 'VPC Endpoint to connect environment to App Runner for private services' + Type: AWS::EC2::VPCEndpoint + Condition: CreateAppRunnerVPCEndpoint + Properties: + VpcEndpointType: Interface + VpcId: !Ref VPC + SecurityGroupIds: + - !Ref AppRunnerVpcEndpointSecurityGroup + ServiceName: !Sub 'com.amazonaws.${AWS::Region}.apprunner.requests' + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + LogResourcePolicy: + Metadata: + 'aws:copilot:description': 'A resource policy to allow AWS services to create log streams for your workloads.' + Type: AWS::Logs::ResourcePolicy + Properties: + PolicyName: !Sub '${AppName}-${EnvironmentName}-LogResourcePolicy' + PolicyDocument: + Fn::Sub: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "StateMachineToCloudWatchLogs", + "Effect": "Allow", + "Principal": { + "Service": ["delivery.logs.amazonaws.com"] + }, + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/copilot/${AppName}-${EnvironmentName}-*:log-stream:*" + ], + "Condition": { + "StringEquals": { + "aws:SourceAccount": "${AWS::AccountId}" + }, + "ArnLike": { + "aws:SourceArn": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*" + } + } + } + ] + } +Outputs: + VpcId: + Value: !Ref VPC + Export: + Name: !Sub ${AWS::StackName}-VpcId + PublicSubnets: + Value: !Join [ ',', [ !Ref PublicSubnet1, !Ref PublicSubnet2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PublicSubnets + PrivateSubnets: + Value: !Join [ ',', [ !Ref PrivateSubnet1, !Ref PrivateSubnet2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PrivateSubnets + InternetGatewayID: + Value: !Ref InternetGateway + Export: + Name: !Sub ${AWS::StackName}-InternetGatewayID + PublicRouteTableID: + Value: !Ref PublicRouteTable + Export: + Name: !Sub ${AWS::StackName}-PublicRouteTableID + PrivateRouteTableIDs: + Condition: CreateNATGateways + Value: !Join [ ',', [ !Ref PrivateRouteTable1, !Ref PrivateRouteTable2, ] ] + Export: + Name: !Sub ${AWS::StackName}-PrivateRouteTableIDs + ServiceDiscoveryNamespaceID: + Value: !GetAtt ServiceDiscoveryNamespace.Id + Export: + Name: !Sub ${AWS::StackName}-ServiceDiscoveryNamespaceID + EnvironmentSecurityGroup: + Value: !Ref EnvironmentSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-EnvironmentSecurityGroup + PublicLoadBalancerDNSName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.DNSName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerDNS + PublicLoadBalancerFullName: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.LoadBalancerFullName + Export: + Name: !Sub ${AWS::StackName}-PublicLoadBalancerFullName + PublicLoadBalancerHostedZone: + Condition: CreateALB + Value: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-CanonicalHostedZoneID + HTTPListenerArn: + Condition: CreateALB + Value: !Ref HTTPListener + Export: + Name: !Sub ${AWS::StackName}-HTTPListenerArn + HTTPSListenerArn: + Condition: ExportHTTPSListener + Value: !Ref HTTPSListener + Export: + Name: !Sub ${AWS::StackName}-HTTPSListenerArn + DefaultHTTPTargetGroupArn: + Condition: CreateALB + Value: !Ref DefaultHTTPTargetGroup + Export: + Name: !Sub ${AWS::StackName}-DefaultHTTPTargetGroup + InternalLoadBalancerDNSName: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.DNSName + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerDNS + InternalLoadBalancerFullName: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.LoadBalancerFullName + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerFullName + InternalLoadBalancerHostedZone: + Condition: CreateInternalALB + Value: !GetAtt InternalLoadBalancer.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerCanonicalHostedZoneID + InternalWorkloadsHostedZone: + Condition: CreateInternalALB + Value: !Ref InternalWorkloadsHostedZone + Export: + Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneID + InternalWorkloadsHostedZoneName: + Condition: CreateInternalALB + Value: !Sub ${EnvironmentName}.${AppName}.internal + Export: + Name: !Sub ${AWS::StackName}-InternalWorkloadsHostedZoneName + InternalHTTPListenerArn: + Condition: CreateInternalALB + Value: !Ref InternalHTTPListener + Export: + Name: !Sub ${AWS::StackName}-InternalHTTPListenerArn + InternalHTTPSListenerArn: + Condition: ExportInternalHTTPSListener + Value: !Ref InternalHTTPSListener + Export: + Name: !Sub ${AWS::StackName}-InternalHTTPSListenerArn + InternalLoadBalancerSecurityGroup: + Condition: CreateInternalALB + Value: !Ref InternalLoadBalancerSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-InternalLoadBalancerSecurityGroup + ClusterId: + Value: !Ref Cluster + Export: + Name: !Sub ${AWS::StackName}-ClusterId + EnvironmentManagerRoleARN: + Value: !GetAtt EnvironmentManagerRole.Arn + Description: The role to be assumed by the ecs-cli to manage environments. + Export: + Name: !Sub ${AWS::StackName}-EnvironmentManagerRoleARN + CFNExecutionRoleARN: + Value: !GetAtt CloudformationExecutionRole.Arn + Description: The role to be assumed by the Cloudformation service when it deploys application infrastructure. + Export: + Name: !Sub ${AWS::StackName}-CFNExecutionRoleARN + EnvironmentHostedZone: + Condition: DelegateDNS + Value: !Ref EnvironmentHostedZone + Description: The HostedZone for this environment's private DNS. + Export: + Name: !Sub ${AWS::StackName}-HostedZone + EnvironmentSubdomain: + Condition: DelegateDNS + Value: !Sub ${EnvironmentName}.${AppName}.${AppDNSName} + Description: The domain name of this environment. + Export: + Name: !Sub ${AWS::StackName}-SubDomain + EnabledFeatures: + Value: !Sub '${ALBWorkloads},${InternalALBWorkloads},${EFSWorkloads},${NATWorkloads},${Aliases},${AppRunnerPrivateWorkloads}' + Description: Required output to force the stack to update if mutating feature params, like ALBWorkloads, does not change the template. + ManagedFileSystemID: + Condition: CreateEFS + Value: !Ref FileSystem + Description: The ID of the Copilot-managed EFS filesystem. + Export: + Name: !Sub ${AWS::StackName}-FilesystemID + PublicALBAccessible: + Condition: CreateALB + Value: true + LastForceDeployID: + Value: "" + Description: Optionally force the template to update when no immediate resource change is present. + AppRunnerVpcEndpointId: + Condition: CreateAppRunnerVPCEndpoint + Value: !Ref AppRunnerVpcEndpoint + Description: VPC Endpoint to App Runner for private services + Export: + Name: !Sub ${AWS::StackName}-AppRunnerVpcEndpointId diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml index 8ece1b5c6dc..b6f0e1604d1 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml @@ -112,7 +112,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-cloudfront-observability.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-cloudfront-observability.yml index b8bc6894137..d369ffd2ff5 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-cloudfront-observability.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-cloudfront-observability.yml @@ -761,7 +761,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index fb6d9d44755..cf2e3a9b00c 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -635,7 +635,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml index 834e67877b8..5c96c060077 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml @@ -169,7 +169,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-defaultvpc-flowlogs.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-defaultvpc-flowlogs.yml index 1553e6a22af..e77dafc14d2 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-defaultvpc-flowlogs.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-defaultvpc-flowlogs.yml @@ -117,7 +117,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-sslpolicy-custom-empty-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-sslpolicy-custom-empty-security-group.yml index f1e81e2e297..de7fd044035 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-sslpolicy-custom-empty-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-sslpolicy-custom-empty-security-group.yml @@ -612,7 +612,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-importedvpc-flowlogs.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-importedvpc-flowlogs.yml index a6c03bca27b..6e7f4b9c107 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-importedvpc-flowlogs.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-importedvpc-flowlogs.yml @@ -124,7 +124,8 @@ Resources: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole Path: / Policies: - PolicyName: root diff --git a/internal/pkg/manifest/env.go b/internal/pkg/manifest/env.go index a051993d645..5111490065f 100644 --- a/internal/pkg/manifest/env.go +++ b/internal/pkg/manifest/env.go @@ -70,13 +70,21 @@ func FromEnvConfig(cfg *config.Environment, parser template.Parser) *Environment Network: environmentNetworkConfig{ VPC: vpc, }, - HTTPConfig: http, - Observability: obs, + HTTPConfig: http, + Observability: obs, + AdditionalAssumeRolePermissions: additionalAssumeRolePermissions(cfg.CustomConfig), }, parser: parser, } } +func additionalAssumeRolePermissions(cfg *config.CustomizeEnv) (permissions []string) { + if cfg == nil { + return permissions + } + return cfg.AdditionalAssumeRolePermissions +} + // MarshalBinary serializes the manifest object into a binary YAML document. // Implements the encoding.BinaryMarshaler interface. func (e *Environment) MarshalBinary() ([]byte, error) { @@ -91,10 +99,11 @@ func (e *Environment) MarshalBinary() ([]byte, error) { // EnvironmentConfig defines the configuration settings for an environment manifest type EnvironmentConfig struct { - Network environmentNetworkConfig `yaml:"network,omitempty,flow"` - Observability environmentObservability `yaml:"observability,omitempty,flow"` - HTTPConfig EnvironmentHTTPConfig `yaml:"http,omitempty,flow"` - CDNConfig EnvironmentCDNConfig `yaml:"cdn,omitempty,flow"` + Network environmentNetworkConfig `yaml:"network,omitempty,flow"` + Observability environmentObservability `yaml:"observability,omitempty,flow"` + HTTPConfig EnvironmentHTTPConfig `yaml:"http,omitempty,flow"` + CDNConfig EnvironmentCDNConfig `yaml:"cdn,omitempty,flow"` + AdditionalAssumeRolePermissions []string `yaml:"additionalAssumeRolePermissions,omitempty"` } // IsPublicLBIngressRestrictedToCDN returns whether an environment has its diff --git a/internal/pkg/manifest/env_test.go b/internal/pkg/manifest/env_test.go index 71d8c6c7e53..8a00a0d64ab 100644 --- a/internal/pkg/manifest/env_test.go +++ b/internal/pkg/manifest/env_test.go @@ -304,6 +304,25 @@ func TestFromEnvConfig(t *testing.T) { }, }, }, + "converts additional assume role permissions": { + in: &config.Environment{ + App: "phonetool", + Name: "test", + CustomConfig: &config.CustomizeEnv{ + AdditionalAssumeRolePermissions: []string{"sts:SetSourceIdentity"}, + }, + }, + + wanted: &Environment{ + Workload: Workload{ + Name: stringP("test"), + Type: stringP("Environment"), + }, + EnvironmentConfig: EnvironmentConfig{ + AdditionalAssumeRolePermissions: []string{"sts:SetSourceIdentity"}, + }, + }, + }, } for name, tc := range testCases { @@ -640,6 +659,23 @@ http: }, }, }, + "unmarshal with additional assume role permissions": { + inContent: `name: prod +type: Environment +additionalAssumeRolePermissions: + - sts:SetSourceIdentity + - sts:TagSession +`, + wantedStruct: &Environment{ + Workload: Workload{ + Name: aws.String("prod"), + Type: aws.String("Environment"), + }, + EnvironmentConfig: EnvironmentConfig{ + AdditionalAssumeRolePermissions: []string{"sts:SetSourceIdentity", "sts:TagSession"}, + }, + }, + }, "fail to unmarshal": { inContent: `watermelon in easter hay`, wantedErrPrefix: "unmarshal environment manifest: ", diff --git a/internal/pkg/manifest/marshal_manifest_integration_test.go b/internal/pkg/manifest/marshal_manifest_integration_test.go index 5202fe9248d..de7cdb85a5d 100644 --- a/internal/pkg/manifest/marshal_manifest_integration_test.go +++ b/internal/pkg/manifest/marshal_manifest_integration_test.go @@ -379,6 +379,15 @@ func TestEnvironment_InitialManifestIntegration(t *testing.T) { }, wantedTestData: "environment-import-vpc.yml", }, + "with additional assume role permissions": { + inProps: EnvironmentProps{ + Name: "test", + CustomConfig: &config.CustomizeEnv{ + AdditionalAssumeRolePermissions: []string{"sts:SetSourceIdentity"}, + }, + }, + wantedTestData: "environment-assume-role-permissions.yml", + }, "basic manifest": { inProps: EnvironmentProps{ Name: "test", diff --git a/internal/pkg/manifest/testdata/environment-adjust-vpc-private-subnets.yml b/internal/pkg/manifest/testdata/environment-adjust-vpc-private-subnets.yml index 7bf7d0aec0d..7a44305a85a 100644 --- a/internal/pkg/manifest/testdata/environment-adjust-vpc-private-subnets.yml +++ b/internal/pkg/manifest/testdata/environment-adjust-vpc-private-subnets.yml @@ -24,3 +24,8 @@ http: # Configure observability for your environment resources. observability: container_insights: false + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +# additionalAssumeRolePermissions: +# - sts:SetSourceIdentity +# - sts:TagSession \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/environment-adjust-vpc.yml b/internal/pkg/manifest/testdata/environment-adjust-vpc.yml index de9e1f90070..99354cf4a7d 100644 --- a/internal/pkg/manifest/testdata/environment-adjust-vpc.yml +++ b/internal/pkg/manifest/testdata/environment-adjust-vpc.yml @@ -30,3 +30,8 @@ http: # Configure observability for your environment resources. observability: container_insights: false + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +# additionalAssumeRolePermissions: +# - sts:SetSourceIdentity +# - sts:TagSession \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml b/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml new file mode 100644 index 00000000000..2270e00aad4 --- /dev/null +++ b/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml @@ -0,0 +1,25 @@ +# The manifest for the "test" environment. +# Read the full specification for the "Environment" type at: +# https://aws.github.io/copilot-cli/docs/manifest/environment/ + +# Your environment name will be used in naming your resources like VPC, cluster, etc. +name: test +type: Environment + +# Import your own VPC and subnets or configure how they should be created. +# network: +# vpc: +# id: + +# Configure the load balancers in your environment, once created. +# http: +# public: +# private: + +# Configure observability for your environment resources. +# observability: +# container_insights: true + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +additionalAssumeRolePermissions: + - sts:SetSourceIdentity \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/environment-default.yml b/internal/pkg/manifest/testdata/environment-default.yml index b5c5c68e59e..9a113ee9b96 100644 --- a/internal/pkg/manifest/testdata/environment-default.yml +++ b/internal/pkg/manifest/testdata/environment-default.yml @@ -19,3 +19,8 @@ type: Environment # Configure observability for your environment resources. # observability: # container_insights: true + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +# additionalAssumeRolePermissions: +# - sts:SetSourceIdentity +# - sts:TagSession \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/environment-import-vpc.yml b/internal/pkg/manifest/testdata/environment-import-vpc.yml index 694ddcd997a..bca8985d18a 100644 --- a/internal/pkg/manifest/testdata/environment-import-vpc.yml +++ b/internal/pkg/manifest/testdata/environment-import-vpc.yml @@ -26,3 +26,8 @@ http: # Configure observability for your environment resources. observability: container_insights: true + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +# additionalAssumeRolePermissions: +# - sts:SetSourceIdentity +# - sts:TagSession \ No newline at end of file diff --git a/internal/pkg/template/env.go b/internal/pkg/template/env.go index 059f95b9ec2..d4e98f206ae 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -122,11 +122,12 @@ type EnvOpts struct { EnableLongARNFormatLambda string CustomDomainLambda string - Addons *Addons - ScriptBucketName string - PermissionsBoundary string - ArtifactBucketARN string - ArtifactBucketKeyARN string + Addons *Addons + ScriptBucketName string + PermissionsBoundary string + AdditionalAssumeRolePermissions []string + ArtifactBucketARN string + ArtifactBucketKeyARN string VPCConfig VPCConfig PublicHTTPConfig PublicHTTPConfig diff --git a/internal/pkg/template/templates/environment/manifest.yml b/internal/pkg/template/templates/environment/manifest.yml index c5152eed1a9..b433242bb18 100644 --- a/internal/pkg/template/templates/environment/manifest.yml +++ b/internal/pkg/template/templates/environment/manifest.yml @@ -79,3 +79,15 @@ http: observability: container_insights: {{.Observability.ContainerInsights}} {{- end}} + +# Configure additional permissions for the trust policy of the environment manager role to be able to assume it +{{- if not .AdditionalAssumeRolePermissions}} +# additionalAssumeRolePermissions: +# - sts:SetSourceIdentity +# - sts:TagSession +{{- else}} +additionalAssumeRolePermissions: + {{- range $perm := .AdditionalAssumeRolePermissions}} + - {{$perm}} + {{- end}} +{{- end}} \ No newline at end of file diff --git a/internal/pkg/template/templates/environment/partials/environment-manager-role.yml b/internal/pkg/template/templates/environment/partials/environment-manager-role.yml index 006f4c66e53..fc9c6faaa89 100644 --- a/internal/pkg/template/templates/environment/partials/environment-manager-role.yml +++ b/internal/pkg/template/templates/environment/partials/environment-manager-role.yml @@ -12,7 +12,11 @@ EnvironmentManagerRole: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole +{{- range $perm := .AdditionalAssumeRolePermissions}} + - "{{$perm}}" +{{- end}} {{- if .PermissionsBoundary}} PermissionsBoundary: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{.PermissionsBoundary}}' {{- end}}