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
1=== added file 'cloudinit/config/cc_ssh_authkey_fingerprints.py'
2--- cloudinit/config/cc_ssh_authkey_fingerprints.py 1970-01-01 00:00:00 +0000
3+++ cloudinit/config/cc_ssh_authkey_fingerprints.py 2012-08-20 19:10:39 +0000
4@@ -0,0 +1,91 @@
5+# vi: ts=4 expandtab
6+#
7+# Copyright (C) 2012 Yahoo! Inc.
8+#
9+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
10+#
11+# This program is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License version 3, as
13+# published by the Free Software Foundation.
14+#
15+# This program is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# You should have received a copy of the GNU General Public License
21+# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
23+import base64
24+import glob
25+import hashlib
26+import os
27+
28+from prettytable import PrettyTable
29+
30+from cloudinit import util
31+from cloudinit import ssh_util
32+
33+
34+def _split_hash(bin_hash):
35+ split_up = []
36+ for i in xrange(0, len(bin_hash), 2):
37+ split_up.append(bin_hash[i:i+2])
38+ return split_up
39+
40+
41+def _gen_fingerprint(b64_text, hash_meth='md5'):
42+ if not b64_text:
43+ return ''
44+ # TBD(harlowja): Maybe we should feed this into 'ssh -lf'?
45+ try:
46+ hasher = hashlib.new(hash_meth)
47+ hasher.update(base64.b64decode(b64_text))
48+ return ":".join(_split_hash(hasher.hexdigest()))
49+ except TypeError:
50+ # Raised when b64 not really b64...
51+ return '?'
52+
53+
54+def _is_printable_key(entry):
55+ if any([entry.keytype, entry.base64, entry.comment, entry.options]):
56+ if entry.keytype and entry.keytype.lower().strip() in ['ssh-dss', 'ssh-rsa']:
57+ return True
58+ return False
59+
60+
61+def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5', prefix='ci-info: '):
62+ if not key_entries:
63+ message = "%sno authorized ssh keys fingerprints found for user %s." % (prefix, user)
64+ util.multi_log(message)
65+ return
66+ tbl_fields = ['Keytype', 'Fingerprint (%s)' % (hash_meth), 'Options', 'Comment']
67+ tbl = PrettyTable(tbl_fields)
68+ for entry in key_entries:
69+ if _is_printable_key(entry):
70+ row = []
71+ row.append(entry.keytype or '-')
72+ row.append(_gen_fingerprint(entry.base64, hash_meth) or '-')
73+ row.append(entry.options or '-')
74+ row.append(entry.comment or '-')
75+ tbl.add_row(row)
76+ authtbl_s = tbl.get_string()
77+ authtbl_lines = authtbl_s.splitlines()
78+ max_len = len(max(authtbl_lines, key=len))
79+ lines = [
80+ util.center("Authorized keys fingerprints from %s for user %s" % (key_fn, user), "+", max_len),
81+ ]
82+ lines.extend(authtbl_lines)
83+ for line in lines:
84+ util.multi_log(text="%s%s\n" % (prefix, line))
85+
86+
87+def handle(name, cfg, cloud, log, _args):
88+ if 'no_ssh_fingerprints' in cfg:
89+ log.debug(("Skipping module named %s, "
90+ "logging of ssh fingerprints disabled"), name)
91+
92+ user_name = util.get_cfg_option_str(cfg, "user", "ubuntu")
93+ hash_meth = util.get_cfg_option_str(cfg, "authkey_hash", "md5")
94+ (auth_key_fn, auth_key_entries) = ssh_util.extract_authorized_keys(user_name, cloud.paths)
95+ _pprint_key_entries(user_name, auth_key_fn, auth_key_entries, hash_meth)
96
97=== modified file 'cloudinit/ssh_util.py'
98--- cloudinit/ssh_util.py 2012-06-29 20:46:19 +0000
99+++ cloudinit/ssh_util.py 2012-08-20 19:10:39 +0000
100@@ -181,12 +181,11 @@
101 return contents
102
103
104-def update_authorized_keys(fname, keys):
105- entries = parse_authorized_keys(fname)
106+def update_authorized_keys(old_entries, keys):
107 to_add = list(keys)
108
109- for i in range(0, len(entries)):
110- ent = entries[i]
111+ for i in range(0, len(old_entries)):
112+ ent = old_entries[i]
113 if ent.empty() or not ent.base64:
114 continue
115 # Replace those with the same base64
116@@ -199,66 +198,81 @@
117 # Don't add it later
118 if k in to_add:
119 to_add.remove(k)
120- entries[i] = ent
121+ old_entries[i] = ent
122
123 # Now append any entries we did not match above
124 for key in to_add:
125- entries.append(key)
126+ old_entries.append(key)
127
128 # Now format them back to strings...
129- lines = [str(b) for b in entries]
130+ lines = [str(b) for b in old_entries]
131
132 # Ensure it ends with a newline
133 lines.append('')
134 return '\n'.join(lines)
135
136
137-def setup_user_keys(keys, user, key_prefix, paths):
138- # Make sure the users .ssh dir is setup accordingly
139- pwent = pwd.getpwnam(user)
140- ssh_dir = os.path.join(pwent.pw_dir, '.ssh')
141- ssh_dir = paths.join(False, ssh_dir)
142- if not os.path.exists(ssh_dir):
143- util.ensure_dir(ssh_dir, mode=0700)
144- util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
145-
146- # Turn the keys given into actual entries
147- parser = AuthKeyLineParser()
148- key_entries = []
149- for k in keys:
150- key_entries.append(parser.parse(str(k), def_opt=key_prefix))
151-
152+def users_ssh_info(username, paths):
153+ pw_ent = pwd.getpwnam(username)
154+ if not pw_ent:
155+ raise RuntimeError("Unable to get ssh info for user %r" % (username))
156+ ssh_dir = paths.join(False, os.path.join(pw_ent.pw_dir, '.ssh'))
157+ return (ssh_dir, pw_ent)
158+
159+
160+def extract_authorized_keys(username, paths):
161+ (ssh_dir, pw_ent) = users_ssh_info(username, paths)
162 sshd_conf_fn = paths.join(True, DEF_SSHD_CFG)
163+ auth_key_fn = None
164 with util.SeLinuxGuard(ssh_dir, recursive=True):
165 try:
166- # AuthorizedKeysFile may contain tokens
167+ # The 'AuthorizedKeysFile' may contain tokens
168 # of the form %T which are substituted during connection set-up.
169 # The following tokens are defined: %% is replaced by a literal
170 # '%', %h is replaced by the home directory of the user being
171 # authenticated and %u is replaced by the username of that user.
172 ssh_cfg = parse_ssh_config_map(sshd_conf_fn)
173- akeys = ssh_cfg.get("authorizedkeysfile", '')
174- akeys = akeys.strip()
175- if not akeys:
176- akeys = "%h/.ssh/authorized_keys"
177- akeys = akeys.replace("%h", pwent.pw_dir)
178- akeys = akeys.replace("%u", user)
179- akeys = akeys.replace("%%", '%')
180- if not akeys.startswith('/'):
181- akeys = os.path.join(pwent.pw_dir, akeys)
182- authorized_keys = paths.join(False, akeys)
183+ auth_key_fn = ssh_cfg.get("authorizedkeysfile", '').strip()
184+ if not auth_key_fn:
185+ auth_key_fn = "%h/.ssh/authorized_keys"
186+ auth_key_fn = auth_key_fn.replace("%h", pw_ent.pw_dir)
187+ auth_key_fn = auth_key_fn.replace("%u", username)
188+ auth_key_fn = auth_key_fn.replace("%%", '%')
189+ if not auth_key_fn.startswith('/'):
190+ auth_key_fn = os.path.join(pw_ent.pw_dir, auth_key_fn)
191+ auth_key_fn = paths.join(False, auth_key_fn)
192 except (IOError, OSError):
193- authorized_keys = os.path.join(ssh_dir, 'authorized_keys')
194+ # Give up and use a default key filename
195+ auth_key_fn = os.path.join(ssh_dir, 'authorized_keys')
196 util.logexc(LOG, ("Failed extracting 'AuthorizedKeysFile'"
197 " in ssh config"
198- " from %s, using 'AuthorizedKeysFile' file"
199- " %s instead"),
200- sshd_conf_fn, authorized_keys)
201-
202- content = update_authorized_keys(authorized_keys, key_entries)
203- util.ensure_dir(os.path.dirname(authorized_keys), mode=0700)
204- util.write_file(authorized_keys, content, mode=0600)
205- util.chownbyid(authorized_keys, pwent.pw_uid, pwent.pw_gid)
206+ " from %r, using 'AuthorizedKeysFile' file"
207+ " %r instead"),
208+ sshd_conf_fn, auth_key_fn)
209+ auth_key_entries = parse_authorized_keys(auth_key_fn)
210+ return (auth_key_fn, auth_key_entries)
211+
212+
213+def setup_user_keys(keys, username, key_prefix, paths):
214+ # Make sure the users .ssh dir is setup accordingly
215+ (ssh_dir, pwent) = users_ssh_info(username, paths)
216+ if not os.path.isdir(ssh_dir):
217+ util.ensure_dir(ssh_dir, mode=0700)
218+ util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
219+
220+ # Turn the 'update' keys given into actual entries
221+ parser = AuthKeyLineParser()
222+ key_entries = []
223+ for k in keys:
224+ key_entries.append(parser.parse(str(k), def_opt=key_prefix))
225+
226+ # Extract the old and make the new
227+ (auth_key_fn, auth_key_entries) = extract_authorized_keys(username, paths)
228+ with util.SeLinuxGuard(ssh_dir, recursive=True):
229+ content = update_authorized_keys(auth_key_entries, key_entries)
230+ util.ensure_dir(os.path.dirname(auth_key_fn), mode=0700)
231+ util.write_file(auth_key_fn, content, mode=0600)
232+ util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)
233
234
235 class SshdConfigLine(object):