From 793230c7a6dcaf93408206e680bd26159ece1b7d Mon Sep 17 00:00:00 2001 From: Devyn Goetsch Date: Tue, 30 Mar 2021 04:31:02 -0500 Subject: [PATCH] feat(ec2): allow disabling inline security group rules (#13613) Adds a resource property and a context key for disabling security group inline rules --- packages/@aws-cdk/aws-ec2/README.md | 21 + .../@aws-cdk/aws-ec2/lib/security-group.ts | 66 ++- .../aws-ec2/test/security-group.test.ts | 452 +++++++++++++++++- 3 files changed, 528 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 5ce9a5c500d4d..123ef79f3aa66 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -520,6 +520,27 @@ listener.connections.allowDefaultPortFromAnyIpv4('Allow public'); appFleet.connections.allowDefaultPortTo(rdsDatabase, 'Fleet can access database'); ``` +### Security group rules + +By default, security group wills be added inline to the security group in the output cloud formation +template, if applicable. This includes any static rules by ip address and port range. This +optimization helps to minimize the size of the template. + +In some environments this is not desirable, for example if your security group access is controlled +via tags. You can disable inline rules per security group or globally via the context key +`@aws-cdk/aws-ec2.securityGroupDisableInlineRules`. + +```ts fixture=with-vpc +const mySecurityGroupWithoutInlineRules = new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc, + description: 'Allow ssh access to ec2 instances', + allowAllOutbound: true, + disableInlineRules: true +}); +//This will add the rule as an external cloud formation construct +mySecurityGroupWithoutInlineRules.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'allow ssh access from the world'); +``` + ## Machine Images (AMIs) AMIs control the OS that gets launched when you start your EC2 instance. The EC2 diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index b5c3e4bf5cff2..70bc1311bf8ab 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -4,12 +4,14 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { Connections } from './connections'; import { CfnSecurityGroup, CfnSecurityGroupEgress, CfnSecurityGroupIngress } from './ec2.generated'; -import { IPeer } from './peer'; +import { IPeer, Peer } from './peer'; import { Port } from './port'; import { IVpc } from './vpc'; const SECURITY_GROUP_SYMBOL = Symbol.for('@aws-cdk/iam.SecurityGroup'); +const SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY = '@aws-cdk/aws-ec2.securityGroupDisableInlineRules'; + /** * Interface for security group-like objects */ @@ -229,6 +231,22 @@ export interface SecurityGroupProps { * @default true */ readonly allowAllOutbound?: boolean; + + /** + * Whether to disable inline ingress and egress rule optimization. + * + * If this is set to true, ingress and egress rules will not be declared under the + * SecurityGroup in cloudformation, but will be separate elements. + * + * Inlining rules is an optimization for producing smaller stack templates. Sometimes + * this is not desirable, for example when security group access is managed via tags. + * + * The default value can be overriden globally by setting the context variable + * '@aws-cdk/aws-ec2.securityGroupDisableInlineRules'. + * + * @default false + */ + readonly disableInlineRules?: boolean; } /** @@ -390,6 +408,11 @@ export class SecurityGroup extends SecurityGroupBase { private readonly directIngressRules: CfnSecurityGroup.IngressProperty[] = []; private readonly directEgressRules: CfnSecurityGroup.EgressProperty[] = []; + /** + * Whether to disable optimization for inline security group rules. + */ + private readonly disableInlineRules: boolean; + constructor(scope: Construct, id: string, props: SecurityGroupProps) { super(scope, id, { physicalName: props.securityGroupName, @@ -399,6 +422,10 @@ export class SecurityGroup extends SecurityGroupBase { this.allowAllOutbound = props.allowAllOutbound !== false; + this.disableInlineRules = props.disableInlineRules !== undefined ? + !!props.disableInlineRules : + !!this.node.tryGetContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY); + this.securityGroup = new CfnSecurityGroup(this, 'Resource', { groupName: this.physicalName, groupDescription, @@ -415,7 +442,7 @@ export class SecurityGroup extends SecurityGroupBase { } public addIngressRule(peer: IPeer, connection: Port, description?: string, remoteRule?: boolean) { - if (!peer.canInlineRule || !connection.canInlineRule) { + if (!peer.canInlineRule || !connection.canInlineRule || this.disableInlineRules) { super.addIngressRule(peer, connection, description, remoteRule); return; } @@ -445,7 +472,7 @@ export class SecurityGroup extends SecurityGroupBase { this.removeNoTrafficRule(); } - if (!peer.canInlineRule || !connection.canInlineRule) { + if (!peer.canInlineRule || !connection.canInlineRule || this.disableInlineRules) { super.addEgressRule(peer, connection, description, remoteRule); return; } @@ -519,10 +546,14 @@ export class SecurityGroup extends SecurityGroupBase { * strictly necessary). */ private addDefaultEgressRule() { - if (this.allowAllOutbound) { - this.directEgressRules.push(ALLOW_ALL_RULE); + if (this.disableInlineRules) { + const peer = this.allowAllOutbound ? ALL_TRAFFIC_PEER : NO_TRAFFIC_PEER; + const port = this.allowAllOutbound ? ALL_TRAFFIC_PORT : NO_TRAFFIC_PORT; + const description = this.allowAllOutbound ? ALLOW_ALL_RULE.description : MATCH_NO_TRAFFIC.description; + super.addEgressRule(peer, port, description, false); } else { - this.directEgressRules.push(MATCH_NO_TRAFFIC); + const rule = this.allowAllOutbound? ALLOW_ALL_RULE : MATCH_NO_TRAFFIC; + this.directEgressRules.push(rule); } } @@ -530,9 +561,20 @@ export class SecurityGroup extends SecurityGroupBase { * Remove the bogus rule if it exists */ private removeNoTrafficRule() { - const i = this.directEgressRules.findIndex(r => egressRulesEqual(r, MATCH_NO_TRAFFIC)); - if (i > -1) { - this.directEgressRules.splice(i, 1); + if (this.disableInlineRules) { + const [scope, id] = determineRuleScope( + this, + NO_TRAFFIC_PEER, + NO_TRAFFIC_PORT, + 'to', + false); + + scope.node.tryRemoveChild(id); + } else { + const i = this.directEgressRules.findIndex(r => egressRulesEqual(r, MATCH_NO_TRAFFIC)); + if (i > -1) { + this.directEgressRules.splice(i, 1); + } } } } @@ -554,6 +596,9 @@ const MATCH_NO_TRAFFIC = { toPort: 86, }; +const NO_TRAFFIC_PEER = Peer.ipv4(MATCH_NO_TRAFFIC.cidrIp); +const NO_TRAFFIC_PORT = Port.icmpTypeAndCode(MATCH_NO_TRAFFIC.fromPort, MATCH_NO_TRAFFIC.toPort); + /** * Egress rule that matches all traffic */ @@ -563,6 +608,9 @@ const ALLOW_ALL_RULE = { ipProtocol: '-1', }; +const ALL_TRAFFIC_PEER = Peer.anyIpv4(); +const ALL_TRAFFIC_PORT = Port.allTraffic(); + export interface ConnectionRule { /** * The IP protocol name (tcp, udp, icmp) or number (see Protocol Numbers). diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index 9851d87235c41..59f7c4192ff99 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -1,7 +1,9 @@ -import { expect, haveResource, not } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert'; import { App, Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { Peer, Port, SecurityGroup, Vpc } from '../lib'; +import { Peer, Port, SecurityGroup, SecurityGroupProps, Vpc } from '../lib'; + +const SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY = '@aws-cdk/aws-ec2.securityGroupDisableInlineRules'; nodeunitShim({ 'security group can allows all outbound traffic by default'(test: Test) { @@ -148,6 +150,23 @@ nodeunitShim({ test.done(); }, + 'Inline Rule Control': { + //Not inlined + 'When props.disableInlineRules is true': testRulesAreNotInlined(undefined, true), + 'When context.disableInlineRules is true': testRulesAreNotInlined(true, undefined), + 'When context.disableInlineRules is true and props.disableInlineRules is true': testRulesAreNotInlined(true, true), + 'When context.disableInlineRules is false and props.disableInlineRules is true': testRulesAreNotInlined(false, true), + 'When props.disableInlineRules is true and context.disableInlineRules is null': testRulesAreNotInlined(null, true), + //Inlined + 'When context.disableInlineRules is false and props.disableInlineRules is false': testRulesAreInlined(false, false), + 'When context.disableInlineRules is true and props.disableInlineRules is false': testRulesAreInlined(true, false), + 'When context.disableInlineRules is false': testRulesAreInlined(false, undefined), + 'When props.disableInlineRules is false': testRulesAreInlined(undefined, false), + 'When neither props.disableInlineRules nor context.disableInlineRules are defined': testRulesAreInlined(undefined, undefined), + 'When props.disableInlineRules is undefined and context.disableInlineRules is null': testRulesAreInlined(null, undefined), + 'When props.disableInlineRules is false and context.disableInlineRules is null': testRulesAreInlined(null, false), + }, + 'peer between all types of peers and port range types'(test: Test) { // GIVEN const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); @@ -311,3 +330,432 @@ nodeunitShim({ test.done(); }, }); + +function testRulesAreInlined(contextDisableInlineRules: boolean | undefined | null, optionsDisableInlineRules: boolean | undefined) { + return { + 'When allowAllOutbound': { + 'new SecurityGroup will create an inline SecurityGroupEgress rule to allow all traffic'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + new SecurityGroup(stack, 'SG1', props); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + })); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addEgressRule rule will not modify egress rules'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule will add a new ingress rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'An external Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + }, + ], + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + }, + + 'When do not allowAllOutbound': { + 'new SecurityGroup rule will create an egress rule that denies all traffic'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + new SecurityGroup(stack, 'SG1', props); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + IpProtocol: 'icmp', + FromPort: 252, + ToPort: 86, + }, + ], + })); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + + test.done(); + }, + 'addEgressRule rule will add a new inline egress rule and remove the denyAllTraffic rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An inline Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'An inline Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + }, + ], + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule will add a new ingress rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'An external Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + }, + ], + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + IpProtocol: 'icmp', + FromPort: 252, + ToPort: 86, + }, + ], + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + }, + }; +} + + +function testRulesAreNotInlined(contextDisableInlineRules: boolean | undefined | null, optionsDisableInlineRules: boolean | undefined) { + return { + 'When allowAllOutbound': { + 'new SecurityGroup will create an external SecurityGroupEgress rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + })); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule rule will not remove external allowAllOutbound rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule rule will not add a new egress rule'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + + expect(stack).to(not(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + Description: 'An external Rule', + }))); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule rule will add a new external ingress rule even if it could have been inlined'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: true, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'An external Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + })); + test.done(); + }, + }, + + 'When do not allowAllOutbound': { + 'new SecurityGroup rule will create an external egress rule that denies all traffic'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + IpProtocol: 'icmp', + FromPort: 252, + ToPort: 86, + })); + test.done(); + }, + + 'addEgressRule rule will remove the rule that denies all traffic if another egress rule is added'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '255.255.255.255/32', + }))); + test.done(); + }, + + 'addEgressRule rule will add a new external egress rule even if it could have been inlined'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addEgressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'An external Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + })); + + expect(stack).to(not(haveResourceLike('AWS::EC2::SecurityGroupIngress', {}))); + test.done(); + }, + + 'addIngressRule will add a new external ingress rule even if it could have been inlined'(test: Test) { + // GIVEN + const stack = new Stack(); + stack.node.setContext(SECURITY_GROUP_DISABLE_INLINE_RULES_CONTEXT_KEY, contextDisableInlineRules); + const vpc = new Vpc(stack, 'VPC'); + const props: SecurityGroupProps = { vpc, allowAllOutbound: false, disableInlineRules: optionsDisableInlineRules }; + + // WHEN + const sg = new SecurityGroup(stack, 'SG1', props); + sg.addIngressRule(Peer.anyIpv4(), Port.tcp(86), 'An external Rule'); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Default/SG1', + VpcId: stack.resolve(vpc.vpcId), + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '0.0.0.0/0', + Description: 'An external Rule', + FromPort: 86, + IpProtocol: 'tcp', + ToPort: 86, + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: stack.resolve(sg.securityGroupId), + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + IpProtocol: 'icmp', + FromPort: 252, + ToPort: 86, + })); + test.done(); + }, + }, + }; +} \ No newline at end of file