Thursday, January 11, 2018

Modifying EC2 security groups via AWS Lambda functions

One task that comes up again and again is adding, removing or updating source CIDR blocks in various security groups in an EC2 infrastructure. This can be automated either fully or partially with the help of simple AWS Lambda functions.

Example 1: Checking a Dynamic DNS IP and replacing it in an EC2 security group

This scenario arises when you have a user without a static IP. They can still get a Dynamic DNS name and have it automatically point to their local dynamic IP. You can check for that name periodically, and update the appropriate rules within EC2 security group(s).

Here is an AWS Lambda function named UpdateSecurityGroupWithHomeIP and written in Python 2.7 that achieves this goal:

import boto3
import hashlib
import json
import copy
import urllib2

# ID of the security group we want to update

# Description of the security rule we want to replace

def lambda_handler(event, context):
    new_ip_address = list(event.values())[0]
    result = update_security_group(new_ip_address)
    return result

def update_security_group(new_ip_address):
    client = boto3.client('ec2')
    response = client.describe_security_groups(GroupIds=[SECURITY_GROUP_ID])
    group = response['SecurityGroups'][0]
    for permission in group['IpPermissions']:
        new_permission = copy.deepcopy(permission)
        ip_ranges = new_permission['IpRanges']
        for ip_range in ip_ranges:
            if ip_range['Description'] == 'My Home IP':
                ip_range['CidrIp'] = "%s/32" % new_ip_address
        client.revoke_security_group_ingress(GroupId=group['GroupId'], IpPermissions=[permission])
        client.authorize_security_group_ingress(GroupId=group['GroupId'], IpPermissions=[new_permission])
    return ""

A few observations:
  • it’s not trivial to do DNS lookups within Lambda, so I preferred to do the DNS lookup in the caller, and pass the resulting IP address as the sole argument to the above Lambda function — which is retrieved as new_ip_address in the lambda_handler function
  • in the update_security_group function I iterate through all permission objects in the IpPermissions list associated to the given security group and I create a deep copy of every permission
  • if any IP range in a permission object has the description “My Home IP”, I change its CidrIp property to the CIDR block corresponding to new_ip_address
  • finally, I revoke the old permission and authorize the new (deep-copied) permission

This Lambda function needs the proper permissions to modify security groups in EC2. I associated it with an IAM role which allows that. Here is the policy associated with that role:

    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Action": [
            "Resource": "arn:aws:logs:*:*:*"
            "Effect": "Allow",
            "Action": [
            "Resource": "*"

I call this Lambda function from a Jenkins job set to run periodically which does the DNS lookup first, then calls the above AWS Lambda function:

IPADDRESS=`dig | grep IN | grep -v ';' | awk '{print $5}'`
aws lambda invoke \
--invocation-type RequestResponse \
--function-name UpdateSecurityGroupWithHomeIP \
--region us-west-2 \
--log-type Tail \
--payload "{\"ip\":\"$IPADDRESS\"}" \

Example 2: adding a new IP/CIDR block to a several security groups

This is useful when you have several security groups and you need to add a new source CIDR block to all of them.

Here is a Lambda function for this purpose:

import boto3
import hashlib
import json
import urllib2

# Ports your application uses that need inbound permissions from the service for
    'web' : [80, 443], 
    'ssh': [22,] 
# Tags which identify the security groups you want to update
SECURITY_GROUP_TAG_FOR_WEB = { 'LambdaUpdate': 'web'}
SECURITY_GROUP_TAG_FOR_SSH = { 'LambdaUpdate': 'ssh'}

def lambda_handler(event, context):
    cidr_blocks = list(event.values())
    result = update_security_groups(cidr_blocks)
    return result

def update_security_groups(cidr_blocks):
    client = boto3.client('ec2')

    web_group = get_security_groups_for_update(client, SECURITY_GROUP_TAG_FOR_WEB)
    ssh_group = get_security_groups_for_update(client, SECURITY_GROUP_TAG_FOR_SSH)
    print ('Found ' + str(len(web_group)) + ' WebSecurityGroups to update')
    print ('Found ' + str(len(ssh_group)) + ' SshSecurityGroups to update')

    result = list()
    web_updated = 0
    ssh_updated = 0
    for group in web_group:
        for port in INGRESS_PORTS['web']:
            if update_security_group(client, group, cidr_blocks, port):
                web_updated += 1
                result.append('Updated ' + group['GroupId'])
    for group in ssh_group:
        for port in INGRESS_PORTS['ssh']:
            if update_security_group(client, group, cidr_blocks, port):
                ssh_updated += 1
                result.append('Updated ' + group['GroupId'])

    result.append('Updated ' + str(web_updated) + ' of ' + str(len(web_group)) + ' WebSecurityGroups')
    result.append('Updated ' + str(ssh_updated) + ' of ' + str(len(ssh_group)) + ' SshSecurityGroups')

    return result

def update_security_group(client, group, cidr_blocks, port):
    added = 0
    if len(group['IpPermissions']) > 0:
        for permission in group['IpPermissions']:
            if permission['FromPort'] <= port and permission['ToPort'] >= port:
                old_prefixes = list()
                to_add = list()
                for cidr_block in cidr_blocks:
                    if old_prefixes.count(cidr_block) == 0:
                        to_add.append({ 'CidrIp': cidr_block })
                        print(group['GroupId'] + ": Adding " + cidr_block + ":" + str(permission['ToPort']))
                added += add_permissions(client, group, permission, to_add)
        to_add = list()
        for cidr_block in cidr_blocks:
            to_add.append({ 'CidrIp': cidr_block })
            print(group['GroupId'] + ": Adding " + cidr_block + ":" + str(port))
        permission = { 'ToPort': port, 'FromPort': port, 'IpProtocol': 'tcp'}
        added += add_permissions(client, group, permission, to_add)

    print (group['GroupId'] + ": Added " + str(added))
    return (added > 0)

def add_permissions(client, group, permission, to_add):
    if len(to_add) > 0:
        add_params = {
            'ToPort': permission['ToPort'],
            'FromPort': permission['FromPort'],
            'IpRanges': to_add,
            'IpProtocol': permission['IpProtocol']

        client.authorize_security_group_ingress(GroupId=group['GroupId'], IpPermissions=[add_params])

    return len(to_add)

def get_security_groups_for_update(client, security_group_tag):
    filters = list();
    for key, value in security_group_tag.iteritems():
                { 'Name': "tag-key", 'Values': [ key ] },
                { 'Name': "tag-value", 'Values': [ value ] }

    response = client.describe_security_groups(Filters=filters)
    return response['SecurityGroups']

This function acts on security groups tagged “web” and “ssh”. For the ones tagged “web”, it adds new rules allowing the IP/CIDR block access to ports 80 and 443. For the groups tagged “ssh”, it does the same but for port 22.

The input for this function is {“ip1”: “$IPAddressBlock”} where IPAddressBlock is a Jenkins parameter that the user specifies when running the appropriate Jenkins job. In this case, I used the AWS Lambda Invocation build step in Jenkins.

Modifying EC2 security groups via AWS Lambda functions

One task that comes up again and again is adding, removing or updating source CIDR blocks in various security groups in an EC2 infrastructur...