From patchwork Wed Mar 26 16:51:06 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Ludovic_Court=C3=A8s?= X-Patchwork-Id: 40858 Return-Path: X-Original-To: patchwork@mira.cbaines.net Delivered-To: patchwork@mira.cbaines.net Received: by mira.cbaines.net (Postfix, from userid 113) id 54CE327BBEA; Wed, 26 Mar 2025 16:52:49 +0000 (GMT) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-7.6 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,MAILING_LIST_MULTI,RCVD_IN_DNSWL_BLOCKED, RCVD_IN_VALIDITY_CERTIFIED,RCVD_IN_VALIDITY_RPBL,RCVD_IN_VALIDITY_SAFE, SPF_HELO_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.6 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mira.cbaines.net (Postfix) with ESMTPS id 403F927BBE2 for ; Wed, 26 Mar 2025 16:52:48 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1txTzQ-0004Az-2I; Wed, 26 Mar 2025 12:52:33 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1txTz9-00045S-TE for guix-patches@gnu.org; Wed, 26 Mar 2025 12:52:12 -0400 Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1txTz2-0007WJ-2O; Wed, 26 Mar 2025 12:52:11 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=debbugs.gnu.org; s=debbugs-gnu-org; h=MIME-Version:References:In-Reply-To:Date:From:To:Subject; bh=qYYVNgb+ThfS8s8i9+Nb3/LexlVoe5V9LNmeKUHcJo0=; b=o1j9aeXyksVsVgqG3qz3GNruXtCK5XGwk3yoEtzWZf+Z//1AS3TP2i9U630CHnL9uzG3yHvQX5LXXiUSIRUCy2AC75DeOPjeSaM733DmQON2JxGmY5KhvilRhluow9jNoQYN2sccm3aP5gy2KF4ZVEXkyRfNecw+uSnK1ehJsCo4yYahgXhj31FMxvEg6BbzT+WFchRlc5WRaetaApYjF19T7kFH+kKdvBiu3dHoImb58ahhmL1OsPGeMcPvDWhMeh5hUNND3BaDhWlBiLrR1593zgRwMgJsN5NE3Yfmi3Z7JDDDz9jTQ+tec9pW21NDYY6e4gFv0ypSn7++OO91kg==; Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1txTz1-0000bJ-PH; Wed, 26 Mar 2025 12:52:03 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#77288] [PATCH 5/6] services: guix: Allow =?utf-8?b?4oCYZ3Vp?= =?utf-8?b?eC1kYWVtb27igJk=?= to run without root privileges. Resent-From: Ludovic =?utf-8?q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: ludo@gnu.org, maxim.cournoyer@gmail.com, guix-patches@gnu.org Resent-Date: Wed, 26 Mar 2025 16:52:03 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 77288 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 77288@debbugs.gnu.org Cc: Ludovic =?utf-8?q?Court=C3=A8s?= , Ludovic =?utf-8?q?Court?= =?utf-8?q?=C3=A8s?= , Maxim Cournoyer X-Debbugs-Original-Xcc: Ludovic =?utf-8?q?Court=C3=A8s?= , Maxim Cournoyer Received: via spool by 77288-submit@debbugs.gnu.org id=B77288.17430079172255 (code B ref 77288); Wed, 26 Mar 2025 16:52:03 +0000 Received: (at 77288) by debbugs.gnu.org; 26 Mar 2025 16:51:57 +0000 Received: from localhost ([127.0.0.1]:44151 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1txTyu-0000aI-A5 for submit@debbugs.gnu.org; Wed, 26 Mar 2025 12:51:57 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:58070) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from ) id 1txTyY-0000YZ-G4 for 77288@debbugs.gnu.org; Wed, 26 Mar 2025 12:51:35 -0400 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1txTyS-0007Lh-7a; Wed, 26 Mar 2025 12:51:29 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=MIME-Version:References:In-Reply-To:Date:Subject:To: From; bh=qYYVNgb+ThfS8s8i9+Nb3/LexlVoe5V9LNmeKUHcJo0=; b=EqT6zJTQ3MgOm8Hm9rZC 2obHZofEXKfaXprA4H22M22MBkq7efMsIEMYfPguPPyElMHAXQJ4rJEmVv9Ie70HSpoBcktNKNyQn HYiEr0r/UzmYgQxMuN/0xv/NXlYucQ3DMzb3mdyvxYhyeq8eKoPfzx6Z1HOF0AwqwTTfVahqz7TGi id7t90uLuYukqoJ/Ukl9oaj4RzQGYUF1fs5FqjjLcXwk4ZynEUfgY3AjkA1pHPXE3KoBhrSDxGHl2 C4p6Qo+DnB9arxfWP5tfL9sIZo6OX+FlGQAkKcr41565ZwdJL9I6jFbYAJZBgry834ujRNzBSXrq3 jEqGn8imdeTJSg==; From: Ludovic =?utf-8?q?Court=C3=A8s?= Date: Wed, 26 Mar 2025 17:51:06 +0100 Message-ID: X-Mailer: git-send-email 2.49.0 In-Reply-To: References: MIME-Version: 1.0 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org Sender: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org X-getmail-retrieved-from-mailbox: Patches * gnu/services/base.scm (run-with-writable-store) (guix-ownership-change-program): New procedures. ()[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 make-guix-configuration guix-configuration? @@ -1958,6 +2052,8 @@ (define-record-type* (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 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 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 +;;; Copyright © 2016-2020, 2022, 2024-2025 Ludovic Courtès ;;; Copyright © 2018 Clément Lassieur ;;; Copyright © 2022 Maxim Cournoyer ;;; Copyright © 2022 Marius Bakke @@ -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 + ;; 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")))))