-
Notifications
You must be signed in to change notification settings - Fork 2
/
keypair.py
executable file
·213 lines (174 loc) · 8.02 KB
/
keypair.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env python3
import os
import subprocess
import boto3
from botocore.exceptions import ClientError
from colorama import init, Fore
def rotate_keypair():
"""Automates AWS EC2 key pair rotation.
Generates EC2 key pair, downloads private key and adds passphrase, adds
passphrase to ssh-agent and OS keychain, regenerates public key and pushes
to EC2 instance, creates/updates SSH alias, tests remote access with new
key pair, removes old EC2 key pair and private key from local disk.
"""
# initial EC2 defaults (update these with EC2 instance values)
ec2_ip = '54.193.18.195' # public IP address or public DNS
ec2_key = 'dev_key.pem' # current private key
ec2_user = 'ec2-user' # default AMI user
ec2_port = '22' # SSH port number
# key pair rotation defaults
key_path = '~/.ssh/'
key_path = os.path.expanduser(key_path)
key_name = 'dev_key' # base name of private key
prv_key = key_name + '.pem'
pub_key = key_name + '.pub'
ssh_host = 'dev' # name of SSH alias
ssh_host_tmp = ssh_host +'_tmp' # name of tmp SSH alias
ssh_cfg = key_path + 'config.d/' + ssh_host
last_host_key = '54.193.18.195' # don't edit, it's autopopulated!
# initial ssh connection string
ec2_ssh_str = key_path + ec2_key + ' ' + ec2_user + '@' + ec2_ip
# colorama: automatically reset style after each call
init(autoreset=True)
ck = Fore.CYAN + ' \N{heavy check mark}'
` ec2 = boto3.client('ec2')
try:
print('\n=> Checking for existing EC2 key pair: ' + key_name + ck)
ec2.describe_key_pairs(KeyNames=[key_name])
except ClientError as err:
if err.response["Error"]["Code"] == "InvalidKeyPair.NotFound":
gen_msg = ' Not Found! Generating a new key pair: '
else:
gen_msg = ' Found! Generating a replacement key pair: '
ec2.delete_key_pair(KeyName=key_name)
`
print('\n' + Fore.CYAN + gen_msg + key_name)
keypair = ec2.create_key_pair(KeyName=key_name)
print('\n=> Checking for existing private key: ' + key_path + prv_key + ck)
if os.path.isfile(key_path + prv_key):
down_msg = '\n Found! Downloading replacement: '
prv_key = 'tmp.' + prv_key
else:
down_msg = '\n Not Found! Downloading new: '
print(Fore.CYAN + down_msg + key_path + prv_key)
# check for/make default SSH directory with octal mode permission syntax
if not os.path.isdir(key_path):
os.mkdir(key_path, 0o700)
# check for/remove tmp private key from failed rotation
if os.path.isfile(key_path + prv_key):
os.remove(key_path + prv_key)
with open(
key_path + prv_key,'w+') as key:
key.write(keypair['KeyMaterial'])
# temporarily tighten file perms; needed for encryption and public key gen
os.chmod(key_path + prv_key, 0o600)
print('\n=> Generating public key' + ck)
subprocess.run('ssh-keygen -y -f ' + key_path + prv_key + ' > ' \
+ key_path + pub_key, shell=True)
print('=> Adding host key to ' + key_path + 'known_hosts' + ck)
if last_host_key in open(key_path + 'known_hosts').read():
print('=> Also found prior host key... removing' + ck)
subprocess.run('ssh-keygen -R ' + last_host_key + \
'&>/dev/null', shell=True)
subprocess.run('ssh -o StrictHostKeyChecking=no -tt -i ' + ec2_ssh_str + \
' exit &>/dev/null', shell=True)
print('=> Pushing public key to EC2 instance\'s authorized_keys' + ck)
subprocess.run('cat ' + key_path + pub_key + ' | ssh -i ' + ec2_ssh_str + \
' "sudo tee -a ~/.ssh/authorized_keys > /dev/null"', shell=True)
print('=> Removing public key from local disk' + ck)
os.remove(key_path + pub_key)
print('=> Encrypting private key with passphrase' + ck + '\n')
subprocess.run('ssh-keygen -p -f ' + key_path + prv_key + \
' &>/dev/null', shell=True)
print('\n=> Busting ssh-agent cache' + ck)
subprocess.run('/usr/bin/ssh-add -DK &>/dev/null', shell=True)
print('=> Adding new passphrase to ssh-agent & keychain' + ck + '\n')
subprocess.run('/usr/bin/ssh-add -K ' + key_path + prv_key, shell=True)
print('\n=> Setting file permissions on private key to 400' + ck)
os.chmod(key_path + prv_key, 0o400)
print('=> Checking for existing SSH alias: ' + ssh_cfg + ck)
if os.path.isfile(ssh_cfg):
create_msg = '\n Found! Creating replacement: '
ssh_cfg = ssh_cfg + '_tmp'
else:
create_msg = '\n Not Found! Creating new: '
# create custom SSH config directory if not already exist
if not os.path.isdir(key_path + 'config.d/'):
os.mkdir(key_path + 'config.d/', 0o700)
# Prepend an include directive to default SSH config
with open(key_path + 'config', 'r+') as config:
first_line = config.readline()
if first_line != 'Include config.d/*\n':
lines = config.readlines()
config.seek(0)
config.write('Include config.d/*\n')
config.write(first_line)
config.writelines(lines)
print(Fore.CYAN + create_msg + ssh_cfg)
with open(ssh_cfg, "w") as config:
txt_lines = [
'Host '+ ssh_host + '_tmp',
'\n HostName ' + ec2_ip,
'\n User ' + ec2_user,
'\n Port ' + ec2_port,
'\n IdentityFile ' + key_path + prv_key,
]
config.writelines(txt_lines)
print('\n=> Setting file permissions on SSH alias to 600' + ck)
os.chmod(ssh_cfg, 0o600)
print('=> Testing remote access to EC2 instance using new key pair' + ck)
try:
subprocess.run(['ssh', ssh_host_tmp, 'exit'],
check=True,
universal_newlines=True,
stderr=subprocess.PIPE
)
except subprocess.CalledProcessError as err:
print(Fore.RED + '\n' + err.stderr)
else:
print(Fore.CYAN + '\n Successfully connected to EC2 instance with '
'new key pair!\n')
print('=> Clearing stale keys in EC2 instance\'s authorized_keys' + ck)
# get line count, subtract 1, then delete remaining line range
cmd = 'file="$HOME/.ssh/authorized_keys" \
&& num=$(wc -l < $file) \
&& num=$((num-1)) \
&& [[ $num -gt 1 ]] \
&& sed -i "1,$num d" $file'
subprocess.run(['ssh', ssh_host, cmd])
prv_key = key_name + '.pem'
if os.path.isfile(key_path + 'config.d/' + ssh_host + '_tmp'):
print('=> Removing prior SSH alias config'+ ck)
os.remove(key_path + 'config.d/' + ssh_host)
ssh_cfg = key_path + 'config.d/' + ssh_host
print('=> Renaming new SSH alias config to ' + ssh_cfg + ck)
os.rename(ssh_cfg + '_tmp', ssh_cfg)
print('=> Updating Host & IdentityFile values in ' + ssh_cfg + ck)
with open(ssh_cfg) as config:
sub = (config.read()
.replace('tmp.' + prv_key, prv_key)
.replace(ssh_host + '_tmp', ssh_host)
)
with open(ssh_cfg, "w") as config:
config.write(sub)
if ec2_key != prv_key:
print('=> Removing initial private key (local & EC2)' + ck)
os.remove(key_path + ec2_key)
ec2_keypair = os.path.splitext(ec2_key)[0] # strip file extention
ec2.delete_key_pair(KeyName=ec2_keypair)
if os.path.isfile(key_path + 'tmp.' + prv_key):
print('=> Removing old private key '+ prv_key + ck)
os.remove(key_path + prv_key)
print('=> Renaming new private key to ' + key_path + prv_key + ck)
os.rename(key_path + 'tmp.' + prv_key, key_path + prv_key)
print('=> Updating script\'s default values' + ck)
with open('keypair.py') as file:
sub = (file.read()
.replace(ec2_key, prv_key)
.replace(last_host_key, ec2_ip))
with open('keypair.py', "w") as file:
file.write(sub)
print(Fore.CYAN + '\n**** EC2 key pair rotation complete... '
'try it: $ ssh dev ****')
if __name__ == '__main__':
rotate_keypair()