def amazon_security_group(name, vpc): """Create a security group authorizing access to aws services. :param vpc: vpc in which to create the group :type vpc: VPC :return: a security group :rtype: SecurityGroup """ ip_ranges = requests.get(IP_RANGES_URL).json()['prefixes'] # Retrieve first the complete list of ipv4 ip ranges for a given region amazon_ip_ranges = { k['ip_prefix'] for k in ip_ranges if k['region'] == vpc.region and 'ip_prefix' in k and k['service'] == 'AMAZON' } # Sustract the list of ip ranges corresponding to EC2 instances ec2_ip_ranges = { k['ip_prefix'] for k in ip_ranges if k['region'] == vpc.region and 'ip_prefix' in k and k['service'] == 'EC2' } amazon_ip_ranges -= ec2_ip_ranges # Authorize https on the resulting list of ip ranges # Note: the limit of rules per security group is set to 50 at AWS. # In case the number of ip ranges returned by Amazon would be greater # than that there would be need to split into several security groups sg = SecurityGroup(name, vpc, description='Allow acces to amazon services') for ip_range in amazon_ip_ranges: sg.add_rule(Ipv4EgressRule('https', ip_range)) return sg
def test_create_instance(): aws_env = AWSEnv(regions=['us-east-1'], stub=True) with default_region('us-east-1'): stub = aws_env.stub('ec2', region='us-east-1') stub.add_response( 'describe_images', {'Images': [{'ImageId': 'ami-1234', 'RootDeviceName': '/dev/sda1'}]}, {'ImageIds': ANY}) i = Instance('testmachine', AMI('ami-1234'), disk_size=20) assert i.properties i.add(EphemeralDisk('/dev/sdb', 0)) assert i.properties vpc = VPC('VPC', '10.10.0.0/16') subnet = Subnet('Subnet', vpc, '10.10.10.0/24') subnet = Subnet('Subnet2', vpc, '10.10.20.0/24') security_group = SecurityGroup('mysgroup', vpc) i.add(NetworkInterface(subnet, description='first network interface')) i.add(NetworkInterface(subnet, groups=[security_group], description='2nd network interface')) i.add(NetworkInterface(subnet, groups=[security_group], description='3rd network interface', device_index=3)) assert i.properties with pytest.raises(AssertionError): i.add("non valid ec2 device")
def github_security_groups(name, vpc, protocol): """Create a dict of security group authorizing access to github services. As the number of rules per security group is limited to 50, we create blocks of 50 rules. :param vpc: vpc in which to create the group :type vpc: VPC :param protocol: protocol to allow (https, ssh) :type protocol: str :return: a dict of security groups indexed by name :rtype: dict(str, SecurityGroup) """ ip_ranges = requests.get("https://api.github.com/meta").json()["git"] # Authorize ssh on the resulting list of ip ranges sgs = {} i = 0 limit = 50 sg_name = name + str(i) sg = SecurityGroup(sg_name, vpc, description="Allow access to github") sgs[sg_name] = sg for ip_range in ip_ranges: if len(sg.egress + sg.ingress) == limit: i += 1 sg_name = name + str(i) sg = SecurityGroup( sg_name, vpc, description=f"Allow access to GitHub {protocol}") sgs[sg_name] = sg sg.add_rule(Ipv4EgressRule(protocol, ip_range)) return sgs
def amazon_security_groups(name, vpc): """Create a dict of security group authorizing access to aws services. As the number of rules per security group is limited to 60, we create blocks of 60 rules. :param vpc: vpc in which to create the group :type vpc: VPC :return: a dict of security groups indexed by name :rtype: dict(str, SecurityGroup) """ def select_region(ip_range_record): """Select the VPN region and the us-east-1 region. Note that some global interface (e.g. sts) are only available in the us-east-1 region. """ return ip_range_record["region"] in (vpc.region, "us-east-1") ip_ranges = requests.get(IP_RANGES_URL).json()["prefixes"] # Retrieve first the complete list of ipv4 ip ranges for a given region amazon_ip_ranges = { k["ip_prefix"] for k in ip_ranges if select_region(k) and "ip_prefix" in k and k["service"] == "AMAZON" } # Substract the list of ip ranges corresponding to services # that we do no need to access to or that we access through VPC # endpoints to limit the number of security groups and rules. services_used = ("AMAZON", "S3") removable_ip_ranges = { k["ip_prefix"] for k in ip_ranges if select_region(k) and "ip_prefix" in k and k["service"] not in services_used } amazon_ip_ranges -= removable_ip_ranges # Authorize https on the resulting list of ip ranges sgs = {} i = 0 limit = 60 sg_name = name + str(i) sg = SecurityGroup(sg_name, vpc, description="Allow access to amazon services") sgs[sg_name] = sg for ip_range in amazon_ip_ranges: if len(sg.egress + sg.ingress) == limit: i += 1 sg_name = name + str(i) sg = SecurityGroup(sg_name, vpc, description="Allow acces to amazon services") sgs[sg_name] = sg sg.add_rule(Ipv4EgressRule("https", ip_range)) return sgs
def test_create_instance(): aws_env = AWSEnv(regions=["us-east-1"], stub=True) with default_region("us-east-1"): stub = aws_env.stub("ec2", region="us-east-1") stub.add_response( "describe_images", { "Images": [ {"ImageId": "ami-1234", "RootDeviceName": "/dev/sda1", "Tags": []} ] }, {"ImageIds": ANY}, ) i = Instance("testmachine", AMI("ami-1234"), disk_size=20) assert i.properties i.add(EphemeralDisk("/dev/sdb", 0)) assert i.properties i.add(EBSDisk("/dev/sdc", size=20, encrypted=True)) assert i.properties vpc = VPC("VPC", "10.10.0.0/16") subnet = Subnet("Subnet", vpc, "10.10.10.0/24") subnet = Subnet("Subnet2", vpc, "10.10.20.0/24") security_group = SecurityGroup("mysgroup", vpc) i.add(EC2NetworkInterface(subnet, description="first network interface")) i.add( EC2NetworkInterface( subnet, groups=[security_group], description="2nd network interface" ) ) i.add( EC2NetworkInterface( subnet, groups=[security_group], description="3rd network interface", device_index=3, ) ) assert i.properties with pytest.raises(AssertionError): i.add("non valid ec2 device")
def test_security_group(): vpc = VPC("vpc", cidr_block="10.10.0.0/16") rule1 = IngressRule("ssh", "10.10.1.1/32", description="ssh rule") rule2 = IngressRule("ip", "10.10.1.1/32", from_port=3389) rule2 = IngressRule("ip", "10.10.1.1/32", from_port=5000, to_port=5550) sg = SecurityGroup("SecurityGroup", vpc, description="basic security group", rules=[rule1, rule2]) assert sg.properties sg.add_rule(EgressRule("ip", "10.10.1.1/32", from_port=80)) assert sg.properties with pytest.raises(AssertionError): sg.add_rule("invalid object")
def amazon_security_groups(name, vpc): """Create a dict of security group authorizing access to aws services. As the number of rules per security group is limited to 50, we create blocks of 50 rules. :param vpc: vpc in which to create the group :type vpc: VPC :return: a dict of security groups indexed by name :rtype: dict(str, SecurityGroup) """ ip_ranges = requests.get(IP_RANGES_URL).json()['prefixes'] # Retrieve first the complete list of ipv4 ip ranges for a given region amazon_ip_ranges = { k['ip_prefix'] for k in ip_ranges if k['region'] == vpc.region and 'ip_prefix' in k and k['service'] == 'AMAZON' } # Sustract the list of ip ranges corresponding to EC2 instances ec2_ip_ranges = { k['ip_prefix'] for k in ip_ranges if k['region'] == vpc.region and 'ip_prefix' in k and k['service'] == 'EC2' } amazon_ip_ranges -= ec2_ip_ranges # Authorize https on the resulting list of ip ranges sgs = {} i = 0 limit = 50 sg_name = name + str(i) sg = SecurityGroup(sg_name, vpc, description='Allow acces to amazon services') sgs[sg_name] = sg for ip_range in amazon_ip_ranges: if len(sg.egress + sg.ingress) == limit: i += 1 sg_name = name + str(i) sg = SecurityGroup(sg_name, vpc, description='Allow acces to amazon services') sgs[sg_name] = sg sg.add_rule(Ipv4EgressRule('https', ip_range)) return sgs
def test_create_fortress_with_too_much_sgs(): aws_env = AWSEnv(regions=["us-east-1"], stub=True) with default_region("us-east-1"): stub = aws_env.stub("ec2", region="us-east-1") stub.add_response( "describe_images", { "Images": [{ "ImageId": "ami-1234", "RootDeviceName": "/dev/sda1", "Tags": [] }] }, {"ImageIds": ANY}, ) d = PolicyDocument().append( Allow( to="s3:GetObject", on=["arn:aws:s3:::mybucket", "arn:aws:s3:::mybucket/*"], )) p = Policy("InternalPolicy", d) f = Fortress("myfortress", bastion_ami=None, internal_server_policy=p) # Adding 16 extra security groups should raise an exception (The maximum # number of security groups is 16 and there is a default InternalSG) sg_groups = [ SecurityGroup(name=f"sg{id}", vpc=f.vpc.vpc) for id in range(16) ] with pytest.raises(AWSFortressError): f.add_private_server( AMI("ami-1234"), ["server1"], amazon_access=False, github_access=False, extra_groups=sg_groups, )
def __init__(self, name, internal_server_policy, bastion_ami=None, allow_ssh_from=None, description=None, vpc_cidr_block='10.10.0.0/16', private_cidr_block='10.10.0.0/17', public_cidr_block='10.10.128.0/17'): """Create a VPC Fortress. This create a vpc with a public and a private subnet. Servers in the private subnet are only accessible through a bastion machine declare in the public subnet. :param name: stack name :type name: str :param internal_server_policy: policy associated with instance role of private servers :type internal_server_policy: Policy :param bastion_ami: AMI used for the bastion server. If None no bastion is setup :type bastion_ami: AMI | None :param allow_ssh_from: ip ranges from which ssh can be done to the bastion. if bastion_ami is None, parameter is discarded :type allow_ssh_from: str | None :param vpc_cidr_block: ip ranges for the associated vpc :type vpc_cidr_block: str :param private_cidr_block: ip ranges (subset of vpc_cidr_block) used for private subnet :type private_cidr_block: str :param public_cidr_block: ip ranges (subset of vpc_cidr_block) used for public subnet :type public_cidr_block: str """ super(Fortress, self).__init__(name, description) # Create VPC along with the two subnets self.add(VPCStack(self.name + 'VPC', vpc_cidr_block)) self.vpc.add_subnet(self.name + 'PublicNet', public_cidr_block, is_public=True, use_nat=True) self.vpc.add_subnet(self.name + 'PrivateNet', private_cidr_block, nat_to=self.name + 'PublicNet') self.add(amazon_security_group(self.name + 'AmazonServices', self.vpc.vpc)) if bastion_ami is not None: # Allow ssh to bastion only from a range of IP address self.add(SecurityGroup( self.name + 'BastionSG', self.vpc.vpc, description='security group for bastion servers', rules=[Ipv4IngressRule('ssh', cidr) for cidr in allow_ssh_from])) # Create the bastion self.add(Instance(self.name + 'Bastion', bastion_ami)) self.bastion.tags['Name'] = 'Bastion (%s)' % self.name self.bastion.add( NetworkInterface(self.public_subnet.subnet, public_ip=True, groups=[self[self.name + 'BastionSG']])) # Create security group for internal servers self.add(SecurityGroup( self.name + 'InternalSG', self.vpc.vpc, description='Allow ssh inside VPC', rules=[Ipv4IngressRule('ssh', self.public_subnet.cidr_block)])) else: # If no bastion is used do not authorize ssh inside the vpc self.add(SecurityGroup( self.name + 'InternalSG', self.vpc.vpc, description='Do not allow ssh inside VPC')) ir = InstanceRole(self.name + 'PrivServerInstanceRole') ir.add_policy(internal_server_policy) self.add(ir)
def __init__( self, name, internal_server_policy, bastion_ami=None, allow_ssh_from=None, description=None, vpc_cidr_block="10.10.0.0/16", private_cidr_block="10.10.0.0/17", public_cidr_block="10.10.128.0/18", aws_endpoints_cidr_block="10.10.192.0/18", ): """Create a VPC Fortress. This create a vpc with a public and a private subnet. Servers in the private subnet are only accessible through a bastion machine declared in the public subnet. An additional subnet is created to host AWS services endpoints network interfaces. :param name: stack name :type name: str :param internal_server_policy: policy associated with instance role of private servers :type internal_server_policy: Policy :param bastion_ami: AMI used for the bastion server. If None no bastion is setup :type bastion_ami: AMI | None :param allow_ssh_from: ip ranges from which ssh can be done to the bastion. if bastion_ami is None, parameter is discarded :type allow_ssh_from: str | None :param vpc_cidr_block: ip ranges for the associated vpc :type vpc_cidr_block: str :param private_cidr_block: ip ranges (subset of vpc_cidr_block) used for private subnet :type private_cidr_block: str :param public_cidr_block: ip ranges (subset of vpc_cidr_block) used for public subnet :type public_cidr_block: str :param aws_endpoints_cidr_block: ip ranges (subset of vpc_cidr_block) used for aws endpoints :type aws_endpoints_cidr_block: str """ super().__init__(name, description) # Create VPC along with the three subnets self.add(VPCStack(self.name + "VPC", vpc_cidr_block)) self.vpc.add_subnet(self.name + "PublicNet", public_cidr_block, is_public=True, use_nat=True) self.vpc.add_subnet(self.name + "PrivateNet", private_cidr_block, nat_to=self.name + "PublicNet") self.vpc.add_subnet(self.name + "AWSEndpointsNet", aws_endpoints_cidr_block) self.amazon_groups = {} self.github_groups = {} if bastion_ami is not None: # Allow ssh to bastion only from a range of IP address self.add( SecurityGroup( self.name + "BastionSG", self.vpc.vpc, description="security group for bastion servers", rules=[ Ipv4IngressRule("ssh", cidr) for cidr in allow_ssh_from ], )) # Create the bastion self.add(Instance(self.name + "Bastion", bastion_ami)) self.bastion.tags["Name"] = "Bastion (%s)" % self.name self.bastion.add( EC2NetworkInterface( self.public_subnet.subnet, public_ip=True, groups=[self[self.name + "BastionSG"]], )) # Create security group for internal servers self.add( SecurityGroup( self.name + "InternalSG", self.vpc.vpc, description=("Allow ssh inside VPC and allow https " "to VPC endpoints subnet"), rules=[ Ipv4IngressRule("ssh", self.public_subnet.cidr_block), Ipv4EgressRule("https", self.aws_endpoints_subnet.cidr_block), ], )) else: # If no bastion is used do not authorize ssh inside the vpc self.add( SecurityGroup( self.name + "InternalSG", self.vpc.vpc, description=("Do not allow ssh inside VPC but allow https " "to the VPC endpoints subnet."), rules=[ Ipv4EgressRule("https", self.aws_endpoints_subnet.cidr_block) ], )) # Create security group for endpoints self.add( SecurityGroup( self.name + "InterfaceEndpointsSG", self.vpc.vpc, description=("Allow https from the private subnet"), rules=[ Ipv4IngressRule("https", self.private_subnet.cidr_block) ], )) ir = InstanceRole(self.name + "PrivServerInstanceRole") ir.add_policy(internal_server_policy) self.add(ir)