1 #!/usr/bin/env bash
2 #
3 # Keymanage provides functionality to push, remove, and swap/refresh ssh keys
4 # on a list of systems for the specified users
5 #
6 # Copyright (C) 2014 Aaron Ball <nullspoon@iohq.net>
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #
21 manifest=''
22 key=''
23 action=''
24 id=''
25 user=''
26
27
28 #
29 # Backups by a specific ssh key to <date_modified>.<key_name>
30 #
31 # @param ssh_base string Path to where the ssh keys and configs are stored
32 # @param key string Name of the key to backup
33 #
34 # @return string The filename of the key backup
35 #
36 function backup_keys {
37 local ssh_base=$1
38 local key=$2
39 moved=0;
40 date=""
41 priv_ls=$(ls -l --time-style='+%Y%m%d%H%M%S' ${ssh_base}/${key})
42 date=$(echo "${priv_ls}" | tr -s ' ' | cut -d ' ' -f 6);
43 # Rename the old key
44 if [[ -e "${ssh_base}/${key}" ]]; then
45 mv ${ssh_base}/${key} ${ssh_base}/${date}.${key}
46 moved=1;
47 fi
48 # Rename the old public key
49 if [[ -e "${ssh_base}/${key}.pub" ]]; then
50 mv ${ssh_base}/${key}.pub ${ssh_base}/${date}.${key}.pub
51 moved=1;
52 fi
53 if [[ ${moved} == 0 ]]; then
54 echo ''
55 else
56 chmod 700 ${ssh_base}/*
57 echo "${ssh_base}/${date}.${key}"
58 fi
59 }
60
61
62 #
63 # Pushes specific public key to remote user's authorized_keys
64 #
65 # @param user string User owning the authorized_keys file to be modified
66 # @param server string Server the user's authorized_keys file is on
67 # @param old_key string The key to use for authentication
68 # @param new_key string The key, public or private, to be pushed
69 #
70 function push_key {
71 local conn=$1
72 local old_key=$2
73 local new_key=$3
74 if [[ ${#new_key} -lt '4' ]]; then
75 echo "Key to be pushed is not a public key."
76 exit
77 fi
78
79 ispub=$(key_is_public ${new_key})
80 if [[ ${ispub} == 0 ]]; then
81 # Append .pub because a public key wasn't specified
82 new_key="${new_key}.pub"
83 fi
84
85 local cmd="if [[ ! -d ~/.ssh/ ]]; then mkdir ~/.ssh/; fi"
86 cmd="${cmd} && echo '$(cat ${new_key})' >> ~/.ssh/authorized_keys"
87
88 # Unset our identity file if it doesn't exist
89 local id_file="-i ${old_key}"
90 if [[ ${old_key} == '' ]]; then
91 id_file=''
92 fi
93 contents=$(cat ${new_key})
94 ssh -q ${id_file} ${conn} "${cmd}"
95 }
96
97
98 #
99 # Removes the specified public key from a remote user's authorized_keys file
100 #
101 # @param user string User owning the authorized_keys file to be modified
102 # @param server string Server the user's authorized_keys file is on
103 # @param key string The key to use for authentication which is to be removed
104 #
105 function remove_remote_key {
106 local conn=$1
107 local key=$2
108 pub_key=''
109 priv_key=''
110 ispub=$(key_is_public ${key})
111 if [[ ${ispub} == 0 ]]; then
112 priv_key="${key}"
113 pub_key="${key}.pub"
114 else
115 priv_key="${key:0:-4}"
116 pub_key="${key}"
117 fi
118 contents=$(cat "${pub_key}")
119 local cmd="if [[ ! -d ~/.ssh/ ]]; then mkdir ~/.ssh/; fi"
120 cmd="${cmd} && cat ~/.ssh/authorized_keys | grep -v '${contents}' "
121 cmd="${cmd} > ~/.ssh/auth_keys"
122 cmd="${cmd} && mv ~/.ssh/auth_keys ~/.ssh/authorized_keys"
123 if [[ ${id} != '' ]]; then
124 ssh -q -i ${id} ${conn} "${cmd}"
125 else
126 ssh -q ${conn} "${cmd}"
127 fi
128 }
129
130
131 #
132 # Determines if the specified key is public (or not which would be private).
133 #
134 # @param key string Path to the key to check
135 #
136 # @return int Whether or not the key is public
137 #
138 function key_is_public {
139 key=$1
140 if [[ ${#key} -lt '4' ]]; then
141 echo 0;
142 fi
143 # Check the extension
144 ext=${key:$((${#key}-4)):${#key}}
145 if [[ ${ext} == '.pub' ]]; then
146 echo 1;
147 fi
148 echo 0
149 }
150
151
152 #
153 # Generates a new ssh key of the length 4096
154 #
155 # @param filepath string Path to where the new ssh key will be written
156 # @param bits int Number of bits in the new key (eg: 2048, 4096, 8192, etc.)
157 #
158 function gen_key {
159 local filepath=$1
160 local bits=$2
161 ssh-keygen -b ${bits} -f "${filepath}" -N ''
162 }
163
164
165 #
166 # Prints the help text
167 #
168 function get_help {
169 echo "
170 Manages ssh keys en masse. Designed to perform pushes, removals, and swaps
171 of ssh keys on a llist of servers for the current or specified user.
172
173 [1mUsage:[0m keymanage.sh action --manifest systems.list
174
175 [1mActions:[0m push, update, remove
176
177 [1mArguments:[0m
178 -m,--manifest Text file containing a list of systems, delimited by new
179 lines.
180 -k,--key Path to a key to perform an action (push or remove) with.
181 -i,--id Key to use for automated logins. Not used when performing an
182 update.
183 -u,--user Username on remote systems to work on (defaults to root).
184
185 "
186 }
187
188
189 function parse_args {
190 argv=(${@})
191 # Parse the arguments
192 for(( i=0; i<${#argv[*]}; i++ )); do
193 if [[ ${argv[$i]} == '-h' || ${argv[$i]} == '--help' ]]; then
194 echo "$(get_help)"
195 exit
196 elif [[ ${argv[$i]} == '-m' || ${argv[$i]} == '--manifest' ]]; then
197 manifest=${argv[$i+1]}
198 i=$(( ${i} + 1 ))
199 elif [[ ${argv[$i]} == '-k' || ${argv[$i]} == '--key' ]]; then
200 key=${argv[$i+1]}
201 i=$(( ${i} + 1 ))
202 elif [[ ${argv[$i]} == '-i' || ${argv[$i]} == '--id' ]]; then
203 id=${argv[$i+1]}
204 i=$(( ${i} + 1 ))
205 elif [[ ${argv[$i]} == '-u' || ${argv[$i]} == '--user' ]]; then
206 user=${argv[$i+1]}
207 i=$(( ${i} + 1 ))
208 else
209 action=${argv[$i]}
210 fi
211 done
212
213 # Enforce some business rules
214 echo
215 exit=0;
216 if [[ ${action} == '' ]]; then
217 echo "Please specify an action.";
218 echo " Available actions: push, remove, update."
219 echo
220 exit=1;
221 fi
222 if [[ ${manifest} == '' ]]; then
223 echo "Please specify a manifest file."
224 echo " Example: keymanage.sh action [-m|--manifest] ./systems.txt"
225 echo
226 exit=1;
227 fi
228 if [[ ${exit} == 1 ]]; then
229 exit
230 fi
231 }
232
233
234 #
235 # Determines the path to the parent directory containing a file.
236 #
237 # @param filepath string Path to the file to get the parent directory for
238 #
239 # @return string Path to the file's parent directory
240 #
241 function get_file_path {
242 filepath=$1
243 filename=$(basename ${filepath})
244 echo ${filepath} | sed "s/\(.*\)${filename}/\1/"
245 }
246
247
248 #
249 # Push main function. One param because the rest are global
250 #
251 function key_push {
252 argv=( ${@} )
253 if [[ ${id} == '' ]]; then
254 echo "No identity file specified (-i). This will likely be painful."
255 fi
256 for (( i=0; i<${#argv[*]}; i++ )); do
257 dest=${argv[$i]}
258 if [[ ${id} == '' ]]; then
259 push_key "${dest}" '' ${key}
260 else
261 push_key "${dest}" ${id} ${key}
262 fi
263 echo "Key ${key} added for ${dest}."
264 done
265 }
266
267
268 #
269 # Update main function. One param because the rest are global
270 #
271 function key_update {
272 argv=( ${@} )
273 ssh_base=$(get_file_path ${key})
274 filename=$(basename ${key})
275 # Backup our old key
276 backup_key="$(backup_keys ${ssh_base} ${filename})"
277
278 # Let's get to work on that new key
279 gen_key "${key}" 4096
280
281 for (( i=0; i<${#argv[*]}; i++ )); do
282 dest=${argv[$i]}
283 if [[ ${backup_key} == '' ]]; then
284 echo "No current key exists."
285 echo "Skipping backup and removal from remote."
286 # Push the new key
287 push_key "${dest}" '' ${key}
288 else
289 # Push the new key
290 push_key "${dest}" ${backup_key} ${key}
291 # Clean up the old key from our remote
292 remove_remote_key "${dest}" "${backup_key}"
293 fi
294 echo "Key ${key} updated for ${dest}."
295 done
296 }
297
298
299 #
300 # Remove main function. One param because the rest are global
301 #
302 function key_remove {
303 argv=( ${@} )
304 for (( i=0; i<${#argv[*]}; i++ )); do
305 dest=${argv[$i]}
306 remove_remote_key "${dest}" "${key}"
307 echo "Key ${key} removed from ${dest}."
308 done
309 }
310
311
312 #
313 # The main function
314 #
315 function main {
316 # Parse our script args
317 # Believe me, this is a lot better than the alternatives
318 parse_args ${@}
319
320 destinations=( $(cat ${manifest}) )
321 # Key required
322 if [[ ${key} == '' ]]; then
323 echo -n "Please specify a key (-k) to ${action}."
324 echo
325 exit
326 fi
327
328 # Let's start doing stuff
329 if [[ ${action} == 'push' ]]; then
330 key_push ${destinations[*]}
331 elif [[ ${action} == 'update' ]]; then
332 key_update ${destinations[*]}
333 elif [[ ${action} == 'remove' ]]; then
334 key_remove ${destinations[*]}
335 fi
336 }
337
338 main ${@}
|