[Libguestfs] [PATCH] customize: Add --ssh-inject option for injecting SSH keys.

Richard W.M. Jones
Sun, 02 Nov 2014 12:46:40 -0000

This adds a customize option:

  virt-customize --ssh-inject USER[=KEY]
  virt-builder --ssh-inject USER[=KEY]
  virt-sysprep --ssh-inject USER[=KEY]

In each case this either injects the current (host) user's ssh pubkey
into the guest user USER (adding it to ~USER/.ssh/authorized_keys in
the guest), or you can specify a particular key.

For example:

  virt-builder fedora-20 --ssh-inject root

will add the local user's ssh pubkey into the root account of the
newly created guest.  Or:

  virt-customize -a disk.img \
     --ssh-inject 'mary=ssh-rsa AAAA.... mary at localhost'

adds the given ssh pubkey to mary's account in the guest.

This doesn't set the SELinux labels correctly on newly created files
and directories, so you have to use --selinux-relabel (probably we
should fix this as part of the general effort to fix SELinux
relabelling).  However it should preserve the labels if the
~/.ssh/authorized_keys file already exists.
---
 builder/cmdline.ml         |  4 +--
 customize/customize_run.ml | 81 ++++++++++++++++++++++++++++++++++++++++++++++
 generator/customize.ml     | 18 +++++++++++
 3 files changed, 101 insertions(+), 2 deletions(-)

diff --git a/builder/cmdline.ml b/builder/cmdline.ml
index c0584f7..e21d5bb 100644
--- a/builder/cmdline.ml
+++ b/builder/cmdline.ml
@@ -306,8 +306,8 @@ read the man page virt-builder(1).
           | `Command _ | `InstallPackages _ | `Script _ | `Update -> true
           | `Delete _ | `Edit _ | `FirstbootCommand _ | `FirstbootPackages _
           | `FirstbootScript _ | `Hostname _ | `Link _ | `Mkdir _
-          | `Password _ | `RootPassword _ | `Scrub _ | `Timezone _ | `Upload _
-          | `Write _ | `Chmod _ -> false
+          | `Password _ | `RootPassword _ | `Scrub _ | `SSHInject _
+          | `Timezone _ | `Upload _ | `Write _ | `Chmod _ -> false
         ) ops.ops in
         if requires_execute_on_guest then
           error (f_"sorry, cannot run commands on a guest with a different
architecture");
diff --git a/customize/customize_run.ml b/customize/customize_run.ml
index 51b218a..099d611 100644
--- a/customize/customize_run.ml
+++ b/customize/customize_run.ml
@@ -135,6 +135,81 @@ exec >>%s 2>&1
       error (f_"sorry, don't know how to use --update with the
'%s' package manager") pm
   in

+  (* Find the local [on the host] user's SSH public key.  See
+   * ssh-copy-id(1) default_ID_file for rationale.
+   *)
+  let pubkey_re = Str.regexp "^id.*\\.pub$" in
+  let pubkey_ignore_re = Str.regexp ".*-cert\\.pub$" in
+
+  let local_user_ssh_pubkey () +    let home_dir +      try getenv
"HOME"
+      with Not_found ->
+        error (f_"ssh-inject: $HOME environment variable is not set")
in
+    let ssh_dir = home_dir // ".ssh" in
+    let files = Sys.readdir ssh_dir in
+    let files = Array.to_list files in
+    let files = List.filter (
+      fun file ->
+        Str.string_match pubkey_re file 0 &&
+          not (Str.string_match pubkey_ignore_re file 0)
+    ) files in
+    if files = [] then
+      error (f_"ssh-inject: no public key file found in %s") ssh_dir;
+
+    (* Newest file. *)
+    let files = List.map (
+      fun file ->
+        let file = ssh_dir // file in
+        let stat = stat file in
+        (file, stat.st_mtime)
+    ) files in
+    let files = List.sort (fun (_,m1) (_,m2) -> compare m2 m1) files in
+    let newest_file = fst (List.hd files) in
+
+    (* Read and return the public key. *)
+    let key = read_whole_file newest_file in
+    if key = "" then
+      error (f_"ssh-inject: public key file (%s) is empty")
newest_file;
+
+    key
+
+  (* Inject SSH key, where possible. *)
+  and do_ssh_inject username key +    match g#inspect_get_type root with
+    | "linux" | "freebsd" | "netbsd" |
"openbsd" | "hurd" ->
+      (* If the key doesn't have \n at the end, add it. *)
+      let len = String.length key in
+      if len < 1 then
+        error (f_"ssh-inject: key is an empty string");
+      let key = if key.[len-1] = '\n' then key else key ^
"\n" in
+
+      (* Get user's home directory. *)
+      g#aug_init "/" 0;
+      let expr = sprintf "/files/etc/passwd/%s/home" username in
+      let home_dir = g#aug_get expr in
+      g#aug_close ();
+
+      (* Create ~user/.ssh if it doesn't exist. *)
+      let ssh_dir = sprintf "%s/.ssh" home_dir in
+      if not (g#exists ssh_dir) then (
+        g#mkdir ssh_dir;
+        g#chmod 0o755 ssh_dir
+      );
+
+      (* Create ~user/.ssh/authorized_keys if it doesn't exist. *)
+      let auth_keys = sprintf "%s/authorized_keys" ssh_dir in
+      if not (g#exists auth_keys) then (
+        g#touch auth_keys;
+        g#chmod 0o644 auth_keys
+      );
+
+      (* Append the key. *)
+      g#write_append auth_keys key
+    | typ ->
+      warning (f_"don't know how to inject SSH keys into %s
guests") typ
+  in
+
   (* Set the random seed. *)
   msg (f_"Setting a random seed");
   if not (Random_seed.set_random_seed g root) then
@@ -232,6 +307,12 @@ exec >>%s 2>&1
       msg (f_"Scrubbing: %s") path;
       g#scrub_file path

+    | `SSHInject user_key ->
+      let user, key = string_split "=" user_key in
+      let key = if key = "" then local_user_ssh_pubkey () else key in
+      msg (f_"SSH key inject: %s") user;
+      do_ssh_inject user key
+
     | `Timezone tz ->
       msg (f_"Setting the timezone: %s") tz;
       if not (Timezone.set_timezone g root tz) then
diff --git a/generator/customize.ml b/generator/customize.ml
index 8642a54..ef91b62 100644
--- a/generator/customize.ml
+++ b/generator/customize.ml
@@ -260,6 +260,24 @@ It cannot delete directories, only regular files.
 =back";
   };

+  { op_name = "ssh-inject";
+    op_type = String "USER[=KEY]";
+    op_discrim = "`SSHInject";
+    op_shortdesc = "Inject a public key into the guest";
+    op_pod_longdesc = "\
+Inject an ssh key so the given C<USER> will be able to log in over
+ssh without supplying a password.
+
+If just I<--ssh-inject> C<USER> is given then we look in the
I<current>
+user's C<~/.ssh> directory to find the default public ID file.  That
+key is uploaded.
+
+You can also upload a specific key using I<--ssh-inject>
C<\"USER=KEY\">
+
+In either case, this will create the C<~USER/.ssh> directory if required,
+and create or append the key to C<~USER/.ssh/authorized_keys>"
+  };
+
   { op_name = "timezone";
     op_type = String "TIMEZONE";
     op_discrim = "`Timezone";
--
2.0.4