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
This commit is contained in:
Ludovic Courtès 2025-03-25 17:41:57 +01:00
parent 2c7c059e0b
commit e2583b5a17
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
3 changed files with 253 additions and 20 deletions

View file

@ -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.

View file

@ -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)
`(,@(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")))))
;; 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))))
;; 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

View file

@ -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")))))