├── firewall_client.py ├── firewall_client.sh ├── lambda_function.py └── readme.md /firewall_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import requests, sys 3 | 4 | # enter the API key provided by your API gateway 5 | k = {'x-api-key': 'YOUR_API_KEY'} 6 | # enter the URL used by your API gateway 7 | u = 'https://xxz6rzgcn6.execute-api.us-east-1.amazonaws.com/prod/DevelopmentFirewallUpdater' 8 | ##### do not touch anything below this line ##### 9 | 10 | def whitelist(): 11 | r = requests.get(u, headers = k) 12 | print r.content 13 | 14 | whitelist() 15 | -------------------------------------------------------------------------------- /firewall_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -H 'x-api-key: YOUR_API_KEY' -v https://xxz6rzgcn6.execute-api.us-east-1.amazonaws.com/prod/DevelopmentFirewallUpdater 3 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # 27/04/2017 - fork of https://github.com/marekq/aws-lambda-firewall completely rewritten by ets for differing usecase 4 | # 5 | 6 | cli_testing_mode = False 7 | # TTL for dynamically whitelisted IPs 8 | dynamicWhitelistDurationSeconds = 60 * 60 * 24 9 | # The region and VPC in which the whitelist will be applied 10 | awsRegion = 'us-east-1' 11 | vpcId = 'vpc-de87bab8' 12 | # The SimpleDB domain name for recording the whitelist 13 | simpleDBDomain = 'lambda-firewall' 14 | 15 | # List of the ElasticIPs (We don't want to hardcode InstanceIds here since those might change) to whitelist for port 22 access 16 | whitelistSSHTargets = ["34.204.105.155"] 17 | # AWS currently limits any instance to 5 security groups, so that's the maximum number of groups you should define 18 | whitelistSSHGroupNames = ['dyn-SSH-1','dyn-SSH-2','dyn-SSH-3','dyn-SSH-4'] 19 | 20 | # List of the ELB ARNs to whitelist for port 80 & 443 access 21 | whiteListHTTPSTargets = ["arn:aws:elasticloadbalancing:us-east-1:903373720037:loadbalancer/app/DevelopmentELB/7b4a10ac9563927a"] 22 | # AWS currently limits any instance to 5 security groups, so that's the maximum number of groups you should define 23 | whitelistHTTPSGroupNames = ['dyn-HTTPS-1','dyn-HTTPS-2','dyn-HTTPS-3','dyn-HTTPS-4'] 24 | 25 | ##### do not touch anything below this line ##### 26 | 27 | AWS_MAX_INGRESS_RULES_PER_SG = 100 28 | 29 | import boto3, re, time, logging, sys 30 | from botocore.exceptions import ClientError 31 | from pprint import pformat 32 | from operator import itemgetter 33 | logger = logging.getLogger() 34 | logger.setLevel(logging.INFO) 35 | ch = logging.StreamHandler(sys.stdout) 36 | ch.setLevel(logging.INFO) 37 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 38 | ch.setFormatter(formatter) 39 | logger.addHandler(ch) 40 | 41 | results = [] 42 | 43 | def add_ingress_rule(ec2Client,sdbClient,ipToWhitelist,sgId,portPairs): 44 | try: 45 | for pair in portPairs: 46 | ec2Client.authorize_security_group_ingress(GroupId = sgId, IpProtocol = 'TCP', CidrIp = ipToWhitelist+'/32', FromPort = pair[0], ToPort = pair[1]) 47 | except ClientError as e: 48 | if e.response['Error']['Code'] != 'InvalidPermission.Duplicate': 49 | logger.error("Error while adding ingress rule: "+str(e)) 50 | return 51 | 52 | creationTime = int(time.time()) 53 | expireTime = creationTime + int(dynamicWhitelistDurationSeconds) 54 | sdbClient.put_attributes( 55 | DomainName=simpleDBDomain, 56 | ItemName=ipToWhitelist, 57 | Attributes=[ 58 | { 'Name': 'ipAddress', 'Value': ipToWhitelist, 'Replace': True }, 59 | { 'Name': 'sgId', 'Value': sgId, 'Replace': False }, 60 | { 'Name': 'expirationTime', 'Value': str(expireTime), 'Replace': True }, 61 | ] 62 | ) 63 | return 64 | 65 | def add_https_security_group(ec2Client,sgName): 66 | sg = ec2Client.create_security_group(GroupName = sgName, Description = 'Knock first firewall maintained group.', VpcId = vpcId) 67 | sgId = sg.get(u'GroupId') 68 | elbClient = get_bolo_client('elbv2') 69 | for lbARN in whiteListHTTPSTargets: 70 | lbDetails = elbClient.describe_load_balancers( 71 | LoadBalancerArns=[lbARN] 72 | ) 73 | for lb in lbDetails[u'LoadBalancers']: 74 | sgIds = lb[u'SecurityGroups'] 75 | sgIds.append(sgId) 76 | elbClient.set_security_groups( 77 | LoadBalancerArn=lbARN, 78 | SecurityGroups=sgIds 79 | ) 80 | 81 | logger.info("Created "+sgName+" ["+sgId+"]") 82 | return sgId 83 | 84 | def update_https_access(ec2Client,sdbClient,ipToWhitelist): 85 | portPairs = [ [80,80],[443,443] ] 86 | for sgName in whitelistHTTPSGroupNames: 87 | securityGroups = ec2Client.describe_security_groups(Filters=[ 88 | { 89 | 'Name': 'group-name', 90 | 'Values': [sgName], 91 | }, 92 | ]) 93 | if len(securityGroups['SecurityGroups']) <= 0: 94 | logger.info("Unable to describe security group "+sgName+" so we'll attempt to create it.") 95 | sgId = add_https_security_group(ec2Client,sgName) 96 | add_ingress_rule(ec2Client,sdbClient,ipToWhitelist,sgId,portPairs) 97 | return 98 | else: 99 | for sg in securityGroups['SecurityGroups']: 100 | ingressRules = sg['IpPermissions'] 101 | if len(ingressRules) <= (AWS_MAX_INGRESS_RULES_PER_SG - len(portPairs) ): 102 | add_ingress_rule(ec2Client,sdbClient,ipToWhitelist,sg.get(u'GroupId'),portPairs) 103 | return 104 | 105 | logger.error("No free rule slots available across all security groups ["+whitelistHTTPSGroupNames+"]. Consider increasing your security group limit for HTTPS whitelisting.") 106 | 107 | def add_ssh_security_group(ec2Client,sgName): 108 | sg = ec2Client.create_security_group(GroupName = sgName, Description = 'Knock first firewall maintained group.', VpcId = vpcId) 109 | sgId = sg.get(u'GroupId') 110 | ec2Instances = ec2Client.describe_instances(Filters=[ 111 | { 112 | 'Name': 'ip-address', 113 | 'Values': whitelistSSHTargets 114 | }, 115 | ]) 116 | for x in ec2Instances[u'Reservations']: 117 | for y in range(len(x[u'Instances'])): 118 | instanceId = x[u'Instances'][int(y)][u'InstanceId'] 119 | securityGroups = ec2Client.describe_instance_attribute(Attribute='groupSet',InstanceId=instanceId) 120 | allSGIds = [group[u'GroupId'] for group in securityGroups[u'Groups']] 121 | allSGIds.append(sgId) 122 | ec2Client.modify_instance_attribute(Groups = allSGIds, InstanceId = instanceId) 123 | 124 | logger.info("Created "+sgName+" ["+sgId+"]") 125 | return sgId 126 | 127 | def update_ssh_access(ec2Client,sdbClient,ipToWhitelist): 128 | portPairs = [ [22,22] ] 129 | for sgName in whitelistSSHGroupNames: 130 | securityGroups = ec2Client.describe_security_groups(Filters=[ 131 | { 132 | 'Name': 'group-name', 133 | 'Values': [sgName], 134 | }, 135 | ]) 136 | if len(securityGroups['SecurityGroups']) <= 0: 137 | logger.info("Unable to describe security group "+sgName+" so we'll attempt to create it.") 138 | sgId = add_ssh_security_group(ec2Client,sgName) 139 | add_ingress_rule(ec2Client,sdbClient,ipToWhitelist,sgId,portPairs) 140 | return 141 | else: 142 | for sg in securityGroups['SecurityGroups']: 143 | ingressRules = sg['IpPermissions'] 144 | if len(ingressRules) <= (AWS_MAX_INGRESS_RULES_PER_SG - len(portPairs) ): 145 | add_ingress_rule(ec2Client,sdbClient,ipToWhitelist,sg.get(u'GroupId'),portPairs) 146 | return 147 | 148 | logger.error("No free rule slots available across all security groups ["+whitelistSSHGroupNames+"]. Consider increasing your security group limit for SSH whitelisting.") 149 | 150 | def whitelist_ip(ipToWhitelist): 151 | sdbClient = get_bolo_client('sdb') 152 | query = 'select * from `'+simpleDBDomain+'` where `ipAddress` = "'+ipToWhitelist+'"' 153 | logger.debug('Executing: '+query) 154 | existingRules = sdbClient.select( 155 | SelectExpression=query 156 | ) 157 | try: 158 | logger.info("Whitelisting "+ipToWhitelist) 159 | if 'Items' not in existingRules or len(existingRules['Items']) <= 0: 160 | ec2Client = get_bolo_client('ec2') 161 | update_ssh_access(ec2Client,sdbClient,ipToWhitelist) 162 | update_https_access(ec2Client,sdbClient,ipToWhitelist) 163 | results.append('Whitelisted '+ipToWhitelist+' for access.') 164 | else: 165 | results.append("Your IP is already whitelisted for access") 166 | except Exception as e: 167 | msg = "Unable to whitelist "+ipToWhitelist+" due to: "+pformat(e) 168 | logger.error(msg) 169 | results.append(msg) 170 | 171 | def get_bolo_client(serv): 172 | s = boto3.session.Session() 173 | client = s.client(serv, region_name = awsRegion ) 174 | return client 175 | 176 | def remove_expired_rules(): 177 | logger.info("Removing expired rules.") 178 | sdbClient = get_bolo_client('sdb') 179 | now = int(time.time()) 180 | query = "select * from `"+simpleDBDomain+"` where expirationTime < '"+str(now)+"'" 181 | logger.debug('Executing this: '+query) 182 | expiredRules = sdbClient.select( 183 | SelectExpression=query 184 | ) 185 | logger.debug("Results: "+pformat(expiredRules)) 186 | if 'Items' in expiredRules and len(expiredRules['Items']) > 0: 187 | ec2Client = get_bolo_client('ec2') 188 | for expiredItem in expiredRules['Items']: 189 | logger.debug("Attempting to delete: "+pformat(expiredItem)) 190 | expiredRuleSecurityGroupIdList = [attr['Value'] for attr in expiredItem['Attributes'] if attr['Name'] == 'sgId'] 191 | expiredRuleCidr = [attr['Value'] for attr in expiredItem['Attributes'] if attr['Name'] == 'ipAddress'][0] + '/32' 192 | securityGroups = ec2Client.describe_security_groups(Filters=[ 193 | { 194 | 'Name': 'group-id', 195 | 'Values': expiredRuleSecurityGroupIdList, 196 | }, 197 | ]) 198 | try: 199 | for sg in securityGroups['SecurityGroups']: 200 | ingressPorts = [attr['FromPort'] for attr in sg['IpPermissions'] if expiredRuleCidr in map(itemgetter('CidrIp'), attr['IpRanges']) ] 201 | for port in ingressPorts: 202 | ec2Client.revoke_security_group_ingress( 203 | GroupId=sg['GroupId'], 204 | IpPermissions=[ 205 | { 206 | 'IpProtocol': 'TCP', 207 | 'FromPort': port, 208 | 'ToPort': port, 209 | 'IpRanges': [ 210 | { 211 | 'CidrIp': expiredRuleCidr 212 | }, 213 | ], 214 | }, 215 | ] 216 | ) 217 | results.append('Expired access for '+expiredRuleCidr) 218 | response = sdbClient.delete_attributes( 219 | DomainName=simpleDBDomain, 220 | ItemName=expiredItem['Name'], 221 | Attributes=expiredItem['Attributes'] 222 | ) 223 | except Exception as e: 224 | msg = "Unable to revoke ingress rule: "+pformat(e) 225 | results.append(msg) 226 | logger.error(msg) 227 | 228 | def lambda_handler(event, context): 229 | logger.debug('got event{}'.format(event)) 230 | try: 231 | try: 232 | ipToWhitelist = event['requestContext']['identity']['sourceIp'] 233 | if ipToWhitelist: 234 | whitelist_ip(ipToWhitelist) 235 | else: 236 | remove_expired_rules() 237 | except KeyError as e: 238 | remove_expired_rules() 239 | except ClientError as e: 240 | if e.response['Error']['Code'] == 'NoSuchDomain': 241 | logger.info("Our SimpleDB domain does not exist. Creating it and retrying request.") 242 | sdbClient = get_bolo_client('sdb') 243 | sdbClient.create_domain( 244 | DomainName=simpleDBDomain 245 | ) 246 | lambda_handler(event,context) 247 | else: 248 | logger.error("Error: "+str(e)) 249 | 250 | 251 | return { 252 | "isBase64Encoded": False, 253 | "statusCode": 200, 254 | "headers": { }, 255 | "body": '. '.join(results) 256 | } 257 | 258 | if cli_testing_mode: 259 | exampleLambdaAPIGatewayProxyRequest = {u'body': u'{"test":"body"}', u'resource': u'/{proxy+}', u'requestContext': {u'resourceId': u'123456', u'apiId': u'1234567890', u'resourcePath': u'/{proxy+}', u'httpMethod': u'POST', u'requestId': u'c6af9ac6-7b61-11e6-9a41-93e8deadbeef', u'stage': u'prod', u'identity': {u'apiKey': None, u'userArn': None, u'sourceIp': u'127.0.0.42', u'caller': None, u'cognitoIdentityId': None, u'user': None, u'cognitoIdentityPoolId': None, u'userAgent': u'Custom User Agent String', u'accountId': None, u'cognitoAuthenticationType': None, u'cognitoAuthenticationProvider': None}, u'accountId': u'123456789012'}, u'queryStringParameters': {u'foo': u'bar'}, u'httpMethod': u'POST', u'pathParameters': {u'proxy': u'path/to/resource'}, u'headers': {u'Via': u'1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.8', u'CloudFront-Is-Desktop-Viewer': u'true', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-Port': u'443', u'X-Forwarded-For': u'127.0.0.1, 127.0.0.2', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', u'Upgrade-Insecure-Requests': u'1', u'Host': u'1234567890.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'X-Amz-Cf-Id': u'cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==', u'CloudFront-Is-Tablet-Viewer': u'false', u'Cache-Control': u'max-age=0', u'User-Agent': u'Custom User Agent String', u'CloudFront-Is-Mobile-Viewer': u'false', u'Accept-Encoding': u'gzip, deflate, sdch'}, u'stageVariables': {u'baz': u'qux'}, u'path': u'/path/to/resource'} 260 | result = lambda_handler(exampleLambdaAPIGatewayProxyRequest,None) 261 | logger.info("Result of lambda invocation: "+pformat(result) ) 262 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | aws-lambda-firewall 2 | =================== 3 | Use a secure & convenient "knock for access" protocol for creating/expiring AWS Security Group ingress rules. 4 | 5 | Current usecase scenarios are: 6 | 7 | * Allow authorized users to add their current IP address to multiple security groups thereby granting access to ports 22,80,443 on specific [EC2 instances](https://aws.amazon.com/ec2/) and/or [ELBs](https://aws.amazon.com/elasticloadbalancing/) 8 | * Conveniently support access for users behind dynamic IP addresses without opening up sensitive ports to the public internet 9 | 10 | End users "knock for access" using a valid API Gateway token. By making a valid call to this AWS Lambda function behind an AWS API Gateway, the end user's IP is added (for 24 hours) to security groups that permit access to other resources. 11 | 12 | This allows us to restrict access to ports (e.g. SSH port on our Bastion host or 443 on the ELB that fronts our development & test servers) but allow access to authorized users without the need to establish a VPN or otherwise modify routing across the Internet. 13 | 14 | IAM policies required by the role assigned to the lambda 15 | --------------------------------------------------------- 16 | ``` 17 | { 18 | "Version": "2012-10-17", 19 | "Statement": [ 20 | { 21 | "Sid": "securityGroupManipulationPermissions", 22 | "Effect": "Allow", 23 | "Action": [ 24 | "ec2:AuthorizeSecurityGroupEgress", 25 | "ec2:AuthorizeSecurityGroupIngress", 26 | "ec2:CreateSecurityGroup", 27 | "ec2:DeleteSecurityGroup", 28 | "ec2:DescribeInstanceAttribute", 29 | "ec2:DescribeInstanceStatus", 30 | "ec2:DescribeInstances", 31 | "ec2:DescribeNetworkAcls", 32 | "ec2:DescribeSecurityGroups", 33 | "ec2:ModifyInstanceAttribute", 34 | "ec2:RevokeSecurityGroupEgress", 35 | "ec2:RevokeSecurityGroupIngress", 36 | "elasticloadbalancing:DescribeLoadBalancerAttributes", 37 | "elasticloadbalancing:DescribeLoadBalancers", 38 | "elasticloadbalancing:ModifyLoadBalancerAttributes", 39 | "elasticloadbalancing:SetSecurityGroups" 40 | ], 41 | "Resource": [ 42 | "*" 43 | ] 44 | }, 45 | { 46 | "Sid": "cloudwatchloggingforwhitelister", 47 | "Effect": "Allow", 48 | "Action": [ 49 | "logs:*" 50 | ], 51 | "Resource": [ 52 | "arn:aws:logs:*:*:*" 53 | ] 54 | }, 55 | { 56 | "Sid": "simpleDBdatastorageforwhitelister", 57 | "Effect": "Allow", 58 | "Action": [ 59 | "sdb:*" 60 | ], 61 | "Resource": [ 62 | "arn:aws:sdb:us-east-1:903373720037:domain/SIMPLEDB_DOMAIN_NAME_DECLARED_IN_LAMBDA_SCRIPT" 63 | ] 64 | } 65 | ] 66 | } 67 | ``` 68 | 69 | Description 70 | ------------ 71 | 72 | The Lambda firewall can be used in sensitive environments where you want to keep strict control over security groups. Users with a valid API gateway key can make a request to temporarily whitelist their IP address for a specific duration without the need for access to the console or IAM permissions to alter Security Groups. After the whitelist entry expires, it is automatically removed. You no longer need to add or remove ingress rules or security groups manually, which is especially useful for users with many different source/origin IP addresses. 73 | 74 | Installation 75 | ------------ 76 | 77 | 1. Add the Lambda function (lambda_function.py) to your account with a Python 2.x handler "lambda_function.lambda_handler" 78 | 2. Use the API Gateway trigger and for Security use "Open with Access Key" 79 | 3. Configure the Lambda with the IAM Role defined using the rules in the section above 80 | 4. Next, create a second trigger for your Lambda using CloudWatch and set it to call the lambda periodically to delete expired groups 81 | 5. Under API Gateway, create a Usage Plan with a set of API Keys 82 | 5. Add a valid API key and the correct Lambda URL in the "firewall_client" scripts and distribute it to your users. 83 | 84 | Usage 85 | ----- 86 | - To whitelist your IP, call the firewall_client (python and CURL examples included) manually 87 | 88 | History 89 | ------- 90 | * 2017-05-01 This was initially a fork of https://github.com/marekq/aws-lambda-firewall but was subsequently rewritten 91 | 92 | Contact 93 | ------- 94 | 95 | For any questions or fixes, please reach out via github! 96 | --------------------------------------------------------------------------------