This adds a customize option: virt-customize --ssh-inject USER virt-customize --ssh-inject USER:string:KEY_STRING virt-customize --ssh-inject USER:file:FILENAME (ditto for virt-builder and virt-sysprep) In each case this injects into the guest user USER a) the current (host) user's ssh pubkey b) the key specified as KEY_STRING c) the key in FILENAME adding it to ~USER/.ssh/authorized_keys in the guest. 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:string:ssh-rsa AAAA.... mary@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. Most of this work is based on a patch sent to the mailing list by Richard W.M. Jones <rjones@redhat.com>: https://www.redhat.com/archives/libguestfs/2014-November/msg00000.html --- builder/Makefile.am | 1 + builder/cmdline.ml | 4 +- builder/virt-builder.pod | 31 +++++++++++ customize/Makefile.am | 3 + customize/customize_run.ml | 8 +++ customize/ssh_key.ml | 133 +++++++++++++++++++++++++++++++++++++++++++++ customize/ssh_key.mli | 31 +++++++++++ generator/customize.ml | 38 ++++++++++++- po/POTFILES-ml | 1 + sysprep/Makefile.am | 1 + v2v/Makefile.am | 1 + 11 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 customize/ssh_key.ml create mode 100644 customize/ssh_key.mli diff --git a/builder/Makefile.am b/builder/Makefile.am index 414279f..206abce 100644 --- a/builder/Makefile.am +++ b/builder/Makefile.am @@ -111,6 +111,7 @@ deps = \ $(top_builddir)/customize/crypt-c.o \ $(top_builddir)/customize/crypt.cmx \ $(top_builddir)/customize/password.cmx \ + $(top_builddir)/customize/ssh_key.cmx \ $(top_builddir)/customize/customize_cmdline.cmx \ $(top_builddir)/customize/customize_run.cmx \ $(top_builddir)/fish/guestfish-uri.o \ 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/builder/virt-builder.pod b/builder/virt-builder.pod index 993e92c..82533d6 100644 --- a/builder/virt-builder.pod +++ b/builder/virt-builder.pod @@ -774,6 +774,37 @@ If C</tmp> or C<C:\Temp> is missing. If you don't want the log file to appear in the final image, then use the I<--no-logfile> command line option. +=head2 SSH KEYS + +The I<--ssh-inject> option is used to inject ssh keys for users in +the guest, so they can login without supplying a password. + +The C<SELECTOR> part of the option value is optional; in this case, +I<--ssh-inject> C<USER> means that we look in the I<current> +user's C<~/.ssh> directory to find the default public ID file. That +key is uploaded. "default public ID" is the I<default_ID_file> file +described in L<ssh-copy-id(1)>. + +If specified, the C<SELECTOR> can be in one of the following formats: + +=over 4 + +=item B<--ssh-inject> USER:file:FILENAME + +Read the ssh key from C<FILENAME>. C<FILENAME> is usually a I<.pub> +file. + +=item B<--ssh-inject> USER:string:KEY_STRING + +Use the specified C<KEY_STRING>. C<KEY_STRING> is usually a public +string like I<ssh-rsa AAAA.... user@localhost>. + +=back + +In any case, the C<~USER/.ssh> directory and the +C<~USER/.ssh/authorized_keys> file will be created if not existing +already. + =head2 INSTALLATION PROCESS When you invoke virt-builder, installation proceeds as follows: diff --git a/customize/Makefile.am b/customize/Makefile.am index 60e2091..56c5ad5 100644 --- a/customize/Makefile.am +++ b/customize/Makefile.am @@ -55,6 +55,8 @@ SOURCES = \ perl_edit.mli \ random_seed.ml \ random_seed.mli \ + ssh_key.ml \ + ssh_key.mli \ timezone.ml \ timezone.mli \ urandom.ml \ @@ -92,6 +94,7 @@ ocaml_modules = \ password \ perl_edit \ random_seed \ + ssh_key \ timezone \ customize_cmdline \ customize_run \ diff --git a/customize/customize_run.ml b/customize/customize_run.ml index 51b218a..09ada7d 100644 --- a/customize/customize_run.ml +++ b/customize/customize_run.ml @@ -232,6 +232,14 @@ exec >>%s 2>&1 msg (f_"Scrubbing: %s") path; g#scrub_file path + | `SSHInject (user, selector) -> + (match g#inspect_get_type root with + | "linux" | "freebsd" | "netbsd" | "openbsd" | "hurd" -> + msg (f_"SSH key inject: %s") user; + Ssh_key.do_ssh_inject_unix g user selector + | _ -> + warning (f_"SSH key could be injected for this type of guest")) + | `Timezone tz -> msg (f_"Setting the timezone: %s") tz; if not (Timezone.set_timezone g root tz) then diff --git a/customize/ssh_key.ml b/customize/ssh_key.ml new file mode 100644 index 0000000..09664bf --- /dev/null +++ b/customize/ssh_key.ml @@ -0,0 +1,133 @@ +(* virt-customize + * Copyright (C) 2014 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +open Common_gettext.Gettext +open Common_utils + +open Customize_utils + +open Printf +open Sys +open Unix + +module G = Guestfs + +type ssh_key_selector = +| SystemKey +| KeyFile of string +| KeyString of string + +let rec parse_selector arg = + parse_selector_list arg (string_nsplit ":" arg) + +and parse_selector_list orig_arg = function + | [] | [ "" ] -> + SystemKey + | [ "file"; f ] -> + KeyFile f + | [ "string"; s ] -> + KeyString s + | _ -> + error (f_"invalid ssh-inject selector '%s'; see the man page") orig_arg + +(* 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$" +let pubkey_ignore_re = Str.regexp ".*-cert\\.pub$" + +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 + + fst (List.hd files) + +let read_key file = + (* Read and return the public key. *) + let key = read_whole_file file in + if key = "" then + error (f_"ssh-inject: public key file (%s) is empty") file; + key + +let key_string_from_selector = function + | SystemKey -> + read_key (local_user_ssh_pubkey ()) + | KeyFile f -> + read_key f + | KeyString s -> + if String.length s < 1 then + error (f_"ssh-inject: key is an empty string"); + s + +(* Inject SSH key, where possible. *) +let do_ssh_inject_unix (g : Guestfs.guestfs) user selector = + let key = key_string_from_selector selector in + assert (String.length key > 0); + + (* If the key doesn't have \n at the end, add it. *) + let len = String.length key in + let key = if key.[len-1] = '\n' then key else key ^ "\n" in + + (* Get user's home directory. *) + g#aug_init "/" 0; + let home_dir = + try + let expr = sprintf "/files/etc/passwd/%s/home" user in + g#aug_get expr + with G.Error _ -> + error (f_"ssh-inject: the user %s does not exist on the guest") + user 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 diff --git a/customize/ssh_key.mli b/customize/ssh_key.mli new file mode 100644 index 0000000..3223e55 --- /dev/null +++ b/customize/ssh_key.mli @@ -0,0 +1,31 @@ +(* virt-customize + * Copyright (C) 2014 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +type ssh_key_selector = +| SystemKey (* Default key from the user in the system, in + * the style of ssh-copy-id(1)/default_ID_file. + *) +| KeyFile of string (* Key from the specified file. *) +| KeyString of string (* Key specified as string. *) + +val parse_selector : string -> ssh_key_selector +(** Parse the selector field in --ssh-ibject. Note this + doesn't parse the username part. Exits if the format is not valid. *) + +val do_ssh_inject_unix : Guestfs.guestfs -> string -> ssh_key_selector -> unit +(** ... *) diff --git a/generator/customize.ml b/generator/customize.ml index 8642a54..82ecb79 100644 --- a/generator/customize.ml +++ b/generator/customize.ml @@ -42,6 +42,7 @@ and op_type = | TargetLinks of string (* target:link[:link...] *) | PasswordSelector of string (* password selector *) | UserPasswordSelector of string (* user:selector *) +| SSHKeySelector of string (* user:selector *) let ops = [ { op_name = "chmod"; @@ -260,6 +261,22 @@ It cannot delete directories, only regular files. =back"; }; + { op_name = "ssh-inject"; + op_type = SSHKeySelector "USER[:SELECTOR]"; + 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. The C<USER> must exist already +in the guest. + +See L<virt-builder(1)/SSH KEYS> for the format of +the C<SELECTOR> field. + +You can have multiple I<--ssh-inject> options, for different users +and also for more keys for each user." + }; + { op_name = "timezone"; op_type = String "TIMEZONE"; op_discrim = "`Timezone"; @@ -539,6 +556,19 @@ let rec argspec () = pr " s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc; pr " ),\n"; pr " Some %S, %S;\n" v longdesc + | { op_type = SSHKeySelector v; op_name = name; op_discrim = discrim; + op_shortdesc = shortdesc; op_pod_longdesc = longdesc } -> + pr " (\n"; + pr " \"--%s\",\n" name; + pr " Arg.String (\n"; + pr " fun s ->\n"; + pr " let user, selstr = string_split \":\" s in\n"; + pr " let sel = Ssh_key.parse_selector selstr in\n"; + pr " ops := %s (user, sel) :: !ops\n" discrim; + pr " ),\n"; + pr " s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v shortdesc; + pr " ),\n"; + pr " Some %S, %S;\n" v longdesc ) ops; List.iter ( @@ -606,6 +636,10 @@ type ops = { op_name = name } -> pr " | %s of string * Password.password_selector\n (* --%s %s *)\n" discrim name v + | { op_type = SSHKeySelector v; op_discrim = discrim; + op_name = name } -> + pr " | %s of string * Ssh_key.ssh_key_selector\n (* --%s %s *)\n" + discrim name v ) ops; pr "]\n"; @@ -631,7 +665,7 @@ let generate_customize_synopsis_pod () = | { op_type = Unit; op_name = n } -> n, sprintf "[--%s]" n | { op_type = String v | StringPair v | StringList v | TargetLinks v - | PasswordSelector v | UserPasswordSelector v; + | PasswordSelector v | UserPasswordSelector v | SSHKeySelector v; op_name = n } -> n, sprintf "[--%s %s]" n v ) ops @ @@ -671,7 +705,7 @@ let generate_customize_options_pod () = | { op_type = Unit; op_name = n; op_pod_longdesc = ld } -> n, sprintf "B<--%s>" n, ld | { op_type = String v | StringPair v | StringList v | TargetLinks v - | PasswordSelector v | UserPasswordSelector v; + | PasswordSelector v | UserPasswordSelector v | SSHKeySelector v; op_name = n; op_pod_longdesc = ld } -> n, sprintf "B<--%s> %s" n v, ld ) ops @ diff --git a/po/POTFILES-ml b/po/POTFILES-ml index a3086eb..b6d88b0 100644 --- a/po/POTFILES-ml +++ b/po/POTFILES-ml @@ -25,6 +25,7 @@ customize/hostname.ml customize/password.ml customize/perl_edit.ml customize/random_seed.ml +customize/ssh_key.ml customize/timezone.ml customize/urandom.ml mllib/JSON.ml diff --git a/sysprep/Makefile.am b/sysprep/Makefile.am index 6553c9c..17fe612 100644 --- a/sysprep/Makefile.am +++ b/sysprep/Makefile.am @@ -102,6 +102,7 @@ deps = \ $(top_builddir)/customize/firstboot.cmx \ $(top_builddir)/customize/perl_edit-c.o \ $(top_builddir)/customize/perl_edit.cmx \ + $(top_builddir)/customize/ssh_key.cmx \ $(top_builddir)/customize/customize_cmdline.cmx \ $(top_builddir)/customize/customize_run.cmx \ $(top_builddir)/fish/guestfish-uri.o \ diff --git a/v2v/Makefile.am b/v2v/Makefile.am index b4bb9cc..9217777 100644 --- a/v2v/Makefile.am +++ b/v2v/Makefile.am @@ -139,6 +139,7 @@ BOBJECTS = \ $(top_builddir)/customize/perl_edit.cmo \ $(top_builddir)/customize/crypt.cmo \ $(top_builddir)/customize/password.cmo \ + $(top_builddir)/customize/ssh_key.cmo \ $(top_builddir)/customize/customize_run.cmo \ $(SOURCES_ML:.ml=.cmo) XOBJECTS = $(BOBJECTS:.cmo=.cmx) -- 1.9.3