1
0
mirror of https://git.savannah.gnu.org/git/guix.git synced 2026-04-06 21:20:33 +02:00

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
3 changed files with 253 additions and 20 deletions

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