aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020-2023 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix 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 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu build secret-service)
  #:use-module (guix build utils)

  #:use-module (srfi srfi-26)
  #:use-module (rnrs bytevectors)
  #:use-module (ice-9 binary-ports)
  #:use-module (ice-9 match)
  #:use-module (ice-9 rdelim)

  #:export (secret-service-receive-secrets
            secret-service-send-secrets))

;;; Commentary:
;;;
;;; Utility procedures for copying secrets into a VM.
;;;
;;; Code:

(define-syntax log
  (lambda (s)
    "Log the given message."
    (syntax-case s ()
      ((_ fmt args ...)
       (with-syntax ((fmt (string-append "secret service: "
                                         (syntax->datum #'fmt))))
         ;; Log to the current output port.  That way, when
         ;; 'secret-service-send-secrets' is called from shepherd, output goes
         ;; to syslog.
         #'(format (current-output-port) fmt args ...))))))

(define-syntax with-modules
  (syntax-rules ()
    "Dynamically load the given MODULEs at run time, making the chosen
bindings available within the lexical scope of BODY."
    ((_ ((module #:select (bindings ...)) rest ...) body ...)
     (let* ((iface (resolve-interface 'module))
            (bindings (module-ref iface 'bindings))
            ...)
       (with-modules (rest ...) body ...)))
    ((_ () body ...)
     (begin body ...))))

(define (wait-for-readable-fd port timeout)
  "Wait until PORT has data available for reading or TIMEOUT has expired.
Return #t in the former case and #f in the latter case."
  (match (resolve-module '(fibers) #f #:ensure #f) ;using Fibers?
    (#f
     (log "blocking on socket...~%")
     (match (select (list port) '() '() timeout)
       (((_) () ()) #t)
       ((() () ())  #f)))
    (fibers
     ;; We're running on the Shepherd 0.9+ with Fibers.  Arrange to make a
     ;; non-blocking wait so that other fibers can be scheduled in while we
     ;; wait for PORT.
     (with-modules (((fibers) #:select (spawn-fiber sleep))
                    ((fibers channels)
                     #:select (make-channel put-message get-message)))
       ;; Make PORT non-blocking.
       (let ((flags (fcntl port F_GETFL)))
         (fcntl port F_SETFL (logior O_NONBLOCK flags)))

       (let ((channel (make-channel)))
         (spawn-fiber
          (lambda ()
            (sleep timeout)                       ;suspends the fiber
            (put-message channel 'timeout)))
         (spawn-fiber
          (lambda ()
            (lookahead-u8 port)                   ;suspends the fiber
            (put-message channel 'readable)))
         (log "suspending fiber on socket...~%")
         (match (get-message channel)
           ('readable #t)
           ('timeout  #f)))))))

(define (socket-address->string address)
  "Return a human-readable representation of ADDRESS, an object as returned by
'make-socket-address'."
  (let ((family (sockaddr:fam address)))
    (cond ((= AF_INET family)
           (string-append (inet-ntop AF_INET (sockaddr:addr address))
                          ":" (number->string (sockaddr:port address))))
          ((= AF_INET6 family)
           (string-append "[" (inet-ntop AF_INET6 (sockaddr:addr address)) "]"
                          ":" (number->string (sockaddr:port address))))
          ((= AF_UNIX family)
           (sockaddr:path address))
          (else
           (object->string address)))))

(define* (secret-service-send-secrets address secret-root
                                      #:key (retry 60)
                                      (handshake-timeout 180))
  "Copy all files under SECRET-ROOT by connecting to secret-service listening
at ADDRESS, an address as returned by 'make-socket-address'.  If connection
fails, sleep 1s and retry RETRY times; once connected, wait for at most
HANDSHAKE-TIMEOUT seconds for handshake to complete.  Return #f on failure."
  (define (file->file+size+mode file-name)
    (let ((stat (stat file-name))
          (target (substring file-name (string-length secret-root))))
      (list target (stat:size stat) (stat:mode stat))))

  (define (send-files sock)
    (let* ((files (if secret-root (find-files secret-root) '()))
           (files-sizes-modes (map file->file+size+mode files))
           (secrets `(secrets
                      (version 0)
                      (files ,files-sizes-modes))))
      (write secrets sock)
      (for-each (lambda (file)
                  (call-with-input-file file
                    (lambda (input)
                      (dump-port input sock))))
                files)))

  (log "sending secrets to ~a~%" (socket-address->string address))

  (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0))
        (sleep (if (resolve-module '(fibers) #f)
                   (module-ref (resolve-interface '(fibers)) 'sleep)
                   sleep)))
    ;; Connect to QEMU on the forwarded port.  The 'connect' call succeeds as
    ;; soon as QEMU is ready, even if there's no server listening on the
    ;; forward port inside the guest.
    (let loop ((retry retry))
      (catch 'system-error
        (cute connect sock address)
        (lambda (key . args)
          (when (zero? retry)
            (apply throw key args))
          (log "retrying connection [~a attempts left]~%"
               (- retry 1))
          (sleep 1)
          (loop (1- retry)))))

    (log "connected; waiting for handshake...~%")

    ;; Wait for "hello" message from the server.  This is the only way to know
    ;; that we're really connected to the server inside the guest.
    (if (wait-for-readable-fd sock handshake-timeout)
        (match (read sock)
          (('secret-service-server ('version version ...))
           (log "sending files from ~s...~%" secret-root)
           (send-files sock)
           (log "done sending files to ~a~%"
                (socket-address->string address))
           (close-port sock)
           secret-root)
          (x
           (log "invalid handshake ~s~%" x)
           (close-port sock)
           #f))
        (begin                                    ;timeout
         (log "timeout while sending files to ~a~%"
              (socket-address->string address))
         (close-port sock)
         #f))))

(define (delete-file* file)
  "Ensure FILE does not exist."
  (catch 'system-error
    (lambda ()
      (delete-file file))
    (lambda args
      (unless (= ENOENT (system-error-errno args))
        (apply throw args)))))

(define (secret-service-receive-secrets address)
  "Listen to ADDRESS, an address returned by 'make-socket-address', and wait
for a secret service client to send secrets.  Write them to the file system.
Return the list of files installed on success, and #f otherwise."

  (define (wait-for-client address)
    ;; Wait for a connection on ADDRESS.  Note: virtio-serial ports are safer
    ;; than TCP connections but they are (presumably) unsupported on GNU/Hurd.
    (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0)))
      (bind sock address)
      (listen sock 1)
      (log "waiting for secrets on ~a...~%"
           (socket-address->string address))

      (match (select (list sock) '() '() 60)
        (((_) () ())
         (match (accept sock)
           ((client . address)
            (log "client connection from ~a~%"
                 (inet-ntop (sockaddr:fam address)
                            (sockaddr:addr address)))

            ;; Send a "hello" message.  This allows the client running on the
            ;; host to know that it's now actually connected to server running
            ;; in the guest.
            (write '(secret-service-server (version 0)) client)
            (force-output client)
            (close-port sock)
            client)))
        ((() () ())
         (log "did not receive any secrets; time out~%")
         (close-port sock)
         #f))))

  ;; 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)
                   (log "installing file '~a' (~a bytes)...~%"
                        file size)
                   (mkdir-p (dirname file))

                   ;; It could be that FILE already exists, for instance
                   ;; because it has been created by a service's activation
                   ;; snippet (e.g., SSH host keys).  Delete it.
                   (delete-file* file)

                   (call-with-output-file file
                     (lambda (output)
                       (dump port output size)
                       (chmod file mode))))
                 files sizes modes)
       (log "received ~a secret files~%" (length files))
       files)
      (_
       (log "invalid secrets received~%")
       #f)))

  (let* ((port   (wait-for-client address))
         (result (and=> port read-secrets)))
    (when port
      (close-port port))
    result))

;;; Local Variables:
;;; eval: (put 'with-modules 'scheme-indent-function 1)
;;; End:

;;; secret-service.scm ends here
nfiguration): Likewise. * gnu/services/file-sharing.scm (transmission-daemon-configuration): Likewise. * gnu/services/getmail.scm (getmail-configuration): Likewise. * gnu/services/mail.scm (dovecot-configuration) (opaque-dovecot-configuration): Likewise. * gnu/services/messaging.scm (prosody-configuration) (opaque-prosody-configuration): Likewise. * gnu/services/monitoring.scm (zabbix-server-configuration) (zabbix-agent-configuration): Likewise. * gnu/services/networking.scm (opendht-configuration): Likewise. * gnu/services/pm.scm (tlp-configuration): Likewise. * gnu/services/telephony.scm (jami-configuration): Likewise. * gnu/services/virtualization.scm (libvirt-configuration) (qemu-guest-agent-configuration): Likewise. * gnu/services/vpn.scm (openvpn-client-configuration): Likewise. Tobias Geerinckx-Rice 2021-11-15gnu: hpcguix-web: Update to 0.2.0....* gnu/packages/web.scm (hpcguix-web): Update to 0.2.0. [arguments]: In 'wrap-program' phase, add guile-zlib to DEPS. [native-inputs]: Add GUILE. [inputs]: Add GUILE-ZLIB, GUILE-COMMONMARK, and GUILE-JSON. [propagated-inputs]: Remove. These were pointless. * gnu/services/web.scm (<hpcguix-web-configuration>)[address, port]: New fields. * doc/guix.texi (Web Services): Document them. * gnu/tests/web.scm (%hpcguix-web-os): Add 'address'. Ludovic Courtès 2021-09-25gnu: Consolidate duplicate copyright names....* gnu/packages/android.scm: Consolidate copyright lines with the same email address. * gnu/packages/code.scm: Likewise. * gnu/packages/cpp.scm: Likewise. * gnu/packages/databases.scm: Likewise. * gnu/packages/emacs-xyz.scm: Likewise. * gnu/packages/file.scm: Likewise. * gnu/packages/freedesktop.scm: Likewise. * gnu/packages/gl.scm: Likewise. * gnu/packages/gps.scm: Likewise. * gnu/packages/linux.scm: Likewise. * gnu/packages/networking.scm: Likewise. * gnu/packages/python-xyz.scm: Likewise. * gnu/packages/python.scm: Likewise. * gnu/packages/video.scm: Likewise. * gnu/packages/xdisorg.scm: Likewise. * gnu/services/web.scm: Likewise. Signed-off-by: Liliana Marie Prikler <liliana.prikler@gmail.com> Greg Hogan 2021-07-23gnu: hpcguix-web: Update to 0.1.0....* gnu/packages/web.scm (hpcguix-web): Update to 0.1.0. [arguments]: In 'wrap-program' phase, change "/bin/run" to "/bin/hpcguix-web". [inputs]: Add BASH-MINIMAL. * gnu/services/web.scm (hpcguix-web-shepherd-service): Run "/bin/hpcguix-web". Ludovic Courtès 2021-04-01gnu: services: web: Have fcgiwrap log to a file....This makes it easier to access the output. * gnu/services/web.scm (fcgiwrap-shepherd-service): Pass #:log-file to make-forkexec-constructor. Christopher Baines 2021-04-01gnu: services: web: Cleanup whitespace....Replace tabs with spaces, and delete trailing whitespace. * gnu/services/web.scm: Cleanup whitespace. Christopher Baines 2021-02-15services: Add Agate Gemini service....* gnu/services/web.scm (<agate-configuration>): New record type. (agate-accounts, agate-shepherd-service): New procedures. (agate-service-type): New variable. * doc/guix.texi (Web Services): Document it. Signed-off-by: Nicolas Goaziou <mail@nicolasgoaziou.fr> Alexandru-Sergiu Marton 2021-01-17gnu: php-fpm: Ensure no duplicate group....* gnu/services/web.scm (php-fpm-accounts): Ensure `php-fpm` group is not duplicated. Signed-off-by: Leo Prikler <leo.prikler@student.tugraz.at> Julien Lepiller 2048647debbaa58a6'>services: dhcp-client: Deprecate 'dhcp-client-service' procedure....* gnu/services/networking.scm (dhcp-client-service-type): Add default value. * gnu/system/examples/bare-bones.tmpl: Use (service dhcp-client-service-type) instead of (dhcp-client-service). * gnu/system/examples/beaglebone-black.tmpl: Likewise. * gnu/tests/base.scm (%avahi-os): Likewise. * gnu/tests/databases.scm (%memcached-os): Likewise. (%mongodb-os): Likewise. * gnu/tests/dict.scm (%dicod-os): Likewise. * gnu/tests/mail.scm (%opensmtpd-os): Likewise. (%exim-os): Likewise. (%dovecot-os): Likewise. * gnu/tests/messaging.scm (run-xmpp-test): Likewise. (run-bitlbee-test): Likewise. * gnu/tests/monitoring.scm (%prometheus-node-exporter-os): Likewise. * gnu/tests/networking.scm (%inetd-os): Likewise. (run-iptables-test): Likewise. * gnu/tests/nfs.scm (%base-os): Likewise. * gnu/tests/rsync.scm (%rsync-os): Likewise. * gnu/tests/ssh.scm (run-ssh-test): Likewise. * gnu/tests/version-control.scm (%cgit-os): Likewise. (%git-http-os): Likewise. (%gitolite-os): Likewise. * gnu/tests/virtualization.scm (%libvirt-os): Likewise. * gnu/tests/web.scm (%httpd-os): Likewise. (%nginx-os): Likewise. (%varnish-os): Likewise. (%php-fpm-os): Likewise. (%hpcguix-web-os): Likewise. (%tailon-os): Likewise. * tests/guix-system.sh: Likewise. * doc/guix.texi (Networking Services): Document 'dhcp-client-service-type' and remove 'dhcp-client-service'. Ludovic Courtès 2018-07-05doc: Improve UEFI/BIOS bootloader documentation....Partly fixes <https://bugs.gnu.org/30312>. * doc/guix.texi (Preparing for Installation): Add note on how to choose between UEFI and BIOS. (Using the Configuration System)[Bootloader]: New subsubsection. (Bootloader Configuration): Expound on the bootloader type and target. * gnu/system/examples/desktop.tmpl: Switch to UEFI. * gnu/system/examples/bare-bones.tmpl: Explicitly mention "legacy" and "BIOS" in the comments. Ludovic Courtès 2018-05-28file-systems: Remove 'title' field and add <file-system-label>....The 'title' field was easily overlooked and was an endless source of confusion. Now, the value of the 'device' field is self-contained. * gnu/system/file-systems.scm (<file-system>): Change constructor name to '%file-system'. [title]: Remove. (<file-system-label>): New record type with printer. (report-deprecation, device-expression) (process-file-system-declaration, file-system): New macros. (file-system-title): New procedure. (file-system->spec, spec->file-system): Adjust to handle <file-system-label>. * gnu/system.scm (bootable-kernel-arguments): Add case for 'file-system-label?'. (read-boot-parameters): Likewise. (mapped-device-user): Avoid 'file-system-title'. (fs->boot-device): Remove. (operating-system-boot-parameters): Use 'file-system-device' instead of 'fs->boot-device'. (device->sexp): Add case for 'file-system-label?'. * gnu/bootloader/grub.scm (grub-root-search): Add case for 'file-system-label?'. * gnu/system/examples/bare-bones.tmpl, gnu/system/examples/beaglebone-black.tmpl, gnu/system/examples/lightweight-desktop.tmpl, gnu/system/examples/vm-image.tmpl: Remove uses of 'title'. * gnu/system/vm.scm (virtualized-operating-system): Remove uses of 'file-system-title'. * guix/scripts/system.scm (check-file-system-availability): Likewise, and adjust fix-it hint. (check-initrd-modules)[file-system-/dev]: Likewise. * gnu/build/file-systems.scm (canonicalize-device-spec): Remove 'title' parameter. [canonical-title]: Remove. Match on SPEC's type rather than on CANONICAL-TITLE. (mount-file-system): Adjust caller. * gnu/build/linux-boot.scm (boot-system): Interpret ROOT here. * gnu/services/base.scm (file-system->fstab-entry): Remove use of 'file-system-title'. * doc/guix.texi (File Systems): Remove documentation of the 'title' field. Rewrite documentation of 'device' and document 'file-system-label'. Ludovic Courtès 2018-03-24system: Add "guix system docker-image" command....* gnu/system/vm.scm (system-docker-image): New procedure. * guix/scripts/system.scm (system-derivation-for-action): Add a case for docker-image, and in that case, call system-docker-image. (show-help): Document docker-image. (guix-system): Parse arguments for docker-image. * doc/guix.texi (Invoking guix system): Document "guix system docker-image". * gnu/system/examples/docker-image.tmpl: New file. Chris Marusich 2018-03-03system: beaglebone-black: Use 'initrd-modules'....* gnu/system/examples/beaglebone-black.tmpl: Use 'initrd-modules' instead of 'initrd'. Ludovic Courtès 2018-01-21doc: No longer mention Wicd in OS examples....* gnu/system/examples/desktop.tmpl, gnu/system/examples/lightweight-desktop.tmpl: Mention NetworkManager instead of Wicd. Ludovic Courtès 2017-12-22system: examples: Add missing initrd to beaglebone-black.tmpl....* gnu/system/examples/beaglebone-black.tmpl (operating-system): Add the initrd with "omap_hsmmc" as an extra-module. Mathieu Othacehe 2017-12-18system: examples: Add a template for BeagleBone Black....* gnu/system/examples/beaglebone-black.tmpl: New file. * Makefile.am (EXAMPLES): Add it. * gnu/system/install.scm (/etc/configuration-files): Add it. Mathieu Othacehe 2017-12-06gnu, doc, tests: Use ‘bootloader-configuration’ everywhere....* doc/guix.texi (Proceeding with the Installation): Replace the old-style ‘grub-configuration’ with the newer ‘bootloader-configuration’ syntax. * gnu/system/examples/vm-image.tmpl: Likewise. * gnu/system/install.scm (installation-os): Likewise. * gnu/tests.scm (%simple-os): Likewise. * gnu/tests/install.scm (%minimal-os, %minimal-os-on-vda, %separate-home-os) (%separate-store-os, %raid-root-os, %encrypted-root-os, %btrfs-root-os): Likewise. * gnu/tests/nfs.scm (%base-os): Likewise. * tests/guix-system.scm (OS_BASE, make_user_config): Likewise. * tests/system.scm (%os, %os-with-mapped-device): Likewise. Tobias Geerinckx-Rice 2017-10-11doc: Give an example with a FAT UUID....* gnu/system/examples/lightweight-desktop.tmpl <file-systems>: Add a UUID for the /boot/efi partition. * doc/guix.texi (Using the Configuration System): Mention it. Ludovic Courtès 2017-09-12doc: Use Screen and OpenSSH in the bare-bones example....* gnu/system/examples/bare-bones.tmpl (packages): Remove TCPDUMP; add SCREEN and OPENSSH. * doc/guix.texi (Using the Configuration System): Adjust explanation accordingly. Ludovic Courtès 2017-08-23gnu: bootloader: Deprecate "device" field in favor of "target"....* gnu/bootloader.scm (<bootloader-configuration>): Deprecate "device" field in favor of "target" field. This is mostly a renaming but also a generalization to support UEFI targets being paths to a mounted partition instead of a device name. * gnu/system/examples/bare-bones.tmpl: * gnu/system/examples/desktop.tmpl: * gnu/system/examples/lightweight-desktop.tmpl: * gnu/system/examples/vm-image.tmpl: * gnu/system/install.scm: * gnu/tests.scm: * gnu/tests/install.scm: * gnu/tests/nfs.scm: * tests/system.scm: Adapt all invocations of bootloader-configuration. * guix/scripts/system.scm (perform-action): Rename device argument to bootloader-target. (process-action): Adapt caller. * doc/guix.texi (Proceeding with the Installation): * doc/guix.texi (Bootloader Configuration): Update documentation. Andy Wingo 2017-08-20gnu: grub-efi-bootloader: Specialize grub-install invocation....* gnu/bootloader/grub.scm (install-grub-efi): Fix grub-install invocation for EFI systems. * gnu/system/examples/bare-bones.tmpl: Use the newer "bootloader-configuration" syntax. * gnu/system/examples/desktop.tmpl: Use bootloader-configuration sytax. Also, use the same label for the LUKS-mapped device and the root partition. Remove unneeded "title" field for the file-system based on LUKS; as noted in the manual, the "title" field is ignored for mapped devices. * gnu/system/examples/lightweight-desktop.tmpl: Use bootloader-configuration, and use grub-efi-bootloader. Andy Wingo