~ruther/guix-local

e2583b5a17bfdedc1d24b3bab2d752fbf8fa6db6 — Ludovic Courtès 1 year, 6 months ago 2c7c059
services: guix: Allow ‘guix-daemon’ to run without root privileges.

* gnu/services/base.scm (run-with-writable-store)
(guix-ownership-change-program): New procedures.
(<guix-configuration>)[privileged?]: New field.
(guix-shepherd-service): Rename to…
(guix-shepherd-services): … this.   Add the ‘guix-ownership’ service.
Change ‘guix-daemon’ service to depend on it; when unprivileged,
prefix ‘daemon-command’ by ‘run-with-writable-store’ and
omit ‘--build-users-group’; adjust socket activation endpoints.
(guix-accounts): When unprivileged, create the “guix-daemon” user and
group in addition to the others.
(guix-service-type)[extensions]: Adjust to name change.
* gnu/tests/base.scm (run-guix-daemon-test): Add ‘name’ parameter.
(%test-guix-daemon): Adjust accordingly.
(%test-guix-daemon-unprivileged): New test.
* doc/guix.texi (Base Services): Document ‘privileged?’.
(Migrating to the Unprivileged Daemon): Explain that this is automatic
on Guix System.

Reviewed-by: Maxim Cournoyer <maxim.cournoyer@gmail.com>
Change-Id: I28a9a22e617416c551dccb24e43a253b544ba163
3 files changed, 254 insertions(+), 21 deletions(-)

M doc/guix.texi
M gnu/services/base.scm
M gnu/tests/base.scm
M doc/guix.texi => doc/guix.texi +38 -0
@@ 1037,6 1037,14 @@ number of steps must be taken: creating a new dedicated
files to @code{guix-daemon}, and ensuring that the @command{guix-daemon}
program runs as @code{guix-daemon}.

On Guix System, these steps are carried out automatically when you set
the @code{privileged?} field of the @code{guix-configuration} record to
@code{#f} and reconfigure (@pxref{guix-configuration-type,
@code{guix-configuration}}).

However, on a foreign distribution, the process is manual.  The
following paragraphs describe what you need to do.

@quotation Warning
Follow the instructions below only after making sure you have a recent
version of @command{guix-daemon} with support for unprivileged


@@ 20109,6 20117,36 @@ This data type represents the configuration of the Guix build daemon.
The Guix package to use.  @xref{Customizing the System-Wide Guix} to
learn how to provide a package with a pre-configured set of channels.

@cindex unprivileged @command{guix-daemon}
@cindex rootless @command{guix-daemon}
@item @code{privileged?} (default: @code{#t})
Whether to run @command{guix-daemon} as root.

When true, @command{guix-daemon} runs with root privileges and build
processes run under unprivileged user accounts as specified by
@code{build-group} and @code{build-accounts} (see below); when false,
@command{guix-daemon} run as the @code{guix-daemon} user, which is
unprivileged, and so do build processes.  The unprivileged or
``rootless'' mode can reduce the impact of some classes of
vulnerabilities that could affect the daemon.

The default is currently @code{#t} (@command{guix-daemon} runs with root
privileges) but may eventually be changed to @code{#f}.

@quotation Warning
When changing this option, @file{/gnu/store}, @file{/var/guix}, and
@file{/etc/guix} have their ownership automatically changed by the
@code{guix-ownership} service to either the @code{guix-daemon} user or
the @code{root} user (@pxref{unprivileged-daemon-migration}).

This can take a while, especially if @file{/gnu/store} is big; it cannot
be interrupted and @command{guix-daemon} cannot be used until it has
completed.
@end quotation

@xref{Build Environment Setup}, for more information on the two ways to
run @command{guix-daemon}.

@item @code{build-group} (default: @code{"guixbuild"})
Name of the group for build user accounts.


M gnu/services/base.scm => gnu/services/base.scm +180 -16
@@ 1918,6 1918,102 @@ archive' public keys, with GUIX."
                                   #$machines))
                 machines-file))))

(define (run-with-writable-store)
  "Return a wrapper that runs the given command under the specified UID and
GID in a context where the store is writable, even if it was bind-mounted
read-only via %IMMUTABLE-STORE (this wrapper must run as root)."
  (program-file "run-with-writable-store"
                (with-imported-modules (source-module-closure
                                        '((guix build syscalls)))
                  #~(begin
                      (use-modules (guix build syscalls)
                                   (ice-9 match))

                      (define (ensure-writable-store store)
                        ;; Create a new mount namespace and remount STORE with
                        ;; write permissions if it's read-only.
                        (unshare CLONE_NEWNS)
                        (let ((fs (statfs store)))
                          (unless (zero? (logand (file-system-mount-flags fs)
                                                 ST_RDONLY))
                            (mount store store "none"
                                   (logior MS_BIND MS_REMOUNT)))))

                      (match (command-line)
                        ((_ user group command args ...)
                         (ensure-writable-store #$(%store-prefix))
                         (let ((uid (or (string->number user)
                                        (passwd:uid (getpwnam user))))
                               (gid (or (string->number group)
                                        (group:gid (getgrnam group)))))
                           (setgroups #())
                           (setgid gid)
                           (setuid uid)
                           (apply execl command command args))))))))

(define (guix-ownership-change-program)
  "Return a program that changes ownership of the store and other data files
of Guix to the given UID and GID."
  (program-file
   "validate-guix-ownership"
   (with-imported-modules (source-module-closure
                           '((guix build utils)))
     #~(begin
         (use-modules (guix build utils)
                      (ice-9 ftw)
                      (ice-9 match))

         (define (lchown file uid gid)
           (let ((parent (open (dirname file) O_DIRECTORY)))
             (chown-at parent (basename file) uid gid
                       AT_SYMLINK_NOFOLLOW)
             (close-port parent)))

         (define (change-ownership directory uid gid)
           ;; chown -R UID:GID DIRECTORY
           (file-system-fold (const #t)                              ;enter?
                             (lambda (file stat result)              ;leaf
                               (if (eq? 'symlink (stat:type stat))
                                   (lchown file uid gid)
                                   (chown file uid gid)))
                             (const #t)           ;down
                             (lambda (directory stat result) ;up
                               (chown directory uid gid))
                             (const #t)           ;skip
                             (lambda (file stat errno result)
                               (format (current-error-port)
                                       "i/o error: ~a: ~a~%"
                                       file (strerror errno))
                               #f)
                             #t                   ;seed
                             directory
                             lstat))

         (define (claim-data-ownership uid gid)
           (format #t "Changing file ownership for /gnu/store \
and data directories to ~a:~a...~%"
                   uid gid)
           (change-ownership #$(%store-prefix) uid gid)
           (let ((excluded '("." ".." "profiles" "userpool")))
             (for-each (lambda (directory)
                         (change-ownership (in-vicinity "/var/guix" directory)
                                           uid gid))
                       (scandir "/var/guix"
                                (lambda (file)
                                  (not (member file
                                               excluded))))))
           (chown "/var/guix" uid gid)
           (change-ownership "/etc/guix" uid gid)
           (mkdir-p "/var/log/guix")
           (change-ownership "/var/log/guix" uid gid))

         (match (command-line)
           ((_ (= string->number (? integer? uid))
               (= string->number (? integer? gid)))
            (setlocale LC_ALL "C.UTF-8")          ;for file name decoding
            (setvbuf (current-output-port) 'line)
            (claim-data-ownership uid gid)))))))

(define-record-type* <guix-configuration>
  guix-configuration make-guix-configuration
  guix-configuration?


@@ 1959,6 2055,8 @@ archive' public keys, with GUIX."
                    (default #f))
  (tmpdir           guix-tmpdir                   ;string | #f
                    (default #f))
  (privileged?      guix-configuration-privileged?
                    (default #t))
  (build-machines   guix-configuration-build-machines ;list of gexps | '()
                    (default '()))
  (environment      guix-configuration-environment  ;list of strings


@@ 2021,7 2119,7 @@ proxy of 'guix-daemon'...~%")
                    (environ environment)
                    #t)))))

(define (guix-shepherd-service config)
(define (guix-shepherd-services config)
  "Return a <shepherd-service> for the Guix daemon service with CONFIG."
  (define locales
    (let-system (system target)


@@ 2030,16 2128,57 @@ proxy of 'guix-daemon'...~%")
          glibc-utf8-locales)))

  (match-record config <guix-configuration>
    (guix build-group build-accounts chroot? authorize-key? authorized-keys
    (guix privileged?
          build-group build-accounts chroot? authorize-key? authorized-keys
          use-substitutes? substitute-urls max-silent-time timeout
          log-compression discover? extra-options log-file
          http-proxy tmpdir chroot-directories environment
          socket-directory-permissions socket-directory-group
          socket-directory-user)
    (list (shepherd-service
           (provision '(guix-ownership))
           (requirement '(user-processes user-homes))
           (one-shot? #t)
           (start #~(lambda ()
                      (let* ((store #$(%store-prefix))
                             (stat (lstat store))
                             (privileged? #$(guix-configuration-privileged?
                                             config))
                             (change-ownership #$(guix-ownership-change-program))
                             (with-writable-store #$(run-with-writable-store)))
                        ;; Check whether we're switching from privileged to
                        ;; unprivileged guix-daemon, or vice versa, and adjust
                        ;; file ownership accordingly.  Spawn a child process
                        ;; if and only if something needs to be changed.
                        ;;
                        ;; Note: This service remains in 'starting' state for
                        ;; as long as CHANGE-OWNERSHIP is running.  That way,
                        ;; 'guix-daemon' starts only once we're done.
                        (cond ((and (not privileged?)
                                    (or (zero? (stat:uid stat))
                                        (zero? (stat:gid stat))))
                               (let ((user (getpwnam "guix-daemon")))
                                 (format #t "Changing to unprivileged guix-daemon.~%")
                                 (zero?
                                  (system* with-writable-store "0" "0"
                                           change-ownership
                                           (number->string (passwd:uid user))
                                           (number->string (passwd:gid user))))))
                              ((and privileged?
                                    (and (not (zero? (stat:uid stat)))
                                         (not (zero? (stat:gid stat)))))
                               (format #t "Changing to privileged guix-daemon.~%")
                               (zero? (system* with-writable-store "0" "0"
                                               change-ownership "0" "0")))
                              (else #t)))))
           (documentation "Ensure that the store and other data files used by
guix-daemon have the right ownership."))

          (shepherd-service
           (documentation "Run the Guix daemon.")
           (provision '(guix-daemon))
           (requirement `(user-processes
                          guix-ownership
                          ,@(if discover? '(avahi-daemon) '())))
           (actions (list shepherd-set-http-proxy-action
                          shepherd-discover-action))


@@ 2063,8 2202,15 @@ proxy of 'guix-daemon'...~%")
                    (or (getenv "discover") #$discover?))

                  (define daemon-command
                    (cons* #$(file-append guix "/bin/guix-daemon")
                           "--build-users-group" #$build-group
                    (cons* #$@(if privileged?
                                  #~()
                                  #~(#$(run-with-writable-store)
                                     "guix-daemon" "guix-daemon"))

                           #$(file-append guix "/bin/guix-daemon")
                           #$@(if privileged?
                                  #~("--build-users-group" #$build-group)
                                  #~())
                           "--max-silent-time"
                           #$(number->string max-silent-time)
                           "--timeout" #$(number->string timeout)


@@ 2145,9 2291,11 @@ proxy of 'guix-daemon'...~%")
                                     "/var/guix/daemon-socket/socket")
                                    #:name "socket"
                                    #:socket-owner
                                    (or #$socket-directory-user 0)
                                    (or #$socket-directory-user
                                        #$(if privileged? 0 "guix-daemon"))
                                    #:socket-group
                                    (or #$socket-directory-group 0)
                                    (or #$socket-directory-group
                                        #$(if privileged? 0 "guix-daemon"))
                                    #:socket-directory-permissions
                                    #$socket-directory-permissions)))
                       ((make-systemd-constructor daemon-command


@@ 2162,15 2310,31 @@ proxy of 'guix-daemon'...~%")

(define (guix-accounts config)
  "Return the user accounts and user groups for CONFIG."
  (cons (user-group
         (name (guix-configuration-build-group config))
         (system? #t)

         ;; Use a fixed GID so that we can create the store with the right
         ;; owner.
         (id 30000))
        (guix-build-accounts (guix-configuration-build-accounts config)
                             #:group (guix-configuration-build-group config))))
  `(,@(if (guix-configuration-privileged? config)
          '()
          (list (user-group (name "guix-daemon") (system? #t))
                (user-account
                 (name "guix-daemon")
                 (group "guix-daemon")
                 (system? #t)
                 (supplementary-groups '("kvm"))
                 (comment "Guix Daemon User")
                 (home-directory "/var/empty")
                 (shell (file-append shadow "/sbin/nologin")))))

    ;; When reconfiguring from privileged to unprivileged, the running daemon
    ;; (privileged) relies on the availability of the build accounts and build
    ;; group until 'guix system reconfigure' has completed.  The simplest way
    ;; to meet this requirement is to create these accounts unconditionally so
    ;; they are not removed in the middle of the 'reconfigure' process.
    ,(user-group
      (name (guix-configuration-build-group config))
      (system? #t)

      ;; Use a fixed GID so that we can create the store with the right owner.
      (id 30000))
    ,@(guix-build-accounts (guix-configuration-build-accounts config)
                           #:group (guix-configuration-build-group config))))

(define (guix-activation config)
  "Return the activation gexp for CONFIG."


@@ 2228,7 2392,7 @@ proxy of 'guix-daemon'...~%")
  (service-type
   (name 'guix)
   (extensions
    (list (service-extension shepherd-root-service-type guix-shepherd-service)
    (list (service-extension shepherd-root-service-type guix-shepherd-services)
          (service-extension account-service-type guix-accounts)
          (service-extension activation-service-type guix-activation)
          (service-extension profile-service-type

M gnu/tests/base.scm => gnu/tests/base.scm +36 -5
@@ 1,5 1,5 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016-2020, 2022, 2024 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2016-2020, 2022, 2024-2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2018 Clément Lassieur <clement@lassieur.org>
;;; Copyright © 2022 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; Copyright © 2022 Marius Bakke <marius@gnu.org>


@@ 63,7 63,8 @@

            %hello-dependencies-manifest
            guix-daemon-test-cases
            %test-guix-daemon))
            %test-guix-daemon
            %test-guix-daemon-unprivileged))

(define %simple-os
  (simple-operating-system))


@@ 1121,7 1122,7 @@ test."
                              (system-error-errno args)))
                         #$marionette))))

(define (run-guix-daemon-test os)
(define (run-guix-daemon-test os name)
  (define test-image
    (image (operating-system os)
           (format 'compressed-qcow2)


@@ 1168,7 1169,7 @@ test."

          (test-end))))

  (gexp->derivation "guix-daemon-test" test))
  (gexp->derivation name test))

(define %test-guix-daemon
  (system-test


@@ 1190,4 1191,34 @@ test."
                              %base-user-accounts)))
               #:imported-modules '((gnu services herd)
                                    (guix combinators)))))
      (run-guix-daemon-test os)))))
      (run-guix-daemon-test os "guix-daemon-test")))))

(define %test-guix-daemon-unprivileged
  (system-test
   (name "guix-daemon-unprivileged")
   (description
    "Test 'guix-daemon' behavior on a multi-user system, where 'guix-daemon'
runs unprivileged.")
   (value
    (let ((os (marionette-operating-system
               (let ((base (operating-system-with-gc-roots
                            %daemon-os
                            (list (profile
                                   (name "hello-build-dependencies")
                                   (content %hello-dependencies-manifest))))))
                 (operating-system
                   (inherit base)
                   (kernel-arguments '("console=ttyS0"))
                   (users (cons (user-account
                                 (name "user")
                                 (group "users"))
                                %base-user-accounts))
                   (services
                    (modify-services (operating-system-user-services base)
                      (guix-service-type
                       config => (guix-configuration
                                  (inherit config)
                                  (privileged? #f)))))))
               #:imported-modules '((gnu services herd)
                                    (guix combinators)))))
      (run-guix-daemon-test os "guix-daemon-unprivileged-test")))))