Subject: [bug#43106] [PATCH] DRAFT services: childhurd: Support for setting secrets.

From: Ludovic Courtès <[email-masked]>
Date: Sun, 30 Aug 2020 15:44:03 +0200

Hi!

"Jan (janneke) Nieuwenhuizen" <[email-masked]> skribis:

> TODO: This seems to work...but it can keep the shepherd from finishing for
> quite some time (half a minute)...not sure what to do here, WDYT?
>
> A great way to play with it is by doing something like
>
> sudo -E ./pre-inst-env guile -c '(use-modules (gnu build childhurd)) (hurd-vm-copy-secrets 10022 "/etc/childhurd")'
>
> * gnu/build/childhurd.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * gnu/services/virtualization.scm (hurd-vm-shepherd-service): Use it to set
> secrets.
> (hurd-vm-port): New function.
> (hurd-vm-net-options): Use it.
> * doc/guix.texi (The Hurd in a Virtual Machine): Document it.

Nice, thanks for working on it!

> +@item @code{secret-root} (default: @code{#f})
> +If set, the root directory with out-of-band secrets to be injected into
> +the childhurd once it runs.  Childhurds are volatile which means that on
> +every startup, secrets such as the SSH host keys and Guix signing key
> +are recreated.
> +
> +Typical use is setting @code{secret-root} to @code{"/etc/childhurd"}
> +pointing at a tree of non-volatile secrets like so
> +
> +@example
> +/etc/childhurd/etc/guix/signing-key.pub
> +/etc/childhurd/etc/guix/signing-key.sec
> +/etc/childhurd/etc/ssh/ssh_host_ed25519_key
> +/etc/childhurd/etc/ssh/ssh_host_ecdsa_key
> +/etc/childhurd/etc/ssh/ssh_host_ed25519_key.pub
> +/etc/childhurd/etc/ssh/ssh_host_ecdsa_key.pub
> +@end example

Would it make sense to have a list of source/target pairs instead of a
directory:

  (("/etc/childhurd/pubkey" . "/etc/guix/signing-key.pub")
   …)

?

[...]

> +(define-module (gnu build childhurd)
> +  #:use-module (ice-9 rdelim)
> +  #:use-module (guix build utils)
> +
> +  ;; #:use-module (ssh auth)
> +  ;; #:use-module (ssh channel)
> +  ;; #:use-module (ssh session)
> +  ;; #:use-module (ssh sftp)
> +
> +  #:autoload (ssh auth) (userauth-password!)

You could add the file to MODULES_NOT_COMPILED in gnu/local.mk to avoid
the autoload dance.

> +(define* (hurd-vm-copy-secrets port secret-root #:key (retry 20))
> +  "Copy all files under SECRET-ROOT using ssh to childhurd at local PORT."
> +  (format (current-error-port) "hurd-vm-copy-secrets\n")
> +  (let ((session (make-session #:host "127.0.0.1" #:port port
> +                               #:user "root")))

I just realized that we have a bootstrapping issue here: we have to
explicitly skip SSH host authentication because we haven’t installed the
host keys yet.

The boot sequence of the guest is actually: generate SSH host keys,
start sshd, receive host keys over SFTP.

[...]

> -      (start #~(make-forkexec-constructor #$vm-command))
> +      (requirement '(loopback networking user-processes))
> +      (start
> +       (with-imported-modules (source-module-closure '((gnu build childhurd)
> +                                                       (guix build utils)))
> +        (with-extensions (list guile-ssh)
> +          #~(let ((spawn (make-forkexec-constructor #$vm-command)))
> +              (use-modules (gnu build childhurd))

We should use the ‘modules’ field of <shepherd-service> instead of a
non-top-level ‘use-modules’.

> +              (lambda _
> +                (let ((pid (spawn))
> +                      (port #$(hurd-vm-port config %hurd-vm-ssh-port))
> +                      (root #$(hurd-vm-configuration-secret-root config)))
> +                  (when (and root (directory-exists? root))
> +                    (catch #t
> +                      (lambda _
> +                        (hurd-vm-copy-secrets port root))
> +                      (lambda (key . args)
> +                        (format (current-error-port) "childhurd: ~a ~s\n" key args))))

To avoid race conditions, we probably have to wait until PORT becomes
available, no?  Also, the VM boots even if we’ve failed to inject the
secrets, right?

As discussed on IRC, attached is my attempt at addressing this problem:
the guest would run an activation snippet early on to receive secret
files over raw unauthenticated TCP, blocking until it has received them.
What’s missing from this patch is the host side that actually connects
to the guest and sends this file.

I think it has the advantage of failing in case the secrets haven’t been
installed and it avoids the SSH host key bootstrapping issue.  (It has
at least the disadvantage of not being fully implemented.  :-))  Also,
longer term, it would allow us to not force password-less root
authentication in the VM.

I’m tempted to go the raw TCP way; WDYT?  We can pair-hack on it if you
feel like it!

Thanks,
Ludo’.

modified   gnu/services/virtualization.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2017 Ryan Moe <[email-masked]>
-;;; Copyright © 2018 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2018, 2020 Ludovic Courtès <[email-masked]>
 ;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -804,6 +804,93 @@ given QEMU package."
 compiled for other architectures using QEMU and the @code{binfmt_misc}
 functionality of the kernel Linux.")))
 
+
+;;;
+;;; Secrets for guest VMs.
+;;;
+
+(define (secret-service-activation port)
+  "Return an activation snippet that fetches sensitive material at PORT, over
+TCP."
+  (with-imported-modules '((guix build utils))
+    #~(begin
+        (use-modules (guix build utils)
+                     (rnrs io ports)
+                     (rnrs bytevectors)
+                     (ice-9 match))
+
+        (define (wait-for-client port)
+          ;; Wait for a TCP connection on PORT.  Note: We cannot use the
+          ;; virtio-serial ports, which would be safer, because they are
+          ;; (presumably) unsupported on GNU/Hurd.
+          (let ((sock (socket AF_INET SOCK_STREAM 0)))
+            (bind sock AF_INET INADDR_ANY port)
+            (listen sock 1)
+            (format (current-error-port)
+                    "waiting for secrets on port ~a...~%"
+                    port)
+            (match (accept sock)
+              ((client . address)
+               (format (current-error-port) "client connection from ~a~%"
+                       (inet-ntop (sockaddr:fam address)
+                                  (sockaddr:addr address)))
+               (close-port sock)
+               client))))
+
+        ;; TODO: Remove when (@ (guix build utils) dump-port) has a 'size'
+        ;; parameter.
+        (define (dump in out size)
+          ;; Copy SIZE bytes from IN to OUT.
+          (define buf-size 65536)
+          (define buf (make-bytevector buf-size))
+
+          (let loop ((left size))
+            (if (<= left 0)
+                0
+                (let ((read (get-bytevector-n! in buf 0 (min left buf-size))))
+                  (if (eof-object? read)
+                      left
+                      (begin
+                        (put-bytevector out buf 0 read)
+                        (loop (- left read))))))))
+
+        (define (read-secrets port)
+          ;; Read secret files from PORT and install them.
+          (match (false-if-exception (read port))
+            (('secrets ('version 0)
+                       ('files ((files sizes modes) ...)))
+             (for-each (lambda (file size mode)
+                         (format (current-error-port)
+                                 "installing file '~a' (~a bytes)...~%"
+                                 file size)
+                         (mkdir-p (dirname file))
+                         (call-with-output-file file
+                           (lambda (output)
+                             (dump port output size)
+                             (chmod file mode))))
+                       files sizes modes))
+            (_
+             (format (current-error-port)
+                     "invalid secrets received~%")
+             (sleep 3)
+             (reboot))))
+
+        (let ((port (wait-for-client #$port)))
+          (read-secrets port)
+          (close-port port))))
+
+  (computed-file "secret-service-client" install-secrets))
+
+(define secret-service-type
+  (service-type
+   (name 'secret-service)
+   (extensions (list (service-extension activation-service-type
+                                        secret-service-activation)))
+   (description
+    "This service fetches secret key and other sensitive material over TCP at
+boot time.  This service is meant to be used by virtual machines (VMs) that
+can only be accessed by their host.")))
+
 
 ;;;
 ;;; The Hurd in VM service: a Childhurd.
@@ -819,6 +906,8 @@ functionality of the kernel Linux.")))
                  (target "/dev/vda")
                  (timeout 0)))
     (services (cons*
+               ;; Receive secret keys on port 5900, TCP.
+               (service secret-service-type 5900)
                (service openssh-service-type
                         (openssh-configuration
                          (openssh openssh-sans-x)
modified   gnu/system/examples/bare-hurd.tmpl
@@ -41,14 +41,16 @@
     (host-name "guixygnu")
     (timezone "Europe/Amsterdam")
     (packages (cons openssh-sans-x %base-packages/hurd))
-    (services (cons (service openssh-service-type
-                             (openssh-configuration
-                              (openssh openssh-sans-x)
-                              (use-pam? #f)
-                              (port-number 2222)
-                              (permit-root-login #t)
-                              (allow-empty-passwords? #t)
-                              (password-authentication? #t)))
-               %base-services/hurd))))
+    (services (append (list (service openssh-service-type
+                                     (openssh-configuration
+                                      (openssh openssh-sans-x)
+                                      (use-pam? #f)
+                                      (port-number 2222)
+                                      (permit-root-login #t)
+                                      (allow-empty-passwords? #t)
+                                      (password-authentication? #t)))
+                            (service (@@ (gnu services virtualization)
+                                         secret-service-type)))
+                      %base-services/hurd))))
 
 %hurd-os