Having recently moved out of my place, where I had comparitively great Internet access supplied with static IP addresses, I’m currently working mostly tethered to my phone. It turns out that good LTE service actually works pretty well for most things – even long distance SSH – but sitting on a cellular network with a dynamic IP address can get really annoying.
Yes, I should be establishing a VPN to reach internal services. Yes, opening holes for services on the wider Internet is totally a bad idea. But I have other security measures too, and a VPN would be just one extra. And surprise, tunnelling encrypted TCP over another encrypted TCP connection over a cellular network doesn’t provide a great experience.
So I’m dialling straight in over SSH, and here’s the little script I knocked together to make that just a little bit nicer than logging into the AWS console and manually creating security exceptions for myself (requires Python 3.6).
#!/usr/bin/env python3
import argparse, boto3, botocore, os, sys, urllib
from ipaddress import IPv4Address, IPv4Network
from pprint import pprint
class AuthError(Exception):
pass
def describe_rule(GroupId, IpPermissions):
print(f" Group ID: {GroupId}")
print(f" Port: {IpPermissions[0]['IpProtocol'].upper()}/{IpPermissions[0]['ToPort']}")
print(f" CIDR: {IpPermissions[0]['IpRanges'][0]['CidrIp']}")
print(f" Description: {IpPermissions[0]['IpRanges'][0]['Description']}")
def main():
name = os.environ.get('USER').capitalize()
parser = argparse.ArgumentParser()
parser.add_argument('-g', '--group', required=True, help='Name of security group to authorize')
parser.add_argument('-p', '--profile', default='default', help='AWS profile to use')
parser.add_argument('-r', '--region', default='', help='AWS region to use (defaults to profile setting)')
parser.add_argument('-t', '--port', type=int, action='append', default=[], help='TCP port to allow (default: 22)')
parser.add_argument('-d', '--description', default=name, help='Description for rule CIDR')
parser.add_argument('-D', '--delete', action='store_true', help='Delete other rules with matching description')
args = parser.parse_args()
description = args.description
try:
session = boto3.session.Session(profile_name=args.profile)
except botocore.exceptions.ProfileNotFound as e:
raise AuthError(e)
client_args = {}
if args.region:
client_args['region_name'] = args.region
client = session.client('ec2', **client_args)
groups = client.describe_security_groups(Filters=[{'Name': 'group-name', 'Values': [args.group]}])
if 'SecurityGroups' not in groups or len(groups['SecurityGroups']) == 0:
raise AuthError('Security group "{}" not found'.format(args.group))
elif len(groups['SecurityGroups']) > 1:
raise AuthError("More than one security group found for \"{0}\":\n - {1}".format(args.group, "\n - ".join([g['GroupName'] for g in groups['SecurityGroups']])))
group = groups['SecurityGroups'][0]
print('Found matching group: {}'.format(group['GroupName']))
try:
req = urllib.request.Request('https://ifconfig.co/ip', headers={'Accept': 'text/plain', 'User-Agent': 'curl/7.54.0'})
res = urllib.request.urlopen(req)
ip = res.read().decode('utf-8').strip()
except urllib.error.HTTPError as e:
raise AuthError('Could not determine public IP address, got {0} error when accessing ifconfig.co'.format(e.code))
cidr = ip + '/32'
print('Determined current public IP: {}'.format(ip))
if len(args.port):
ports = args.port
else:
ports = (22,)
for port in ports:
for perm in group['IpPermissions']:
if perm['IpProtocol'] == 'tcp' and perm['FromPort'] <= port and perm['ToPort'] >= port:
for iprange in perm['IpRanges']:
if IPv4Address(ip) in IPv4Network(iprange['CidrIp']):
print('{0} already authorized by {1}'.format(ip, iprange['CidrIp']))
return True
if args.delete:
for perm in group['IpPermissions']:
if perm['IpProtocol'] == 'tcp' and perm['FromPort'] <= port and perm['ToPort'] >= port:
for iprange in perm['IpRanges']:
if 'Description' in iprange and iprange['Description'] == args.description:
old_rule = {
'GroupId': group['GroupId'],
'IpPermissions': [{
'IpProtocol': perm['IpProtocol'],
'FromPort': perm['FromPort'],
'ToPort': perm['ToPort'],
'IpRanges': [{
'CidrIp': iprange['CidrIp'],
'Description': iprange['Description'],
}],
}],
}
print('Deleting rule:')
describe_rule(**old_rule)
client.revoke_security_group_ingress(**old_rule)
new_rule = {
'GroupId': group['GroupId'],
'IpPermissions': [{
'IpProtocol': 'tcp',
'FromPort': port,
'ToPort': port,
'IpRanges': [{
'CidrIp': cidr,
'Description': description,
}],
}],
}
print('Creating rule:')
describe_rule(**new_rule)
client.authorize_security_group_ingress(**new_rule)
if __name__ == "__main__":
try:
main()
except AuthError as e:
print(str(e), file=sys.stderr)
# vim: set ft=python ts=4 sts=4 sw=4 et:
Run it like this.
# Create a new rule in the employees security group
authorize-aws -g employees
# Create a new rule as above, and delete any existing rule with your name on it
authorize-aws -g employees -D
# As above, but using a different AWS profile than the default one
authorize-aws -p acme -g employees -D
# For Windows instances
authorize-aws -g employees -t 3389
# By default, the description contains your local user name, but can be overridden
authorize-aws -g employees -d 'Carmen mobile'