From 78293970b38008871d700eb3f7a4f1b3d7d5afbc Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Mon, 9 Oct 2023 09:56:41 +0200 Subject: [PATCH 1/7] Allow assume role chaining with federated users (source identity and session tags) Depending on the original credentials used to invoke the copilot cli command, the EnvManagerRole can only be assumed when it allows the original source identity and transitive tags to be passed to the session. These permissions should not be of harm when the user's session does not have a source identity or transitive tags. --- .../environment/partials/environment-manager-role.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ff39256ead2..67d61eae581 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,10 @@ EnvironmentManagerRole: - Effect: Allow Principal: AWS: !Sub ${ToolsAccountPrincipalARN} - Action: sts:AssumeRole + Action: + - sts:AssumeRole + - sts:SetSourceIdentity + - stsTagSession {{- if .PermissionsBoundary}} PermissionsBoundary: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{.PermissionsBoundary}}' {{- end}} From 3075aa775449e4a25617da7f9b065e1009427229 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Wed, 25 Oct 2023 13:46:08 +0200 Subject: [PATCH 2/7] Make additional assume role permissions in envmanager role configurable --- internal/pkg/cli/deploy/env.go | 25 ++++---- internal/pkg/cli/env_init.go | 30 +++++++--- internal/pkg/cli/env_init_test.go | 3 + internal/pkg/cli/flag.go | 2 + internal/pkg/cli/init.go | 1 + internal/pkg/config/env.go | 13 +++-- .../pkg/deploy/cloudformation/stack/env.go | 57 ++++++++++--------- .../deploy/cloudformation/stack/env_test.go | 26 +++++++-- internal/pkg/manifest/env.go | 21 +++++-- internal/pkg/manifest/env_test.go | 36 ++++++++++++ internal/pkg/template/env.go | 11 ++-- .../templates/environment/manifest.yml | 12 ++++ .../partials/environment-manager-role.yml | 5 +- 13 files changed, 170 insertions(+), 72 deletions(-) diff --git a/internal/pkg/cli/deploy/env.go b/internal/pkg/cli/deploy/env.go index bee9c1e96b4..6c315dc4f6c 100644 --- a/internal/pkg/cli/deploy/env.go +++ b/internal/pkg/cli/deploy/env.go @@ -420,18 +420,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.Manifest.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 7e65fbc93de..b929556eda3 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -131,6 +131,7 @@ const ( enableContainerInsightsFlag = "container-insights" defaultConfigFlag = "default-config" + allowFederatedSessionFlag = "federated-session" accessKeyIDFlag = "aws-access-key-id" secretAccessKeyFlag = "aws-secret-access-key" @@ -404,6 +405,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_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/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/template/env.go b/internal/pkg/template/env.go index 4504129f5e3..94a4948d349 100644 --- a/internal/pkg/template/env.go +++ b/internal/pkg/template/env.go @@ -115,11 +115,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 67d61eae581..9ecb28a9e6a 100644 --- a/internal/pkg/template/templates/environment/partials/environment-manager-role.yml +++ b/internal/pkg/template/templates/environment/partials/environment-manager-role.yml @@ -14,8 +14,9 @@ EnvironmentManagerRole: AWS: !Sub ${ToolsAccountPrincipalARN} Action: - sts:AssumeRole - - sts:SetSourceIdentity - - stsTagSession +{{- range $perm := .AdditionalAssumeRolePermissions}} + - "{{$perm}}" +{{- end}} {{- if .PermissionsBoundary}} PermissionsBoundary: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/{{.PermissionsBoundary}}' {{- end}} From 01ec03bdbc2f335905369e6b9e33d10ab9dee0e6 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Thu, 26 Oct 2023 11:00:41 +0200 Subject: [PATCH 3/7] missing integration tests --- internal/pkg/cli/deploy/env.go | 9 +- .../stack/env_integration_test.go | 27 + ...ith-additional-assume-role-permissions.yml | 1220 +++++++++++++++++ .../template-with-basic-manifest.yml | 3 +- ...template-with-cloudfront-observability.yml | 3 +- .../template-with-custom-security-group.yml | 3 +- ...emplate-with-default-access-log-config.yml | 3 +- .../template-with-defaultvpc-flowlogs.yml | 3 +- ...-sslpolicy-custom-empty-security-group.yml | 3 +- .../template-with-importedvpc-flowlogs.yml | 3 +- 10 files changed, 1269 insertions(+), 8 deletions(-) create mode 100644 internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-additional-assume-role-permissions.yml diff --git a/internal/pkg/cli/deploy/env.go b/internal/pkg/cli/deploy/env.go index 6c315dc4f6c..dbfccc93570 100644 --- a/internal/pkg/cli/deploy/env.go +++ b/internal/pkg/cli/deploy/env.go @@ -270,6 +270,13 @@ type DeployEnvironmentInput struct { Detach bool } +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) @@ -428,7 +435,7 @@ func (d *envDeployer) buildStackInput(in *DeployEnvironmentInput) (*cfnstack.Env CIDRPrefixListIDs: cidrPrefixListIDs, PublicALBSourceIPs: d.publicALBSourceIPs(in), Mft: in.Manifest, - AdditionalAssumeRolePermissions: in.Manifest.AdditionalAssumeRolePermissions, + AdditionalAssumeRolePermissions: in.AdditionalAssumeRolePermissions(), ForceUpdate: in.ForceNewUpdate, RawMft: in.RawManifest, PermissionsBoundary: in.PermissionsBoundary, 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/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..fe6d8d8091a --- /dev/null +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-additional-assume-role-permissions.yml @@ -0,0 +1,1220 @@ +# 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" + ] + 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 8eacf13f998..6ae86ddcc04 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 4b5d2e48da7..3a06b9dc5ee 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 e68829e78de..69b4961a7ca 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 f2e1234c0f1..fca7e96bbec 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 23a562dbba3..3492cd3129b 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 18d22f46915..1421e45a922 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 01cbe915544..2147bd3d73d 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 From 8a77a4c731a1c3aabfb7dd0b96919789209af786 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Thu, 26 Oct 2023 11:12:09 +0200 Subject: [PATCH 4/7] fix codestyle :) --- internal/pkg/cli/deploy/env.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/pkg/cli/deploy/env.go b/internal/pkg/cli/deploy/env.go index dbfccc93570..7d3cda08a2f 100644 --- a/internal/pkg/cli/deploy/env.go +++ b/internal/pkg/cli/deploy/env.go @@ -270,6 +270,8 @@ 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{} From 179975b313c7c139b8b3114484259b653d6b1133 Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Wed, 1 Nov 2023 15:16:13 +0100 Subject: [PATCH 5/7] adjust another integration test for new default env manifest yml --- .../marshal_manifest_integration_test.go | 9 +++++++ ...environment-adjust-vpc-private-subnets.yml | 5 ++++ .../testdata/environment-adjust-vpc.yml | 5 ++++ .../environment-assume-role-permissions.yml | 25 +++++++++++++++++++ .../manifest/testdata/environment-default.yml | 5 ++++ .../testdata/environment-import-vpc.yml | 5 ++++ 6 files changed, 54 insertions(+) create mode 100644 internal/pkg/manifest/testdata/environment-assume-role-permissions.yml 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..78c81473555 --- /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:SetSourceIdentity1 \ 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 From af271ee2ba57220195749fe2de8d1040f5282bed Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Wed, 1 Nov 2023 15:42:04 +0100 Subject: [PATCH 6/7] fix typo --- .../manifest/testdata/environment-assume-role-permissions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml b/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml index 78c81473555..2270e00aad4 100644 --- a/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml +++ b/internal/pkg/manifest/testdata/environment-assume-role-permissions.yml @@ -22,4 +22,4 @@ type: Environment # Configure additional permissions for the trust policy of the environment manager role to be able to assume it additionalAssumeRolePermissions: - - sts:SetSourceIdentity1 \ No newline at end of file + - sts:SetSourceIdentity \ No newline at end of file From bfec7513502be2f6a18633739bc1041957ac4a3f Mon Sep 17 00:00:00 2001 From: Florian Schmidt Date: Wed, 1 Nov 2023 16:03:23 +0100 Subject: [PATCH 7/7] add new permission from merging remote mainline --- .../template-with-additional-assume-role-permissions.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index fe6d8d8091a..2204223c0ee 100644 --- 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 @@ -168,7 +168,8 @@ Resources: "ecs:DescribeTaskDefinition", "ecs:ListTaskDefinitions", "ecs:ListClusters", - "ecs:RunTask" + "ecs:RunTask", + "ecs:ListServicesByNamespace" ] Resource: "*" - Sid: ExecuteCommand