diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72364f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c3a4b7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Johannes Ebke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ac97c4 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# aws-emergency + +This script encapsulates some actions that can be taken if you suspect that some of your +access credentials or IAM users have been compromised. + +## Warning + +This script can be used to restrict activities on your account. It does NOT currently +delete any resources, and its direct effects should be reversible. However, it can also +prevent activity by AWS services, e.g. log storage, CloudFormation actions, etc. and +BREAK your AWS setup. + +Be sure to understand what this script does before using it. + +## Usage + +You need to have python (both 2 or 3 work) with boto3 installed, +as well as AWS credentials which can be picked up by boto3. + +The most useful (but also most drastic) action of this tool is to add inline DENY ALL policies +named "EmergencyUserLock-\" and "EmergencyRoleLock-\" +to both IAM users and roles (except the user which executes this action): + +./aws\_emergency.py --lock-all + +If you accidentally executed this command for testing and now want to undo this action, do: + +./aws\_emergency.py --unlock-all + +To lock individual users or roles, use --lock-user \, --unlock-user \ +and --lock-role \, --unlock-role \. diff --git a/aws_emergency.py b/aws_emergency.py new file mode 100755 index 0000000..539d15e --- /dev/null +++ b/aws_emergency.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# pylint: disable=import-error,invalid-name +"""Module providing emergency shutdown functions""" +from __future__ import print_function + +from multiprocessing.pool import ThreadPool +from random import randint +import json + +import boto3 + +LOCK_DOCUMENT_STRING = json.dumps({ + 'Version': '2012-10-17', + 'Statement': [{ + 'Sid': 'EmergencyLock', + 'Effect': 'Deny', + 'Action': ['*'], + 'Resource': ['*'] + }] +}) + + +def get_all_access_keys(): + """List access keys from all IAM users in this account""" + iam = boto3.client('iam') + usernames = [user['UserName'] for user in iam.list_users()['Users']] + access_keys = sum(ThreadPool(16).imap( + lambda user: iam.list_access_keys(UserName=user)['AccessKeyMetadata'], + usernames + ), []) + return access_keys + + +def update_access_key(iam, access_key, enabled): + """Set the access key described by the given document to either enabled or disabled""" + params = { + 'AccessKeyId': access_key['AccessKeyId'], + 'Status': 'Active' if enabled else 'Inactive' + } + if 'UserName' in access_key: + params['UserName'] = access_key['UserName'] + + iam.update_access_key(**params) + + return '{}abled access key from {} with id {}'.format( + 'En' if enabled else 'Dis', + access_key.get('UserName', ''), + access_key['AccessKeyId'] + ) + + +def disable_all_access_keys(): + """Disable the access keys of all IAM users in this account, except the one used currently""" + access_keys = get_all_access_keys() + session = boto3.session.Session() + iam = session.client('iam') + + my_access_key_id = session.get_credentials().access_key + keys_to_disable = [access_key for access_key in access_keys + if access_key['Status'] == 'Active' + and access_key['AccessKeyId'] != my_access_key_id] + def disable(key): + """Disable this access key""" + update_access_key(iam, key, enabled=False) + for output in ThreadPool(16).imap(disable, keys_to_disable): + print(output) + for access_key in access_keys: + if access_key['AccessKeyId'] == my_access_key_id: + print('Skipped own access key:', access_key['AccessKeyId']) + + +def enable_all_access_keys(): + """Enable all access keys of all IAM users in this account. + + This also enables access keys that may have already been disabled for other reasons!""" + access_keys = get_all_access_keys() + iam = boto3.client('iam') + for access_key in access_keys: + if access_key['Status'] == 'Inactive': + print(update_access_key(iam, access_key, enabled=True)) + + +def lock_user(username): + """Add an inline user policy to the given user with a DENY ALL rule, denying all activity. + + The affected user can still login, but should not be able do to anything.""" + iam = boto3.client('iam') + iam.put_user_policy( + UserName=username, + PolicyName='EmergencyUserLock-{}'.format(randint(1e12, 9e12)), + PolicyDocument=LOCK_DOCUMENT_STRING + ) + return 'Locked user ' + username + + +def unlock_user(username): + """Revert the action of lock_user. It does not grant any additional rights""" + iam = boto3.client('iam') + policy_names = iam.list_user_policies(UserName=username)['PolicyNames'] + for policy_name in policy_names: + if policy_name.startswith('EmergencyUserLock-'): + iam.delete_user_policy(UserName=username, PolicyName=policy_name) + return 'Unlocked user ' + username + + +def lock_role(rolename): + """Add an inline role policy to the given role with a DENY ALL rule, denying all activity. + + The affected role can still be assumed, but should not be able do to anything.""" + iam = boto3.client('iam') + iam.put_role_policy( + RoleName=rolename, + PolicyName='EmergencyRoleLock-{}'.format(randint(1e12, 9e12)), + PolicyDocument=LOCK_DOCUMENT_STRING + ) + return 'Locked role ' + rolename + + +def unlock_role(rolename): + """Revert the action of lock_role. It does not grant any additional rights""" + iam = boto3.client('iam') + policy_names = iam.list_role_policies(RoleName=rolename)['PolicyNames'] + for policy_name in policy_names: + if policy_name.startswith('EmergencyRoleLock-'): + iam.delete_role_policy(RoleName=rolename, PolicyName=policy_name) + return 'Unlocked role ' + rolename + + +def lock_all(): + """List and lock all IAM users and roles in this account. + + This may affect the operation of AWS services that can assume roles to take action on + the users behalf, e.g. CloudFormation""" + iam = boto3.client('iam') + iam.list_access_keys() + rolenames = [role['RoleName'] for role in iam.list_roles()['Roles']] + for output in ThreadPool(16).imap(lock_role, rolenames): + print(output) + my_username = iam.get_user()['User'].get('UserName', '') + usernames = [user['UserName'] for user in iam.list_users()['Users'] + if user['UserName'] != my_username] + for output in ThreadPool(16).imap(lock_user, usernames): + print(output) + + +def unlock_all(): + """Remove emergency locks from all IAM users and roles in this account.""" + iam = boto3.client('iam') + usernames = [user['UserName'] for user in iam.list_users()['Users']] + for output in ThreadPool(16).imap(unlock_user, usernames): + print(output) + rolenames = [role['RoleName'] for role in iam.list_roles()['Roles']] + for output in ThreadPool(16).imap(unlock_role, rolenames): + print(output) + + +def main(): + """Parse CLI arguments to either list services, operations, queries or existing pickles""" + import argparse + parser = argparse.ArgumentParser( + description='Emergency shutdown procedures' + ) + parser.add_argument('--disable-all-access-keys', action='store_true', + help='Disable all user access keys') + parser.add_argument('--enable-all-access-keys', action='store_true', + help='Re-enable all user access keys') + parser.add_argument('--lock-user', + help='Prevent the specified user from taking any actions via inline policy') + parser.add_argument('--unlock-user', + help='Remove a previously placed user lock') + parser.add_argument('--lock-role', + help='Prevent the specified role from taking any actions via inline policy') + parser.add_argument('--unlock-role', + help='Remove a previously placed role lock') + parser.add_argument('--lock-all', action='store_true', + help='Locks all users and roles on the account') + parser.add_argument('--unlock-all', action='store_true', + help='Unlocks all users and roles on the account') + + args = parser.parse_args() + + if args.disable_all_access_keys: + disable_all_access_keys() + elif args.enable_all_access_keys: + enable_all_access_keys() + elif args.lock_user: + print(lock_user(args.lock_user)) + elif args.unlock_user: + print(unlock_user(args.unlock_user)) + elif args.lock_role: + print(lock_role(args.lock_role)) + elif args.unlock_role: + print(unlock_role(args.unlock_role)) + elif args.lock_all: + lock_all() + elif args.unlock_all: + unlock_all() + else: + parser.print_help() + + +if __name__ == '__main__': + main()