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