Merge lp:~harlowja/cloud-init/ssh-auth-keys into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow
Status: Merged
Merged at revision: 625
Proposed branch: lp:~harlowja/cloud-init/ssh-auth-keys
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 235 lines (+147/-42)
2 files modified
cloudinit/config/cc_ssh_authkey_fingerprints.py (+91/-0)
cloudinit/ssh_util.py (+56/-42)
To merge this branch: bzr merge lp:~harlowja/cloud-init/ssh-auth-keys
Reviewer Review Type Date Requested Status
Scott Moser Approve
Review via email: mp+120281@code.launchpad.net

Description of the change

Adds a new module for bug @ https://bugs.launchpad.net/cloud-init/+bug/1010582

The output from this new module being ran is the following (for an example, ignore the no datasource...)

http://pastebin.ubuntu.com/1154749/

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

This looks fine. Ideally there'd be some tests...
The only other comment is that it seems like '_gen_fingerprint' as well be part of ssh_util, and even have 'fingerprint' be returned by AuthKeyLineParser:parse, but nothing else uses that now, so that seems not necessary.

review: Approve
622. By Joshua Harlow

Fixup the columns and add a check to make
sure that a key given is one that we actually
want to print out. Also add in a config option
which lets people select a different hashing
method (not md5 if they want).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'cloudinit/config/cc_ssh_authkey_fingerprints.py'
--- cloudinit/config/cc_ssh_authkey_fingerprints.py 1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_ssh_authkey_fingerprints.py 2012-08-20 19:10:39 +0000
@@ -0,0 +1,91 @@
1# vi: ts=4 expandtab
2#
3# Copyright (C) 2012 Yahoo! Inc.
4#
5# Author: Joshua Harlow <harlowja@yahoo-inc.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3, as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import base64
20import glob
21import hashlib
22import os
23
24from prettytable import PrettyTable
25
26from cloudinit import util
27from cloudinit import ssh_util
28
29
30def _split_hash(bin_hash):
31 split_up = []
32 for i in xrange(0, len(bin_hash), 2):
33 split_up.append(bin_hash[i:i+2])
34 return split_up
35
36
37def _gen_fingerprint(b64_text, hash_meth='md5'):
38 if not b64_text:
39 return ''
40 # TBD(harlowja): Maybe we should feed this into 'ssh -lf'?
41 try:
42 hasher = hashlib.new(hash_meth)
43 hasher.update(base64.b64decode(b64_text))
44 return ":".join(_split_hash(hasher.hexdigest()))
45 except TypeError:
46 # Raised when b64 not really b64...
47 return '?'
48
49
50def _is_printable_key(entry):
51 if any([entry.keytype, entry.base64, entry.comment, entry.options]):
52 if entry.keytype and entry.keytype.lower().strip() in ['ssh-dss', 'ssh-rsa']:
53 return True
54 return False
55
56
57def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5', prefix='ci-info: '):
58 if not key_entries:
59 message = "%sno authorized ssh keys fingerprints found for user %s." % (prefix, user)
60 util.multi_log(message)
61 return
62 tbl_fields = ['Keytype', 'Fingerprint (%s)' % (hash_meth), 'Options', 'Comment']
63 tbl = PrettyTable(tbl_fields)
64 for entry in key_entries:
65 if _is_printable_key(entry):
66 row = []
67 row.append(entry.keytype or '-')
68 row.append(_gen_fingerprint(entry.base64, hash_meth) or '-')
69 row.append(entry.options or '-')
70 row.append(entry.comment or '-')
71 tbl.add_row(row)
72 authtbl_s = tbl.get_string()
73 authtbl_lines = authtbl_s.splitlines()
74 max_len = len(max(authtbl_lines, key=len))
75 lines = [
76 util.center("Authorized keys fingerprints from %s for user %s" % (key_fn, user), "+", max_len),
77 ]
78 lines.extend(authtbl_lines)
79 for line in lines:
80 util.multi_log(text="%s%s\n" % (prefix, line))
81
82
83def handle(name, cfg, cloud, log, _args):
84 if 'no_ssh_fingerprints' in cfg:
85 log.debug(("Skipping module named %s, "
86 "logging of ssh fingerprints disabled"), name)
87
88 user_name = util.get_cfg_option_str(cfg, "user", "ubuntu")
89 hash_meth = util.get_cfg_option_str(cfg, "authkey_hash", "md5")
90 (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(user_name, cloud.paths)
91 _pprint_key_entries(user_name, auth_key_fn, auth_key_entries, hash_meth)
092
=== modified file 'cloudinit/ssh_util.py'
--- cloudinit/ssh_util.py 2012-06-29 20:46:19 +0000
+++ cloudinit/ssh_util.py 2012-08-20 19:10:39 +0000
@@ -181,12 +181,11 @@
181 return contents181 return contents
182182
183183
184def update_authorized_keys(fname, keys):184def update_authorized_keys(old_entries, keys):
185 entries = parse_authorized_keys(fname)
186 to_add = list(keys)185 to_add = list(keys)
187186
188 for i in range(0, len(entries)):187 for i in range(0, len(old_entries)):
189 ent = entries[i]188 ent = old_entries[i]
190 if ent.empty() or not ent.base64:189 if ent.empty() or not ent.base64:
191 continue190 continue
192 # Replace those with the same base64191 # Replace those with the same base64
@@ -199,66 +198,81 @@
199 # Don't add it later198 # Don't add it later
200 if k in to_add:199 if k in to_add:
201 to_add.remove(k)200 to_add.remove(k)
202 entries[i] = ent201 old_entries[i] = ent
203202
204 # Now append any entries we did not match above203 # Now append any entries we did not match above
205 for key in to_add:204 for key in to_add:
206 entries.append(key)205 old_entries.append(key)
207206
208 # Now format them back to strings...207 # Now format them back to strings...
209 lines = [str(b) for b in entries]208 lines = [str(b) for b in old_entries]
210209
211 # Ensure it ends with a newline210 # Ensure it ends with a newline
212 lines.append('')211 lines.append('')
213 return '\n'.join(lines)212 return '\n'.join(lines)
214213
215214
216def setup_user_keys(keys, user, key_prefix, paths):215def users_ssh_info(username, paths):
217 # Make sure the users .ssh dir is setup accordingly216 pw_ent = pwd.getpwnam(username)
218 pwent = pwd.getpwnam(user)217 if not pw_ent:
219 ssh_dir = os.path.join(pwent.pw_dir, '.ssh')218 raise RuntimeError("Unable to get ssh info for user %r" % (username))
220 ssh_dir = paths.join(False, ssh_dir)219 ssh_dir = paths.join(False, os.path.join(pw_ent.pw_dir, '.ssh'))
221 if not os.path.exists(ssh_dir):220 return (ssh_dir, pw_ent)
222 util.ensure_dir(ssh_dir, mode=0700)221
223 util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)222
224223def extract_authorized_keys(username, paths):
225 # Turn the keys given into actual entries224 (ssh_dir, pw_ent) = users_ssh_info(username, paths)
226 parser = AuthKeyLineParser()
227 key_entries = []
228 for k in keys:
229 key_entries.append(parser.parse(str(k), def_opt=key_prefix))
230
231 sshd_conf_fn = paths.join(True, DEF_SSHD_CFG)225 sshd_conf_fn = paths.join(True, DEF_SSHD_CFG)
226 auth_key_fn = None
232 with util.SeLinuxGuard(ssh_dir, recursive=True):227 with util.SeLinuxGuard(ssh_dir, recursive=True):
233 try:228 try:
234 # AuthorizedKeysFile may contain tokens229 # The 'AuthorizedKeysFile' may contain tokens
235 # of the form %T which are substituted during connection set-up.230 # of the form %T which are substituted during connection set-up.
236 # The following tokens are defined: %% is replaced by a literal231 # The following tokens are defined: %% is replaced by a literal
237 # '%', %h is replaced by the home directory of the user being232 # '%', %h is replaced by the home directory of the user being
238 # authenticated and %u is replaced by the username of that user.233 # authenticated and %u is replaced by the username of that user.
239 ssh_cfg = parse_ssh_config_map(sshd_conf_fn)234 ssh_cfg = parse_ssh_config_map(sshd_conf_fn)
240 akeys = ssh_cfg.get("authorizedkeysfile", '')235 auth_key_fn = ssh_cfg.get("authorizedkeysfile", '').strip()
241 akeys = akeys.strip()236 if not auth_key_fn:
242 if not akeys:237 auth_key_fn = "%h/.ssh/authorized_keys"
243 akeys = "%h/.ssh/authorized_keys"238 auth_key_fn = auth_key_fn.replace("%h", pw_ent.pw_dir)
244 akeys = akeys.replace("%h", pwent.pw_dir)239 auth_key_fn = auth_key_fn.replace("%u", username)
245 akeys = akeys.replace("%u", user)240 auth_key_fn = auth_key_fn.replace("%%", '%')
246 akeys = akeys.replace("%%", '%')241 if not auth_key_fn.startswith('/'):
247 if not akeys.startswith('/'):242 auth_key_fn = os.path.join(pw_ent.pw_dir, auth_key_fn)
248 akeys = os.path.join(pwent.pw_dir, akeys)243 auth_key_fn = paths.join(False, auth_key_fn)
249 authorized_keys = paths.join(False, akeys)
250 except (IOError, OSError):244 except (IOError, OSError):
251 authorized_keys = os.path.join(ssh_dir, 'authorized_keys')245 # Give up and use a default key filename
246 auth_key_fn = os.path.join(ssh_dir, 'authorized_keys')
252 util.logexc(LOG, ("Failed extracting 'AuthorizedKeysFile'"247 util.logexc(LOG, ("Failed extracting 'AuthorizedKeysFile'"
253 " in ssh config"248 " in ssh config"
254 " from %s, using 'AuthorizedKeysFile' file"249 " from %r, using 'AuthorizedKeysFile' file"
255 " %s instead"),250 " %r instead"),
256 sshd_conf_fn, authorized_keys)251 sshd_conf_fn, auth_key_fn)
257252 auth_key_entries = parse_authorized_keys(auth_key_fn)
258 content = update_authorized_keys(authorized_keys, key_entries)253 return (auth_key_fn, auth_key_entries)
259 util.ensure_dir(os.path.dirname(authorized_keys), mode=0700)254
260 util.write_file(authorized_keys, content, mode=0600)255
261 util.chownbyid(authorized_keys, pwent.pw_uid, pwent.pw_gid)256def setup_user_keys(keys, username, key_prefix, paths):
257 # Make sure the users .ssh dir is setup accordingly
258 (ssh_dir, pwent) = users_ssh_info(username, paths)
259 if not os.path.isdir(ssh_dir):
260 util.ensure_dir(ssh_dir, mode=0700)
261 util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
262
263 # Turn the 'update' keys given into actual entries
264 parser = AuthKeyLineParser()
265 key_entries = []
266 for k in keys:
267 key_entries.append(parser.parse(str(k), def_opt=key_prefix))
268
269 # Extract the old and make the new
270 (auth_key_fn, auth_key_entries) = extract_authorized_keys(username, paths)
271 with util.SeLinuxGuard(ssh_dir, recursive=True):
272 content = update_authorized_keys(auth_key_entries, key_entries)
273 util.ensure_dir(os.path.dirname(auth_key_fn), mode=0700)
274 util.write_file(auth_key_fn, content, mode=0600)
275 util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)
262276
263277
264class SshdConfigLine(object):278class SshdConfigLine(object):