Allow SSH identities (ie. secret keys) to be used for authentication
instead of passwords.
---
p2v/config.c | 8 +++
p2v/dependencies.m4 | 4 ++
p2v/gui.c | 34 +++++++++-
p2v/kernel.c | 7 +++
p2v/p2v.h | 3 +
p2v/p2v.ks.in | 12 +++-
p2v/ssh.c | 135 ++++++++++++++++++++++++++++++++++++++--
p2v/virt-p2v-make-disk.pod | 33 ++++++++++
p2v/virt-p2v-make-kickstart.pod | 35 +++++++++++
p2v/virt-p2v.pod | 98 +++++++++++++++++++++++++++--
10 files changed, 355 insertions(+), 14 deletions(-)
diff --git a/p2v/config.c b/p2v/config.c
index 66cafb5..ae8af38 100644
--- a/p2v/config.c
+++ b/p2v/config.c
@@ -64,6 +64,10 @@ copy_config (struct config *old)
c->username = strdup (c->username);
if (c->password)
c->password = strdup (c->password);
+ if (c->identity_url)
+ c->identity_url = strdup (c->identity_url);
+ if (c->identity_file)
+ c->identity_file = strdup (c->identity_file);
if (c->guestname)
c->guestname = strdup (c->guestname);
if (c->disks)
@@ -92,6 +96,8 @@ free_config (struct config *c)
free (c->server);
free (c->username);
free (c->password);
+ free (c->identity_url);
+ free (c->identity_file);
free (c->guestname);
guestfs_int_free_string_list (c->disks);
guestfs_int_free_string_list (c->removable);
@@ -122,6 +128,8 @@ print_config (struct config *config, FILE *fp)
config->username ? config->username : "none");
fprintf (fp, "password . . . . %s\n",
config->password && strlen (config->password) > 0 ?
"***" : "none");
+ fprintf (fp, "identity URL . . %s\n",
+ config->identity_url ? config->identity_url : "none");
fprintf (fp, "sudo . . . . . . %s\n",
config->sudo ? "true" : "false");
fprintf (fp, "guest name . . . %s\n",
diff --git a/p2v/dependencies.m4 b/p2v/dependencies.m4
index e342ce3..acca78e 100644
--- a/p2v/dependencies.m4
+++ b/p2v/dependencies.m4
@@ -28,6 +28,7 @@ ifelse(REDHAT,1,
dnl Run as external programs by the p2v binary.
/usr/bin/ssh
/usr/bin/qemu-nbd
+ curl
dnl The hwdata package contains PCI IDs, used by virt-p2v to display
dnl network vendor information (RHBZ#855059).
@@ -56,6 +57,7 @@ ifelse(DEBIAN,1,
libgtk2.0-0
openssh-client
qemu-utils
+ curl
hwdata
xorg
xserver-xorg-video-all
@@ -72,6 +74,7 @@ ifelse(ARCHLINUX,1,
gtk2
openssh
qemu
+ curl
hwdata
xorg-xinit
xorg-server
@@ -89,6 +92,7 @@ ifelse(SUSE,1,
gtk2
/usr/bin/ssh
/usr/bin/qemu-nbd
+ curl
hwdata
/usr/bin/xinit
/usr/bin/Xorg
diff --git a/p2v/gui.c b/p2v/gui.c
index 9719e70..0339e4f 100644
--- a/p2v/gui.c
+++ b/p2v/gui.c
@@ -53,7 +53,7 @@ static void set_info_label (void);
/* The connection dialog. */
static GtkWidget *conn_dlg,
*server_entry, *port_entry,
- *username_entry, *password_entry, *sudo_button,
+ *username_entry, *password_entry, *identity_entry, *sudo_button,
*spinner_hbox, *spinner, *spinner_message, *next_button;
/* The conversion dialog. */
@@ -107,6 +107,8 @@ create_connection_dialog (struct config *config)
GtkWidget *port_label;
GtkWidget *username_label;
GtkWidget *password_label;
+ GtkWidget *identity_label;
+ GtkWidget *identity_tip_label;
GtkWidget *test_hbox, *test;
GtkWidget *about;
GtkWidget *configure_network;
@@ -121,7 +123,7 @@ create_connection_dialog (struct config *config)
gtk_label_set_line_wrap (GTK_LABEL (intro), TRUE);
gtk_misc_set_padding (GTK_MISC (intro), 10, 10);
- table = gtk_table_new (5, 2, FALSE);
+ table = gtk_table_new (7, 2, FALSE);
server_label = gtk_label_new (_("Conversion server:"));
gtk_misc_set_alignment (GTK_MISC (server_label), 1., 0.5);
gtk_table_attach (GTK_TABLE (table), server_label,
@@ -170,12 +172,29 @@ create_connection_dialog (struct config *config)
gtk_table_attach (GTK_TABLE (table), password_entry,
1, 2, 3, 4, GTK_FILL, GTK_FILL, 4, 4);
+ identity_label = gtk_label_new (_("SSH Identity URL:"));
+ gtk_misc_set_alignment (GTK_MISC (identity_label), 1., 0.5);
+ gtk_table_attach (GTK_TABLE (table), identity_label,
+ 0, 1, 4, 5, GTK_FILL, GTK_FILL, 4, 4);
+ identity_entry = gtk_entry_new ();
+ if (config->identity_url != NULL)
+ gtk_entry_set_text (GTK_ENTRY (identity_entry), config->identity_url);
+ gtk_table_attach (GTK_TABLE (table), identity_entry,
+ 1, 2, 4, 5, GTK_FILL, GTK_FILL, 4, 4);
+
+ identity_tip_label = gtk_label_new (NULL);
+ gtk_label_set_markup (GTK_LABEL (identity_tip_label),
+ _("<i>If using password authentication, leave the SSH
Identity URL blank</i>"));
+ gtk_label_set_line_wrap (GTK_LABEL (identity_tip_label), TRUE);
+ gtk_table_attach (GTK_TABLE (table), identity_tip_label,
+ 1, 2, 5, 6, GTK_FILL, GTK_FILL, 4, 4);
+
sudo_button =
gtk_check_button_new_with_label (_("Use sudo when running virt-v2v"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (sudo_button),
config->sudo);
gtk_table_attach (GTK_TABLE (table), sudo_button,
- 1, 2, 4, 5, GTK_FILL, GTK_FILL, 4, 4);
+ 1, 2, 6, 7, GTK_FILL, GTK_FILL, 4, 4);
test_hbox = gtk_hbox_new (FALSE, 0);
test = gtk_button_new_with_label (_("Test connection"));
@@ -242,6 +261,7 @@ test_connection_clicked (GtkWidget *w, gpointer data)
{
struct config *config = data;
const gchar *port_str;
+ const gchar *identity_str;
size_t errors = 0;
struct config *copy;
int err;
@@ -279,6 +299,14 @@ test_connection_clicked (GtkWidget *w, gpointer data)
free (config->password);
config->password = strdup (gtk_entry_get_text (GTK_ENTRY (password_entry)));
+ free (config->identity_url);
+ identity_str = gtk_entry_get_text (GTK_ENTRY (identity_entry));
+ if (identity_str && STRNEQ (identity_str, ""))
+ config->identity_url = strdup (identity_str);
+ else
+ config->identity_url = NULL;
+ config->identity_file_needs_update = 1;
+
config->sudo = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (sudo_button));
if (errors)
diff --git a/p2v/kernel.c b/p2v/kernel.c
index 9fec47f..4605561 100644
--- a/p2v/kernel.c
+++ b/p2v/kernel.c
@@ -72,6 +72,13 @@ kernel_configuration (struct config *config, char **cmdline, int
cmdline_source)
config->password = strdup (p);
}
+ p = get_cmdline_key (cmdline, "p2v.identity");
+ if (p) {
+ free (config->identity_url);
+ config->identity_url = strdup (p);
+ config->identity_file_needs_update = 1;
+ }
+
p = get_cmdline_key (cmdline, "p2v.sudo");
if (p)
config->sudo = 1;
diff --git a/p2v/p2v.h b/p2v/p2v.h
index 34f6bcf..35b3f3c 100644
--- a/p2v/p2v.h
+++ b/p2v/p2v.h
@@ -59,6 +59,9 @@ struct config {
int port;
char *username;
char *password;
+ char *identity_url;
+ char *identity_file; /* Used to cache the downloaded identity_url. */
+ int identity_file_needs_update;
int sudo;
char *guestname;
int vcpus;
diff --git a/p2v/p2v.ks.in b/p2v/p2v.ks.in
index 4f90af1..d9ad98f 100644
--- a/p2v/p2v.ks.in
+++ b/p2v/p2v.ks.in
@@ -1,5 +1,5 @@
# Kickstart file for creating the virt-p2v ISO.
-# (C) Copyright 2014 Red Hat Inc.
+# (C) Copyright 2014-2015 Red Hat Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -79,6 +79,16 @@ __DEPENDENCIES__
%post
+# Inject an SSH Identity by uncommenting the following lines and
+# pasting the contents between the 'cat' and 'EOF':
+
+#cat > /var/tmp/id_rsa << EOF
+#-----BEGIN RSA PRIVATE KEY-----
+# ...
+#-----END RSA PRIVATE KEY-----
+#EOF
+#chmod 0600 /var/tmp/id_rsa
+
# Base64-decoding of /etc/issue
base64 -d > /etc/issue << EOF
diff --git a/p2v/ssh.c b/p2v/ssh.c
index afb0a25..3c8d309 100644
--- a/p2v/ssh.c
+++ b/p2v/ssh.c
@@ -165,8 +165,111 @@ free_regexps (void)
pcre_free (portfwd_re);
}
+/* Download URL to local file using the external 'curl' command. */
+static int
+curl_download (const char *url, const char *local_file)
+{
+ char curl_config_file[] = "/tmp/curl.XXXXXX";
+ int fd, r;
+ size_t i, len;
+ FILE *fp;
+ CLEANUP_FREE char *curl_cmd = NULL;
+
+ /* Use a secure curl config file because escaping is easier. */
+ fd = mkstemp (curl_config_file);
+ if (fd == -1) {
+ perror ("mkstemp");
+ exit (EXIT_FAILURE);
+ }
+ fp = fdopen (fd, "w");
+ if (fp == NULL) {
+ perror ("fdopen");
+ exit (EXIT_FAILURE);
+ }
+ fprintf (fp, "url = \"");
+ len = strlen (url);
+ for (i = 0; i < len; ++i) {
+ switch (url[i]) {
+ case '\\': fprintf (fp, "\\\\"); break;
+ case '"': fprintf (fp, "\\\""); break;
+ case '\t': fprintf (fp, "\\t"); break;
+ case '\n': fprintf (fp, "\\n"); break;
+ case '\r': fprintf (fp, "\\r"); break;
+ case '\v': fprintf (fp, "\\v"); break;
+ default: fputc (url[i], fp);
+ }
+ }
+ fprintf (fp, "\"\n");
+ fclose (fp);
+
+ /* Run curl to download the URL to a file. */
+ if (asprintf (&curl_cmd, "curl -f -o %s -K %s",
+ local_file, curl_config_file) == -1) {
+ perror ("asprintf");
+ exit (EXIT_FAILURE);
+ }
+
+ r = system (curl_cmd);
+ /* unlink (curl_config_file); - useful for debugging */
+ if (r == -1) {
+ perror ("system");
+ exit (EXIT_FAILURE);
+ }
+
+ /* Did curl subprocess fail? */
+ if (WIFEXITED (r) && WEXITSTATUS (r) != 0) {
+ /* XXX Better error handling. The codes can be looked up in
+ * the curl(1) man page.
+ */
+ set_ssh_error ("%s: curl error %d", url, WEXITSTATUS (r));
+ return -1;
+ }
+ else if (!WIFEXITED (r)) {
+ set_ssh_error ("curl subprocess got a signal (%d)", r);
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Re-cache the identity_url if needed. */
+static int
+cache_ssh_identity (struct config *config)
+{
+ int fd;
+
+ /* If it doesn't need downloading, return. */
+ if (config->identity_url == NULL ||
+ !config->identity_file_needs_update)
+ return 0;
+
+ /* Generate a random filename. */
+ free (config->identity_file);
+ config->identity_file = strdup ("/tmp/id.XXXXXX");
+ if (config->identity_file == NULL) {
+ perror ("strdup");
+ exit (EXIT_FAILURE);
+ }
+ fd = mkstemp (config->identity_file);
+ if (fd == -1) {
+ perror ("mkstemp");
+ exit (EXIT_FAILURE);
+ }
+ close (fd);
+
+ /* Curl download URL to file. */
+ if (curl_download (config->identity_url, config->identity_file) == -1) {
+ free (config->identity_file);
+ config->identity_file = NULL;
+ config->identity_file_needs_update = 1;
+ return -1;
+ }
+
+ return 0;
+}
+
/* Start ssh subprocess with the standard arguments and possibly some
- * optional arguments. Also handles password authentication.
+ * optional arguments. Also handles authentication.
*/
static mexp_h *
start_ssh (struct config *config, char **extra_args, int wait_prompt)
@@ -178,18 +281,29 @@ start_ssh (struct config *config, char **extra_args, int
wait_prompt)
const int ovecsize = 12;
int ovector[ovecsize];
int saved_timeout;
+ int using_password_auth;
+
+ if (cache_ssh_identity (config) == -1)
+ return NULL;
+
+ /* Are we using password or identity authentication? */
+ using_password_auth = config->identity_file == NULL;
/* Create the ssh argument array. */
nr_args = 0;
if (extra_args != NULL)
nr_args = guestfs_int_count_strings (extra_args);
- nr_args += 11;
+ if (using_password_auth)
+ nr_args += 11;
+ else
+ nr_args += 13;
args = malloc (sizeof (char *) * nr_args);
if (args == NULL) {
perror ("malloc");
exit (EXIT_FAILURE);
}
+
j = 0;
args[j++] = "ssh";
args[j++] = "-p"; /* Port. */
@@ -199,8 +313,18 @@ start_ssh (struct config *config, char **extra_args, int
wait_prompt)
args[j++] = config->username ? config->username : "root";
args[j++] = "-o"; /* Host key will always be novel. */
args[j++] = "StrictHostKeyChecking=no";
- args[j++] = "-o"; /* Only use password authentication. */
- args[j++] = "PreferredAuthentications=keyboard-interactive,password";
+ if (using_password_auth) {
+ /* Only use password authentication. */
+ args[j++] = "-o";
+ args[j++] = "PreferredAuthentications=keyboard-interactive,password";
+ }
+ else {
+ /* Use identity file (private key). */
+ args[j++] = "-o";
+ args[j++] = "PreferredAuthentications=publickey";
+ args[j++] = "-i";
+ args[j++] = config->identity_file;
+ }
if (extra_args != NULL) {
for (i = 0; extra_args[i] != NULL; ++i)
args[j++] = extra_args[i];
@@ -213,7 +337,8 @@ start_ssh (struct config *config, char **extra_args, int wait_prompt)
if (h == NULL)
return NULL;
- if (config->password && strlen (config->password) > 0) {
+ if (using_password_auth &&
+ config->password && strlen (config->password) > 0) {
/* Wait for the password prompt. */
switch (mexp_expect (h,
(mexp_regexp[]) {
diff --git a/p2v/virt-p2v-make-disk.pod b/p2v/virt-p2v-make-disk.pod
index 2d4b6a5..30c4ca4 100644
--- a/p2v/virt-p2v-make-disk.pod
+++ b/p2v/virt-p2v-make-disk.pod
@@ -46,6 +46,39 @@ Write a virt-p2v bootable virtual disk image, and boot it under qemu:
where F</var/tmp/guest.img> would be the disk image of some guest that
you want to convert (for testing only).
+=head1 ADDING AN SSH IDENTITY
+
+You can inject an SSH identity (private key) file to the image using
+L<guestfish(1)>. First create a key pair. It must have an empty
+passphrase:
+
+ ssh-keygen -t rsa -N '' -f id_rsa
+
+This creates a private key (C<id_rsa>) and a public key
+(C<id_rsa.pub>) pair. The public key should be appended to the
+C<authorized_keys> file on the virt-v2v conversion server (usually to
+C</root/.ssh/authorized_keys>).
+
+The private key should be injected into the disk image and then
+discarded:
+
+ guestfish -a p2v.img -i upload id_rsa /var/tmp/id_rsa
+ rm id_rsa
+
+When booting virt-p2v, specify the URL of the injected file like this:
+
+ │ User name: [root_____________________________] │
+ │ │
+ │ Password: [ <leave this field blank> ] │
+ │ │
+ │ SSH Identity URL: [file:///var/tmp/id_rsa___________] │
+
+or if using the kernel command line, add:
+
+ p2v.identity=file:///var/tmp/id_rsa
+
+For more information, see L<virt-p2v(1)/SSH IDENTITIES>.
+
=head1 OPTIONS
=over 4
diff --git a/p2v/virt-p2v-make-kickstart.pod b/p2v/virt-p2v-make-kickstart.pod
index d833a45..bbf566c 100644
--- a/p2v/virt-p2v-make-kickstart.pod
+++ b/p2v/virt-p2v-make-kickstart.pod
@@ -191,6 +191,41 @@ pxelinux starts up.
=back
+=head1 ADDING AN SSH IDENTITY
+
+You can inject an SSH identity (private key) file to the ISO by
+modifying the kickstart. Note that you I<cannot> inject a key once
+the ISO has been built.
+
+First create a key pair. It must have an empty passphrase:
+
+ ssh-keygen -t rsa -N '' -f id_rsa
+
+This creates a private key (C<id_rsa>) and a public key
+(C<id_rsa.pub>) pair. The public key should be appended to the
+C<authorized_keys> file on the virt-v2v conversion server (usually to
+C</root/.ssh/authorized_keys>).
+
+The private key should be added to the kickstart file by editing it
+and searching for the string C<SSH Identity> and following the
+instructions there. The ISO can then be built from the kickstart in
+the usual way (see above), and it will contain the embedded SSH
+identity (F</var/tmp/id_rsa>).
+
+When booting virt-p2v, specify the URL of the injected file like this:
+
+ │ User name: [root_____________________________] │
+ │ │
+ │ Password: [ <leave this field blank> ] │
+ │ │
+ │ SSH Identity URL: [file:///var/tmp/id_rsa___________] │
+
+or if using the kernel command line, add:
+
+ p2v.identity=file:///var/tmp/id_rsa
+
+For more information, see L<virt-p2v(1)/SSH IDENTITIES>.
+
=head1 OPTIONS
=over 4
diff --git a/p2v/virt-p2v.pod b/p2v/virt-p2v.pod
index e37e654..93fe4ab 100644
--- a/p2v/virt-p2v.pod
+++ b/p2v/virt-p2v.pod
@@ -89,10 +89,13 @@ When virt-p2v starts up in GUI mode, the first dialog looks like
this:
│ │
│ Password: [_________________________________] │
│ │
+ │ SSH Identity URL: [_________________________________] │
+ │ │
-In the fields above, you must enter the hostname, SSH port number,
-remote user name and password of the conversion server. The
-conversion server must have an up to date version of virt-v2v.
+In the fields above, you must enter the details of the conversion
+server: the hostname, SSH port number, remote user name, and either
+the password or SSH identity (private key) URL. The conversion server
+must have an up to date version of virt-v2v.
Normally you must log in to the conversion server as root, but if you
check the following box:
@@ -320,8 +323,17 @@ The default is to try with no password. If this fails then virt-p2v
will ask the user to type the password (probably several times during
conversion).
-Note that virt-p2v does not support authentication using key
-distribution at this time.
+This setting is ignored if C<p2v.identity> is present.
+
+=item B<p2v.identity=URL>
+
+Provide a URL pointing to an SSH identity (private key) file. The URL
+is interpreted by L<curl(1)> so any URL that curl supports can be used
+here, including C<https://> and C<file://>. For more information on
+using SSH identities, see L</SSH IDENTITIES> below.
+
+If C<p2v.identity> is present, it overrides C<p2v.password>. There is
+no fallback.
=item B<p2v.sudo>
@@ -474,6 +486,82 @@ Set up a static IPv4 network configuration.
=back
+=head1 SSH IDENTITIES
+
+As a somewhat more secure alternative to password authentication, you
+can use an SSH identity (private key) for authentication.
+
+First create a key pair. It must have an empty passphrase:
+
+ ssh-keygen -t rsa -N '' -f id_rsa
+
+This creates a private key (C<id_rsa>) and a public key
+(C<id_rsa.pub>) pair.
+
+The public key should be appended to the C<authorized_keys> file on
+the virt-v2v conversion server (usually to
+C</root/.ssh/authorized_keys>).
+
+For distributing the private key, there are four scenarios from least
+secure to most secure:
+
+=over 4
+
+=item 1.
+
+Not using SSH identities at all, ie. password authentication.
+
+Anyone who can sniff the PXE boot parameters from the network or
+observe the password some other way can log in to the virt-v2v
+conversion server.
+
+=item 2.
+
+SSH identity embedded in the virt-p2v ISO or disk image. In the GUI, use:
+
+ │ Password: [ <leave this field blank> ] │
+ │ │
+ │ SSH Identity URL: [file:///var/tmp/id_rsa_____________] │
+
+or on the kernel command line:
+
+ p2v.identity=file:///var/tmp/id_rsa
+
+The SSH private key can still be sniffed from the network if using
+standard PXE.
+
+=item 3.
+
+SSH identity downloaded from a website. In the GUI, use:
+
+ │ Password: [ <leave this field blank> ] │
+ │ │
+ │ SSH Identity URL: [
https://internal.example.com/id_rsa] │
+
+or on the kernel command line:
+
+
p2v.identity=https://internal.example.com/id_rsa
+
+Anyone could still download the private key and use it to log in to
+the virt-v2v conversion server, but you could provide some extra
+security by configuring the web server to only allow connections from
+P2V machines.
+
+=item 4.
+
+SSH identity embedded in the virt-p2v ISO or disk image (like 2.),
+I<and> use of secure PXE, PXE over separate physical network, or
+sneakernet to distribute virt-p2v to the physical machine.
+
+=back
+
+For instructions on how to embed the private key in the virt-p2v ISO
+or disk image, see the following manual sections:
+
+L<virt-p2v-make-disk(1)/ADDING AN SSH IDENTITY>
+
+L<virt-p2v-make-kickstart(1)/ADDING AN SSH IDENTITY>
+
=head1 OPTIONS
=over 4
--
2.5.0