Merge lp:~racb/cloud-utils/cloud-fingerprint into lp:cloud-utils

Proposed by Robie Basak
Status: Work in progress
Proposed branch: lp:~racb/cloud-utils/cloud-fingerprint
Merge into: lp:cloud-utils
Diff against target: 326 lines (+298/-2)
3 files modified
bin/cloud-fingerprint (+164/-0)
debian/control (+3/-2)
man/cloud-fingerprint.1 (+131/-0)
To merge this branch: bzr merge lp:~racb/cloud-utils/cloud-fingerprint
Reviewer Review Type Date Requested Status
Registry Administrators Pending
Review via email: mp+140228@code.launchpad.net

Commit message

Add a new cloud-fingerprint tool

This automatically and securely updates ~/.ssh/known_hosts using fingerprint information printed to the console by cloud-init.

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) wrote :

I will change some of the commands so that I can in the future support grabbing the public key directly without an ssh-keyscan and verification without breaking the CLI or making it ugly.

Revision history for this message
Robie Basak (racb) wrote :

Public key interface discussion here https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/893400 - thanks Scott!

Unmerged revisions

207. By Robie Basak

Add cloud-fingerprint

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/cloud-fingerprint'
2--- bin/cloud-fingerprint 1970-01-01 00:00:00 +0000
3+++ bin/cloud-fingerprint 2012-12-17 15:32:32 +0000
4@@ -0,0 +1,164 @@
5+#!/usr/bin/python3
6+
7+# Handle ~/.ssh/known_hosts for dynamic cloud instances
8+#
9+# Copyright (C) 2012 Canonical Ltd.
10+#
11+# Authors: Robie Basak <robie.basak@canonical.com>
12+#
13+# This program is free software: you can redistribute it and/or modify
14+# it under the terms of the GNU General Public License as published by
15+# the Free Software Foundation, version 3 of the License.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU General Public License for more details.
21+#
22+# You should have received a copy of the GNU General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
25+import argparse
26+import os
27+import re
28+import subprocess
29+import sys
30+import tempfile
31+
32+HOME = os.getenv('HOME')
33+KNOWN_HOSTS_PATH = os.path.join(HOME, '.ssh', 'known_hosts')
34+
35+EC2_START_MARKER = 'ec2: -----BEGIN SSH HOST KEY FINGERPRINTS-----'
36+EC2_END_MARKER = 'ec2: -----END SSH HOST KEY FINGERPRINTS-----'
37+EC2_PREFIX_TO_DROP = 'ec2: '
38+FINGERPRINT_CHECK = re.compile(r'^[a-f0-9]{2}(:[a-f0-9]{2}){15}$')
39+
40+
41+def drop_prefix(prefix, line):
42+ assert line.startswith(prefix)
43+ return line[len(prefix):]
44+
45+
46+def extract_console_fingerprint(console_output):
47+ start = console_output.index(EC2_START_MARKER) + len(EC2_START_MARKER)
48+ end = console_output.index(EC2_END_MARKER, start)
49+
50+ data = console_output[start:end].lstrip().rstrip().splitlines()
51+ return [drop_prefix(EC2_PREFIX_TO_DROP, line) for line in data]
52+
53+
54+def fingerprint_appears_in_lines(fingerprint, lines):
55+ line_fingerprint_candidates = [
56+ x.split()[1] for x in lines if (len(x) > 0)
57+ ]
58+ line_fingerprints = [
59+ fingerprint for fingerprint in line_fingerprint_candidates
60+ if FINGERPRINT_CHECK.match(fingerprint)
61+ ]
62+
63+ return fingerprint in line_fingerprints
64+
65+
66+def get_known_hosts(dns_name, ssh_proxy=None):
67+ args = []
68+ if ssh_proxy:
69+ args.extend(['ssh', '-S', 'none', ssh_proxy])
70+ args.extend(['ssh-keyscan', '-H', dns_name])
71+ cmd = subprocess.Popen(
72+ args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
73+ result = cmd.communicate()
74+ if cmd.returncode:
75+ raise RuntimeError('ssh-keyscan returned %d: %s' %
76+ (cmd.returncode, result[1]))
77+ return result[0]
78+
79+
80+def get_known_hosts_fingerprint(known_hosts):
81+ '''Return the key fingerprint given a line from a known hosts file.'''
82+ with tempfile.NamedTemporaryFile() as f:
83+ f.write(known_hosts)
84+ f.flush()
85+ cmd = subprocess.Popen(['ssh-keygen', '-lf', f.name],
86+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
87+ result = cmd.communicate()
88+ if cmd.returncode:
89+ raise RuntimeError('ssh-keygen returned %d: %s' %
90+ (cmd.returncode, result[1]))
91+ return result[0].decode('ascii').split()[1]
92+
93+
94+def update_known_hosts(known_hosts_line, remove_hostname=None):
95+ if remove_hostname and os.path.exists(KNOWN_HOSTS_PATH):
96+ subprocess.check_call(['ssh-keygen', '-R', remove_hostname])
97+ with open(KNOWN_HOSTS_PATH, 'ab') as f:
98+ f.write(known_hosts_line)
99+
100+
101+def import_(
102+ fingerprint_lines, hostname, ssh_proxy=None, remove_old_entry=True,
103+ check_only=False):
104+ known_hosts_line = get_known_hosts(hostname, ssh_proxy)
105+ known_hosts_fingerprint = get_known_hosts_fingerprint(known_hosts_line)
106+ if fingerprint_appears_in_lines(
107+ known_hosts_fingerprint, fingerprint_lines):
108+ if not check_only:
109+ remove_hostname = hostname if remove_old_entry else None
110+ update_known_hosts(
111+ known_hosts_line, remove_hostname=remove_hostname)
112+ return True
113+ else:
114+ print(
115+ "Fingerprint mismatch! known_hosts not updated.", file=sys.stderr)
116+ return False
117+
118+
119+def main_grep(args):
120+ console_output = sys.stdin.read()
121+ print(*extract_console_fingerprint(console_output), sep="\n")
122+
123+
124+def main_import(args):
125+ fingerprint_lines = sys.stdin.read().splitlines()
126+ sys.exit(not import_(
127+ fingerprint_lines=fingerprint_lines,
128+ hostname=args.hostname,
129+ ssh_proxy=args.ssh_proxy,
130+ remove_old_entry=args.remove_old_entry,
131+ check_only=args.check_only
132+ ))
133+
134+
135+def main_fix(args):
136+ console_output = sys.stdin.read()
137+ fingerprint_lines = extract_console_fingerprint(console_output)
138+ sys.exit(not import_(
139+ fingerprint_lines=fingerprint_lines,
140+ hostname=args.hostname,
141+ ssh_proxy=args.ssh_proxy
142+ ))
143+
144+
145+def main(args):
146+ parser = argparse.ArgumentParser()
147+ subparsers = parser.add_subparsers()
148+ grep_subparser = subparsers.add_parser('grep')
149+ grep_subparser.set_defaults(func=main_grep)
150+ import_subparser = subparsers.add_parser('import')
151+ import_subparser.set_defaults(func=main_import)
152+ import_subparser.add_argument('--ssh-proxy')
153+ import_subparser.add_argument(
154+ '--remove-old-entry', action='store_true', default=True)
155+ import_subparser.add_argument(
156+ '--no-remove-old-entry', action='store_false', dest='remove_old_entry')
157+ import_subparser.add_argument('--check-only', action='store_true')
158+ import_subparser.add_argument('hostname')
159+ fix_subparser = subparsers.add_parser('fix')
160+ fix_subparser.set_defaults(func=main_fix)
161+ fix_subparser.add_argument('--ssh-proxy')
162+ fix_subparser.add_argument('hostname')
163+ args = parser.parse_args(args)
164+ args.func(args)
165+
166+
167+if __name__ == '__main__':
168+ main(sys.argv[1:])
169
170=== modified file 'debian/control'
171--- debian/control 2012-10-11 21:07:12 +0000
172+++ debian/control 2012-12-17 15:32:32 +0000
173@@ -16,14 +16,15 @@
174 python,
175 python-paramiko,
176 python-yaml,
177+ python3,
178 util-linux (>= 2.17.2),
179 wget,
180 ${misc:Depends}
181 Recommends: distro-info, python-distro-info
182-Suggests: mtools
183+Suggests: mtools, openssh-client
184 Description: cloud image management utilities
185 This package provides a useful set of utilities for managing cloud
186- images.
187+ instances and images.
188 .
189 The euca2ools package (a dependency of cloud-utils) provides an
190 Amazon EC2 API compatible set of utilities for bundling kernels,
191
192=== added file 'man/cloud-fingerprint.1'
193--- man/cloud-fingerprint.1 1970-01-01 00:00:00 +0000
194+++ man/cloud-fingerprint.1 2012-12-17 15:32:32 +0000
195@@ -0,0 +1,131 @@
196+.TH cloud\-fingerprint 1 "17 Dec 2012" cloud\-utils cloud\-utils
197+
198+.SH NAME
199+cloud-fingerprint \- manage \f[CI]~/.ssh/known_hosts\fR for ephemeral
200+instances
201+
202+.SH SYNOPSIS
203+
204+.SY cloud\-fingerprint
205+.B fix
206+.OP \-\-ssh\-proxy ssh_proxy
207+.OP \-\-no\-remove\-old\-entry
208+.OP \-\-check\-only
209+.I hostname
210+.YS
211+
212+.SY cloud\-fingerprint
213+.B grep
214+.YS
215+.SY cloud\-fingerprint
216+.B import
217+.OP \-\-ssh\-proxy ssh_proxy
218+.OP \-\-no\-remove\-old\-entry
219+.OP \-\-check\-only
220+.I hostname
221+.YS
222+
223+.SH DESCRIPTION
224+\fBcloud\-fingerprint\fR securely translates fingerprints printed in instance
225+console output to verified entries in \f[CI]~/.ssh/known_hosts\fR.
226+
227+.TP
228+\fB-h\fR | \fB--help\fR
229+Show usage message.
230+
231+.TP
232+.BI \-\-ssh\-proxy \0ssh\-proxy
233+Call
234+.B ssh\-keyscan(1)
235+via
236+.I ssh\-proxy
237+instead of calling it directly.
238+
239+.B ssh\-keyscan(1)
240+is used to obtain the host's public key for fingerprint verification, but this
241+will not work correctly if you do not have direct access to the host. Instead,
242+use this option to define which machine can access the host directly.
243+
244+This option is useful for hosts which you access using a Proxy\:Command
245+directive in your ssh configuration.
246+
247+.TP
248+.I \-\-check\-only
249+Just verify the fingerprint; do not add or remove entries in
250+\f[CI]~/.ssh/known_hosts\fR. This is useful for testing and verification only.
251+Relying on the result after
252+.B cloud-fingerprint
253+exits is prone to a race condition and is not secure.
254+
255+.TP
256+.I \-\-no\-remove\-old\-entry
257+Do not remove old entries from \f[CI]~/.ssh/known_hosts\fR.
258+
259+.TP
260+.B
261+fix
262+Do everything. Read console output from \fIstdin\fR, parse it for
263+.B ssh-keygen(1)
264+output as marked by \fBcloud-init\fR, ask the host for its public key, verify
265+it against the fingerprint from the console output, and add the public key to
266+\f[CI]~/.ssh/known_hosts\fR.
267+
268+This is currently a shortcut to run \fBgrep\fR and then \fBimport\fR, but may
269+change in the future. It is intended for interactive use only. Scripts should call
270+.B grep
271+and
272+.B import
273+directly as required.
274+
275+.TP
276+.B
277+grep
278+Read
279+.B ssh-keygen(1)
280+output marked by
281+.B cloud-init
282+from
283+.I stdin
284+and write the original
285+.B ssh-keygen(1)
286+output to \fIstdout\fR.
287+
288+.TP
289+.B
290+import
291+Read
292+.B ssh-keygen(1)
293+output from \fIstdin\fR, ask the host for its public key, verify it against the
294+fingerprint from the console output, and add the public key to
295+\f[CI]~/.ssh/known_hosts\fR.
296+
297+.P
298+.I hostname
299+should match the hostname component of your subsequent
300+.B ssh(1)
301+command. It must be resolvable by \fBssh-keyscan(1)\fR.
302+
303+.SH EXAMPLES
304+Use the console output of instance \fIi\-1234\fR and set up
305+\f[CI]~/.ssh/known_hosts\fR so that the following \fBssh\fR command works
306+securely without prompting:
307+
308+.EX
309+.RS
310+$ \fBeuca\-get\-console\-output\fR \fIi\-1234\fR | \\
311+ \fBcloud\-fingerprint fix\fR \fItest\-server.openstack\fR
312+$ \fBssh\fR \fIubuntu@test-server.openstack\fR
313+.EE
314+.RE
315+
316+.SH SEE ALSO
317+euca-get-console-output(1), ssh(1), ssh-keygen(1), ssh-keyscan(1), sshd(8)
318+
319+.SH AUTHOR
320+This manpage and the utility was written by Robie Basak
321+<robie.basak@canonical.com>. Permission is granted to copy, distribute and/or
322+modify this document under the terms of the GNU General Public License, Version
323+3 published by the Free Software Foundation.
324+
325+On Debian systems, the complete text of the GNU General Public License can be
326+found in /usr/share/common-licenses/GPL.

Subscribers

People subscribed via source and target branches