[Libguestfs] [PATCH 3/4] p2v: Add SSH Identity URL.

Richard W.M. Jones rjones at redhat.com
Tue Aug 25 18:34:11 UTC 2015


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




More information about the Libguestfs mailing list