GNU bug report logs - #77288
[PATCH 0/6] Rootless guix-daemon on Guix System

Previous Next

Package: guix-patches;

Reported by: Ludovic Courtès <ludo <at> gnu.org>

Date: Wed, 26 Mar 2025 16:50:01 UTC

Severity: normal

Tags: patch

To reply to this bug, email your comments to 77288 AT debbugs.gnu.org.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to pelzflorian <at> pelzflorian.de, julien <at> lepiller.eu, guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:50:02 GMT) Full text and rfc822 format available.

Acknowledgement sent to Ludovic Courtès <ludo <at> gnu.org>:
New bug report received and forwarded. Copy sent to pelzflorian <at> pelzflorian.de, julien <at> lepiller.eu, guix-patches <at> gnu.org. (Wed, 26 Mar 2025 16:50:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: guix-patches <at> gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH 0/6] Rootless guix-daemon on Guix System
Date: Wed, 26 Mar 2025 17:48:38 +0100
Hello Guix,

This is a followup to <https://issues.guix.gnu.org/75810>,
which also depends on <https://issues.guix.gnu.org/77189>,
allowing us to run ‘guix-daemon’ without root privileges on
Guix System.  It is the second step of the migration path
outlined in <https://issues.guix.gnu.org/75810#111-lineno40>.

This is made difficult by the fact that all this is stateful:
if I switch my system to unprivileged mode, then the store and
all the data files of the daemon must have their owner changed
to ‘guix-daemon’.

This is implemented by an intermediate ‘guix-ownership’ one-shot
service, which completes instantaneously in the normal case and
chowns if when switching from privileged to unprivileged and vice
versa.  This service remains in ‘starting’ state until it is done.

Another complication is that of /gnu/store being mounted read-only.
To provide the ‘guix-ownership’ and ‘guix-daemon’ processes write
access to the store, they are started by a wrapper that creates a
new mount namespace and remounts the store read-write (similar to
‘makeStoreWritable’ in the daemon).

An open issue is ‘--keep-failed’: currently /tmp/guix-build-*
directories will remain owned by ‘guix-daemon’ as was discussed in
the initial message at <https://issues.guix.gnu.org/75810>.  It’s
a regression, but maybe it’s acceptable if we consider that this
feature is primarily used on single-user machines.

For now, the installation procedure creates /gnu/store, /var/guix,
etc. with root:root ownership.  Eventually, if/when we settle on
unprivileged guix-daemon, we should change that code to have
guix-daemon:guix-daemon as the owner.

Ludovic Courtès (6):
  syscalls: Add ‘unshare’.
  services: account: Create /var/guix/profiles/per-user/$USER.
  tests: guix-daemon: Send system log output to /dev/console.
  tests: guix-daemon: Wait for the ‘guix-daemon’ service to be up.
  services: guix: Allow ‘guix-daemon’ to run without root privileges.
  DRAFT news: Add entry about unprivileged guix-daemon on Guix System.

 doc/guix.texi           |  30 +++++++
 etc/news.scm            |  24 ++++++
 gnu/services/base.scm   | 187 ++++++++++++++++++++++++++++++++++++----
 gnu/system/shadow.scm   |  19 +++-
 gnu/tests/base.scm      |  60 +++++++++++--
 guix/build/syscalls.scm |  18 ++++
 tests/syscalls.scm      |   9 ++
 7 files changed, 325 insertions(+), 22 deletions(-)


base-commit: 1a69acce515de9be9b95df04c553a47a808e5034
-- 
2.49.0





Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:01 GMT) Full text and rfc822 format available.

Message #8 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 2/6] services: account: Create
 /var/guix/profiles/per-user/$USER.
Date: Wed, 26 Mar 2025 17:51:03 +0100
* gnu/system/shadow.scm (account-shepherd-service): Create
/var/guix/profiles/per-user/$USER in ‘user-homes’ service.

Change-Id: I22e66e8a34d63686df9bae64c68df65c8889e72a
---
 gnu/system/shadow.scm | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/gnu/system/shadow.scm b/gnu/system/shadow.scm
index b68a818871..d0f1b6b2b1 100644
--- a/gnu/system/shadow.scm
+++ b/gnu/system/shadow.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2020, 2022, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2013-2020, 2022-2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2016 Alex Griffin <a <at> ajgrf.com>
 ;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke <at> gnu.org>
 ;;; Copyright © 2020, 2023 Efraim Flashner <efraim <at> flashner.co.il>
@@ -460,6 +460,12 @@ (define (account-shepherd-service accounts+groups)
   (define accounts
     (filter user-account? accounts+groups))
 
+  (define regular-account-names
+    (filter-map (lambda (account)
+                  (and (not (user-account-system? account))
+                       (user-account-name account)))
+                accounts))
+
   ;; Create home directories only once 'file-systems' is up.  This makes sure
   ;; they are created in the right place if /home lives on a separate
   ;; partition.
@@ -480,6 +486,17 @@ (define (account-shepherd-service accounts+groups)
                       (activate-user-home
                        (map sexp->user-account
                             (list #$@(map user-account->gexp accounts))))
+
+                      ;; Create the user's profile directory upfront:
+                      ;; guix-daemon lacks permissions to create it when it is
+                      ;; running as an unprivileged user.
+                      (for-each (lambda (account)
+                                  (let ((profile (in-vicinity
+                                                  "/var/guix/profiles/per-user"
+                                                  account))
+                                        (owner (getpwnam account)))
+                                    (mkdir-p/perms profile owner #o755)))
+                                '#$regular-account-names)
                       #t)))                       ;success
          (documentation "Create user home directories."))))
 
-- 
2.49.0





Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:02 GMT) Full text and rfc822 format available.

Message #11 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 1/6] syscalls: Add ‘unshare’.
Date: Wed, 26 Mar 2025 17:51:02 +0100
* guix/build/syscalls.scm (unshare): New procedure.

Change-Id: I344273b8bdeaa9366334e6e20ee7efc37eb6c8f7
---
 guix/build/syscalls.scm | 18 ++++++++++++++++++
 tests/syscalls.scm      |  9 +++++++++
 2 files changed, 27 insertions(+)

diff --git a/guix/build/syscalls.scm b/guix/build/syscalls.scm
index 42232fc7f1..cf09cae3a4 100644
--- a/guix/build/syscalls.scm
+++ b/guix/build/syscalls.scm
@@ -145,6 +145,7 @@ (define-module (guix build syscalls)
             CLONE_NEWPID
             CLONE_NEWNET
             clone
+            unshare
             setns
 
             kexec-load-file
@@ -1213,6 +1214,23 @@ (define clone
                    (list err))
             ret)))))
 
+(define unshare
+  (let ((proc (syscall->procedure int "unshare" (list int))))
+    (lambda (flags)
+      "Disassociate the current process from parts of its execution context
+according to FLAGS, which must be a logical or of CLONE_NEW* constants.
+
+Note that CLONE_NEWUSER requires that the calling process be single-threaded,
+which is possible if and only if libgc is running a single marker thread; this
+can be achieved by setting the GC_MARKERS environment variable to 1.  If the
+calling process is multi-threaded, this throws to 'system-error' with EINVAL."
+      (let-values (((ret err)
+                    (without-automatic-finalization (proc flags))))
+        (unless (zero? ret)
+          (throw 'system-error "unshare" "~a: ~A"
+                 (list flags (strerror err))
+                 (list err)))))))
+
 (define setns
   ;; Some systems may be using an old (pre-2.14) version of glibc where there
   ;; is no 'setns' function available.
diff --git a/tests/syscalls.scm b/tests/syscalls.scm
index d2848879d7..879c3e4f25 100644
--- a/tests/syscalls.scm
+++ b/tests/syscalls.scm
@@ -149,6 +149,15 @@ (define perform-container-tests?
             ((_ . status)
              (= 42 (status:exit-val status))))))))
 
+(test-equal "unshare"
+  EPERM
+  ;; Unless running as root, (unshare CLONE_NEWNS) returns EPERM.
+  (catch 'system-error
+    (lambda ()
+      (unshare CLONE_NEWNS))
+    (lambda args
+      (system-error-errno args))))
+
 (unless perform-container-tests?
   (test-skip 1))
 (test-assert "setns"
-- 
2.49.0





Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:02 GMT) Full text and rfc822 format available.

Message #14 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 3/6] tests: guix-daemon: Send system log output to
 /dev/console.
Date: Wed, 26 Mar 2025 17:51:04 +0100
* gnu/tests/base.scm (%daemon-os): New variable.
(%test-guix-daemon): Use it.

Change-Id: Iea31808cc59e94971ea4cbc12d565c94348bf7a4
---
 gnu/tests/base.scm | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/gnu/tests/base.scm b/gnu/tests/base.scm
index a7f8a5bf7c..0f7fb543a7 100644
--- a/gnu/tests/base.scm
+++ b/gnu/tests/base.scm
@@ -994,6 +994,10 @@ (define %test-activation
 ;;; Build daemon.
 ;;;
 
+(define %daemon-os
+  (operating-system-with-console-syslog
+   (simple-operating-system)))
+
 (define (manifest-entry-without-grafts entry)
   "Return ENTRY with grafts disabled on its contents."
   (manifest-entry
@@ -1168,7 +1172,7 @@ (define %test-guix-daemon
     (let ((os (marionette-operating-system
                (operating-system
                  (inherit (operating-system-with-gc-roots
-                           %simple-os
+                           %daemon-os
                            (list (profile
                                   (name "hello-build-dependencies")
                                   (content %hello-dependencies-manifest)))))
-- 
2.49.0





Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:02 GMT) Full text and rfc822 format available.

Message #17 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 4/6] tests: guix-daemon: Wait for the ‘guix-daemon’ service to be up.
Date: Wed, 26 Mar 2025 17:51:05 +0100
* gnu/tests/base.scm (run-guix-daemon-test): Add “guix-daemon service is
up” test.

Change-Id: I4d44a1248599fec45c854c285d4da201c30eb00c
---
 gnu/tests/base.scm | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/gnu/tests/base.scm b/gnu/tests/base.scm
index 0f7fb543a7..83e047f7e6 100644
--- a/gnu/tests/base.scm
+++ b/gnu/tests/base.scm
@@ -1157,6 +1157,13 @@ (define (run-guix-daemon-test os)
           (test-runner-current (system-test-runner #$output))
           (test-begin "guix-daemon")
 
+          (test-assert "guix-service is running"
+            ;; Wait for 'guix-daemon' to be up.
+            (marionette-eval '(begin
+                                (use-modules (gnu services herd))
+                                (start-service 'guix-daemon))
+                             marionette))
+
           #$(guix-daemon-test-cases #~marionette)
 
           (test-end))))
-- 
2.49.0





Information forwarded to pelzflorian <at> pelzflorian.de, julien <at> lepiller.eu, guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:03 GMT) Full text and rfc822 format available.

Message #20 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 6/6] DRAFT news: Add entry about unprivileged guix-daemon on
 Guix System.
Date: Wed, 26 Mar 2025 17:51:07 +0100
DRAFT: Temporary commit.

* etc/news.scm: Add it.

Change-Id: I28eae7f7b4305225b13281b99458cbedda3c3b94
---
 etc/news.scm | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/etc/news.scm b/etc/news.scm
index 4b3da44540..840f5cea53 100644
--- a/etc/news.scm
+++ b/etc/news.scm
@@ -37,6 +37,30 @@
 (channel-news
  (version 0)
 
+ (entry (commit "XXX")
+        (title
+         (en "Guix System can run @command{guix-daemon} without root
+privileges"))
+        (body
+         (en "On Guix System, @code{guix-service-type} can now be configured
+to run the build daemon, @command{guix-daemon}, without root privileges.  In
+that configuration, the daemon runs with the authority of the
+@code{guix-daemon} user, which we think can reduce the impact of some classes
+of vulnerabilities that could affect it.
+
+For now, this is opt-in: you have to change @code{guix-configuration} to set
+the @code{privileged?} field to @code{#f}.  When you do this, all the files in
+@file{/gnu/store}, @file{/var/guix}, etc. will have their ownership changed to
+the @code{guix-daemon} user (instead of @code{root}); this can take a while,
+especially if the store is big.  To learn more about it, run:
+
+@example
+info guix --index-search=guix-service-type
+@end example
+
+Eventually running @command{guix-daemon} without root privileges may become
+the default.")))
+
  (entry (commit "0e51c6547ffdaf91777f7383da4a52a1a07b7286")
         (title
          (en "Incompatible upgrade of the Syncthing service"))
-- 
2.49.0





Information forwarded to ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Wed, 26 Mar 2025 16:52:03 GMT) Full text and rfc822 format available.

Message #23 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 77288 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 5/6] services: guix: Allow ‘guix-daemon’ to run without root privileges.
Date: Wed, 26 Mar 2025 17:51:06 +0100
* 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.
(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?’.

Change-Id: I28a9a22e617416c551dccb24e43a253b544ba163
---
 doc/guix.texi         |  30 +++++++
 gnu/services/base.scm | 187 ++++++++++++++++++++++++++++++++++++++----
 gnu/tests/base.scm    |  47 +++++++++--
 3 files changed, 244 insertions(+), 20 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 5af41830ca..f58688f57a 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -20046,6 +20046,36 @@ Base Services
 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.
+
+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.
 
diff --git a/gnu/services/base.scm b/gnu/services/base.scm
index 9a9dfdb304..8f66f54e74 100644
--- a/gnu/services/base.scm
+++ b/gnu/services/base.scm
@@ -1917,6 +1917,100 @@ (define (guix-machines-files-installation machines)
                                    #$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?
@@ -1958,6 +2052,8 @@ (define-record-type* <guix-configuration>
                     (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
@@ -2020,7 +2116,7 @@ (define shepherd-discover-action
                     (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)
@@ -2029,16 +2125,57 @@ (define (guix-shepherd-service config)
           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))
@@ -2062,8 +2199,15 @@ (define (guix-shepherd-service config)
                     (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)
@@ -2144,9 +2288,11 @@ (define (guix-shepherd-service config)
                                      "/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
@@ -2161,15 +2307,26 @@ (define (guix-shepherd-service config)
 
 (define (guix-accounts config)
   "Return the user accounts and user groups for CONFIG."
-  (cons (user-group
-         (name (guix-configuration-build-group config))
-         (system? #t)
+  (if (guix-configuration-privileged? 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))))
+             ;; 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)))
+      (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"))))))
 
 (define (guix-activation config)
   "Return the activation gexp for CONFIG."
@@ -2227,7 +2384,7 @@ (define guix-service-type
   (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
diff --git a/gnu/tests/base.scm b/gnu/tests/base.scm
index 83e047f7e6..12d4e70ee5 100644
--- a/gnu/tests/base.scm
+++ b/gnu/tests/base.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2016-2020, 2022, 2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2016-2020, 2022, 2024-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2018 Clément Lassieur <clement <at> lassieur.org>
 ;;; Copyright © 2022 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
 ;;; Copyright © 2022 Marius Bakke <marius <at> gnu.org>
@@ -63,7 +63,8 @@ (define-module (gnu tests base)
 
             %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 @@ (define (guix-daemon-test-cases marionette)
                               (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)
@@ -1161,6 +1162,12 @@ (define (run-guix-daemon-test os)
             ;; Wait for 'guix-daemon' to be up.
             (marionette-eval '(begin
                                 (use-modules (gnu services herd))
+                                (start-service 'guix-daemon)
+
+                                ;; XXX: Do it a second time to work around
+                                ;; <https://issues.guix.gnu.org/77274> and its
+                                ;; effect on the 'guix-ownership' service.
+                                ;; TODO: Remove when Shepherd 1.0.4 is out.
                                 (start-service 'guix-daemon))
                              marionette))
 
@@ -1168,7 +1175,7 @@ (define (run-guix-daemon-test os)
 
           (test-end))))
 
-  (gexp->derivation "guix-daemon-test" test))
+  (gexp->derivation name test))
 
 (define %test-guix-daemon
   (system-test
@@ -1190,4 +1197,34 @@ (define %test-guix-daemon
                               %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")))))
-- 
2.49.0





Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Thu, 27 Mar 2025 13:28:03 GMT) Full text and rfc822 format available.

Message #26 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: "pelzflorian (Florian Pelz)" <pelzflorian <at> pelzflorian.de>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: Julien Lepiller <julien <at> lepiller.eu>, 77288 <at> debbugs.gnu.org,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: Re: [bug#77288] [PATCH 6/6] DRAFT news: Add entry about
 unprivileged guix-daemon on Guix System.
Date: Thu, 27 Mar 2025 14:27:33 +0100
[Message part 1 (text/plain, inline)]
Rootless daemon is an important change, though I have not tested yet...

Ludovic Courtès <ludo <at> gnu.org> writes:
> +Eventually running @command{guix-daemon} without root privileges may become
> +the default.")))
> +

I dislike the word “may” in this last sentence.  How about “likely
will” or some such thing, even if we have not reviewed bugfreeness of
Linux here?

Can you tell foreign distro users about their rootless options in the
news, too?

Could you add this German translation?

[german-rootless-news.scm (text/plain, inline)]
 (entry (commit "XXX")
        (title
         (en "Guix System can run @command{guix-daemon} without root
privileges")
         (de "Guix System kann @command{guix-daemon} ohne root-Berechtigungen
ausführen"))
        (body
         (en "On Guix System, @code{guix-service-type} can now be configured
to run the build daemon, @command{guix-daemon}, without root privileges.  In
that configuration, the daemon runs with the authority of the
@code{guix-daemon} user, which we think can reduce the impact of some classes
of vulnerabilities that could affect it.

For now, this is opt-in: you have to change @code{guix-configuration} to set
the @code{privileged?} field to @code{#f}.  When you do this, all the files in
@file{/gnu/store}, @file{/var/guix}, etc. will have their ownership changed to
the @code{guix-daemon} user (instead of @code{root}); this can take a while,
especially if the store is big.  To learn more about it, run:

@example
info guix --index-search=guix-service-type
@end example

Eventually running @command{guix-daemon} without root privileges may become
the default.")
         (de "Auf Guix System kann @code{guix-service-type} jetzt so
konfiguriert werden, dass der Erstellungs-Daemon @command{guix-daemon} ohne
root-Berechtigungen ausgeführt wird.  In dieser Konfiguration läuft der Daemon
mit den Berechtigungen des Benutzers @code{guix-daemon}, wovon wir glauben,
dass es die Auswirkungen mancher Schwachstellen-Kategorien verringert, die ihn
betreffen könnten.

Fürs Erste bleibt es Ihnen überlassen: Sie müssen @code{guix-configuration}
anpassen und dort das Feld @code{privileged?} auf @code{#f} setzen. Wenn Sie
das tun, wird der Besitzer aller Dateien in @file{/gnu/store},
@file{/var/guix}, usw.@: auf den Benutzer @code{guix-daemon} geändert (anstelle
von @code{root}); das kann eine Weile dauern, besonders wenn der Store groß
ist.  Um mehr zu erfahren, führen Sie aus:

@example
info guix --index-search=guix-service-type
@end example

Schließlich wird das Ausführen von @command{guix-daemon} ohne
root-Berechtigungen vielleicht die Vorgabe werden.")))
[Message part 3 (text/plain, inline)]
Regards,
Florian



Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Thu, 27 Mar 2025 13:40:05 GMT) Full text and rfc822 format available.

Message #29 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: "pelzflorian (Florian Pelz)" <pelzflorian <at> pelzflorian.de>
Cc: Julien Lepiller <julien <at> lepiller.eu>, 77288 <at> debbugs.gnu.org,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: Re: [bug#77288] [PATCH 6/6] DRAFT news: Add entry about
 unprivileged guix-daemon on Guix System.
Date: Thu, 27 Mar 2025 14:38:54 +0100
Hi Florian,

"pelzflorian (Florian Pelz)" <pelzflorian <at> pelzflorian.de> skribis:

> Rootless daemon is an important change, though I have not tested yet...
>
> Ludovic Courtès <ludo <at> gnu.org> writes:
>> +Eventually running @command{guix-daemon} without root privileges may become
>> +the default.")))
>> +
>
> I dislike the word “may” in this last sentence.  How about “likely
> will” or some such thing, even if we have not reviewed bugfreeness of
> Linux here?

Sure; I view “may” and “likely will” as synonymous, but maybe there are
subtleties that escape me.

> Can you tell foreign distro users about their rootless options in the
> news, too?

This news item is specifically about Guix System (announced upfront),
but I guess we can add a sentence toward the end.  (Thing is, the
situation will be simpler on foreign distros: we won’t support switching
between privileged and unprivileged, so either you get one or the
other.)

Thanks for the comments and for the translation!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#77288; Package guix-patches. (Fri, 28 Mar 2025 07:41:01 GMT) Full text and rfc822 format available.

Message #32 received at 77288 <at> debbugs.gnu.org (full text, mbox):

From: "pelzflorian (Florian Pelz)" <pelzflorian <at> pelzflorian.de>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: Julien Lepiller <julien <at> lepiller.eu>, 77288 <at> debbugs.gnu.org,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: Re: [bug#77288] [PATCH 6/6] DRAFT news: Add entry about
 unprivileged guix-daemon on Guix System.
Date: Fri, 28 Mar 2025 08:39:14 +0100
Hi Ludo.

Ludovic Courtès <ludo <at> gnu.org> writes:
> "pelzflorian (Florian Pelz)" <pelzflorian <at> pelzflorian.de> skribis:
>> Ludovic Courtès <ludo <at> gnu.org> writes:
>>> +Eventually running @command{guix-daemon} without root privileges may become
>>> +the default.")))
>>> +
>>
>> I dislike the word “may” in this last sentence.  How about “likely
>> will” or some such thing, even if we have not reviewed bugfreeness of
>> Linux here?
>
> Sure; I view “may” and “likely will” as synonymous, but maybe there are
> subtleties that escape me.
>

I had understood “likely” to be “probably”.  With more thinking and
Wiktionary reading, “likely” can also mean “plausibly”, though not in
this context, I think.

The word “may” you had used means “possibly”, which sounds
indecisive/uncommitted and if even Guix does not know yet if rootless is
a good idea, how should a news-reading user know?  I thought you wrote
it for this purpose, because distros had been wary of USERNS some time
ago, or something.

This is why I translated to German as „vielleicht” (maybe).

Apparently some people want to banish the word “may” for its suppossed
ambiguity; it can mean “possibly” and “have permission to”, according to
Wiktionary.  This subtlety is news to me; I think context makes clear
what “may” means.

But perhaps it really is better to use unambiguous adverbs, adjectives
like “probably”.

In case you write this “probably” or “likely”, I would translate in
German as „wahrscheinlich” (probably).



>> Can you tell foreign distro users about their rootless options in the
>> news, too?
>
> This news item is specifically about Guix System (announced upfront),
> but I guess we can add a sentence toward the end.  (Thing is, the
> situation will be simpler on foreign distros: we won’t support switching
> between privileged and unprivileged, so either you get one or the
> other.)
>

Yes, please!  That sentence in parentheses explains the situation, but
needs rewording for etc/news.scm of course.


> Thanks for the comments and for the translation!
>
> Ludo’.

:)

I also now notice it is impossible to translate

info guix --index-search=guix-service-type

as

info guix.de --index-search=guix-service-type

although

info guix.fr --index-search=guix-service-type

works fine.  No idea why; both texi files look the same here.  But since
the rootless documentation is not translated yet anyway, please leave

info guix --index-search=guix-service-type

Regards,
Florian




This bug report was last modified 18 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.