Skip to content

Commit 626f5f9

Browse files
author
Chris Farris
committed
Adding First script for KMS Key Rotation
1 parent 43fa16a commit 626f5f9

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

.gitignore

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
2+
# Generic places I've shoved things from a scratch/perspective
3+
Notes.md
4+
5+
6+
# Build-time crud
7+
*.zip
8+
*.json
9+
*.log
10+
*.out
11+
*dist-info
12+
13+
14+
# Created by https://www.gitignore.io/api/osx,python
15+
16+
### OSX ###
17+
*.DS_Store
18+
.AppleDouble
19+
.LSOverride
20+
21+
# Icon must end with two \r
22+
Icon
23+
24+
# Thumbnails
25+
._*
26+
27+
# Files that might appear in the root of a volume
28+
.DocumentRevisions-V100
29+
.fseventsd
30+
.Spotlight-V100
31+
.TemporaryItems
32+
.Trashes
33+
.VolumeIcon.icns
34+
.com.apple.timemachine.donotpresent
35+
36+
# Directories potentially created on remote AFP share
37+
.AppleDB
38+
.AppleDesktop
39+
Network Trash Folder
40+
Temporary Items
41+
.apdisk
42+
43+
### Python ###
44+
# Byte-compiled / optimized / DLL files
45+
__pycache__/
46+
*.py[cod]
47+
*$py.class
48+
49+
# C extensions
50+
*.so
51+
52+
# Distribution / packaging
53+
.Python
54+
env/
55+
build/
56+
develop-eggs/
57+
dist/
58+
downloads/
59+
eggs/
60+
.eggs/
61+
# lib/
62+
lib64/
63+
parts/
64+
sdist/
65+
var/
66+
wheels/
67+
*.egg-info/
68+
.installed.cfg
69+
*.egg
70+
71+
# PyInstaller
72+
# Usually these files are written by a python script from a template
73+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
74+
*.manifest
75+
*.spec
76+
77+
# Installer logs
78+
pip-log.txt
79+
pip-delete-this-directory.txt
80+
81+
# Unit test / coverage reports
82+
htmlcov/
83+
.tox/
84+
.coverage
85+
.coverage.*
86+
.cache
87+
nosetests.xml
88+
coverage.xml
89+
*,cover
90+
.hypothesis/
91+
92+
# Translations
93+
*.mo
94+
*.pot
95+
96+
# Django stuff:
97+
*.log
98+
local_settings.py
99+
100+
# Flask stuff:
101+
instance/
102+
.webassets-cache
103+
104+
# Scrapy stuff:
105+
.scrapy
106+
107+
# Sphinx documentation
108+
docs/_build/
109+
110+
# PyBuilder
111+
target/
112+
113+
# Jupyter Notebook
114+
.ipynb_checkpoints
115+
116+
# pyenv
117+
.python-version
118+
119+
# celery beat schedule file
120+
celerybeat-schedule
121+
122+
# SageMath parsed files
123+
*.sage.py
124+
125+
# dotenv
126+
.env
127+
128+
# virtualenv
129+
.venv
130+
venv/
131+
ENV/
132+
133+
# Spyder project settings
134+
.spyderproject
135+
.spyproject
136+
137+
# Rope project settings
138+
.ropeproject
139+
140+
# mkdocs documentation
141+
/site
142+
143+
144+
# End of https://www.gitignore.io/api/osx,python
145+

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
# aws-fast-fixes
22
Scripts to quickly fix security and compliance issues
3+
4+
5+
## Scripts in this repo
6+
7+
* [kms-key-rotation](kms-key-rotation/README.md)

kms-key-rotation/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# kms-key-rotation
2+
3+
This script will enable annual key rotation on all AWS Customer Managed Keys in your account.
4+
5+
## Why?
6+
7+
AWS Key rotation triggers AWS to create a new backing-key for your CMK. These backing-keys are the actual bits used for the encryption and decryption with KMS CMKs. Old backing-keys are not removed, and no data or envelop keys that were encrypted with the old backing-key are re-encrypted.
8+
9+
This exists to make old-school on-prem crypto-compliance folks happy. However security tools and security policies often ding account owners for not having this set.
10+
11+
## What the script does.
12+
13+
This script will iterate through all your regions and attempt to list all your keys. If you have permission to the key (ie it is not locked down to a specific principal), it will issue the [EnableKeyRotation API](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html) call.
14+
15+
## AWS Docs
16+
17+
* [Rotating Customer Master Keys](https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html)
18+
* [EnableKeyRotation API](https://docs.aws.amazon.com/kms/latest/APIReference/API_EnableKeyRotation.html)
19+
* [boto3 enable_key_rotation()](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.enable_key_rotation)
20+
* [boto3 list_keys()](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.list_keys)
21+
* [boto3 get_key_rotation_status()](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms.html#KMS.Client.get_key_rotation_status)
22+
23+
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
3+
import boto3
4+
from botocore.exceptions import ClientError
5+
import os
6+
import logging
7+
# logger = logging.getLogger()
8+
9+
10+
def main(args, logger):
11+
12+
all_regions = get_regions(args)
13+
14+
for region in all_regions:
15+
kms_client = boto3.client("kms", region_name = region)
16+
keys = get_all_keys(kms_client)
17+
for k in keys:
18+
try:
19+
status_response = kms_client.get_key_rotation_status(KeyId=k)
20+
if 'KeyRotationEnabled' not in status_response:
21+
logger.error(f"Unable to get KeyRotationEnabled for keyid: {k}")
22+
continue
23+
if status_response['KeyRotationEnabled']:
24+
logger.debug(f"KeyId {k} already has rotation enabled")
25+
else:
26+
if args.actually_do_it is True:
27+
logger.info(f"Enabling KMS Key Rotation on KeyId {k}")
28+
enable_key_rotation(kms_client, k)
29+
else:
30+
logger.info(f"You Need To KMS Key Rotation on KeyId {k}")
31+
except ClientError as e:
32+
if e.response['Error']['Code'] == 'AccessDeniedException':
33+
logger.warning(f"Unable to get details of key {k}: AccessDenied")
34+
continue
35+
36+
37+
def enable_key_rotation(kms_client, KeyId):
38+
'''Actually perform the enabling of Key rotation and checking of the status code'''
39+
response = kms_client.enable_key_rotation(KeyId=KeyId)
40+
if response['ResponseMetadata']['HTTPStatusCode'] == 200:
41+
return(True)
42+
else:
43+
logger.error(f"Attempt to enable key rotation for {KeyId} returned {response}")
44+
return(False)
45+
46+
47+
def get_all_keys(kms_client):
48+
'''Return an array of all KMS keys for this region'''
49+
keys = []
50+
response = kms_client.list_keys()
51+
while response['Truncated']:
52+
keys += response['Keys']
53+
response = kms_client.list_keys(Marker=response['NextMarker'])
54+
keys += response['Keys']
55+
56+
key_ids = []
57+
for k in keys:
58+
key_ids.append(k['KeyId'])
59+
return(key_ids)
60+
61+
62+
def get_regions(args):
63+
'''Return a list of regions with us-east-1 first. If --region was specified, return a list wth just that'''
64+
65+
# If we specifed a region on the CLI, return a list of just that
66+
if hasattr(args, 'region'):
67+
return([args.region])
68+
69+
# otherwise return all the regions, us-east-1 first
70+
ec2 = boto3.client('ec2')
71+
response = ec2.describe_regions()
72+
output = ['us-east-1']
73+
for r in response['Regions']:
74+
# return us-east-1 first, but dont return it twice
75+
if r['RegionName'] == "us-east-1":
76+
continue
77+
output.append(r['RegionName'])
78+
return(output)
79+
80+
81+
82+
def do_args():
83+
import argparse
84+
parser = argparse.ArgumentParser()
85+
parser.add_argument("--debug", help="print debugging info", action='store_true')
86+
parser.add_argument("--error", help="print error info only", action='store_true')
87+
parser.add_argument("--timestamp", help="Output log with timestamp and toolname", action='store_true')
88+
parser.add_argument("--region", help="Only Process Specified Region")
89+
parser.add_argument("--actually-do-it", help="Actually Perform the action", action='store_true')
90+
91+
args = parser.parse_args()
92+
93+
return(args)
94+
95+
if __name__ == '__main__':
96+
97+
args = do_args()
98+
99+
# Logging idea stolen from: https://docs.python.org/3/howto/logging.html#configuring-logging
100+
# create console handler and set level to debug
101+
logger = logging.getLogger('kms-key-rotation')
102+
ch = logging.StreamHandler()
103+
if args.debug:
104+
logger.setLevel(logging.DEBUG)
105+
elif args.error:
106+
logger.setLevel(logging.ERROR)
107+
else:
108+
logger.setLevel(logging.INFO)
109+
110+
# Silence Boto3 & Friends
111+
logging.getLogger('botocore').setLevel(logging.WARNING)
112+
logging.getLogger('boto3').setLevel(logging.WARNING)
113+
logging.getLogger('urllib3').setLevel(logging.WARNING)
114+
115+
# create formatter
116+
if args.timestamp:
117+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
118+
else:
119+
formatter = logging.Formatter('%(levelname)s - %(message)s')
120+
# add formatter to ch
121+
ch.setFormatter(formatter)
122+
# add ch to logger
123+
logger.addHandler(ch)
124+
125+
try:
126+
main(args, logger)
127+
except KeyboardInterrupt:
128+
exit(1)

0 commit comments

Comments
 (0)