summary refs log tree commit diff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2015-05-12 11:41:55 +0200
committerLudovic Courtès <ludo@gnu.org>2015-05-12 21:22:52 +0200
commit5463fe512a02eb186ad95a1cae9d2682dbe2ccd0 (patch)
treedb0566b79c8f85c742f2b34a3da3459da7044d57
parent6ad2e17e8e44064c22c79b530a02fcc78ae85b5b (diff)
downloadguix-5463fe512a02eb186ad95a1cae9d2682dbe2ccd0.tar.gz
publish: Add '--user' option.
* guix/scripts/publish.scm (show-help): Add --user.
  (%options): Likewise.
  (run-publish-server): Change 'port' parameter to 'socket'.  Pass
  #:socket instead of #:addr and #:port to 'run-server'.  Update caller
  accordingly.
  (open-server-socket, gather-user-privileges): New procedures.
  (guix-publish): Use them.  Force %PRIVATE-KEY and %PUBLIC-KEY early
  on.  Warn when running as root.
* doc/guix.texi (Invoking guix publish): Document --user.
-rw-r--r--doc/guix.texi8
-rw-r--r--guix/scripts/publish.scm65
2 files changed, 61 insertions, 12 deletions
diff --git a/doc/guix.texi b/doc/guix.texi
index 3a9f91e7dc..8654e08b4f 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -3657,7 +3657,8 @@ the @code{hydra.gnu.org} build farm.
 For security, each substitute is signed, allowing recipients to check
 their authenticity and integrity (@pxref{Substitutes}).  Because
 @command{guix publish} uses the system's signing key, which is only
-readable by the system administrator, it must run as root.
+readable by the system administrator, it must be started as root; the
+@code{--user} option makes it drop root privileges early on.
 
 The general syntax is:
 
@@ -3686,6 +3687,11 @@ The following options are available:
 @itemx -p @var{port}
 Listen for HTTP requests on @var{port}.
 
+@item --user=@var{user}
+@itemx -u @var{user}
+Change privileges to @var{user} as soon as possible---i.e., once the
+server socket is open and the signing key has been read.
+
 @item --repl[=@var{port}]
 @itemx -r [@var{port}]
 Spawn a Guile REPL server (@pxref{REPL Servers,,, guile, GNU Guile
diff --git a/guix/scripts/publish.scm b/guix/scripts/publish.scm
index c7c66fefbe..86d3a754f3 100644
--- a/guix/scripts/publish.scm
+++ b/guix/scripts/publish.scm
@@ -51,6 +51,8 @@ Publish ~a over HTTP.\n") %store-directory)
   (display (_ "
   -p, --port=PORT        listen on PORT"))
   (display (_ "
+  -u, --user=USER        change privileges to USER as soon as possible"))
+  (display (_ "
   -r, --repl[=PORT]      spawn REPL server on PORT"))
   (newline)
   (display (_ "
@@ -68,6 +70,9 @@ Publish ~a over HTTP.\n") %store-directory)
         (option '(#\V "version") #f #f
                 (lambda _
                   (show-version-and-exit "guix publish")))
+        (option '(#\u "user") #t #f
+                (lambda (opt name arg result)
+                  (alist-cons 'user arg result)))
         (option '(#\p "port") #t #f
                 (lambda (opt name arg result)
                   (alist-cons 'port (string->number* arg) result)))
@@ -220,24 +225,62 @@ example: \"/foo/bar\" yields '(\"foo\" \"bar\")."
           (_ (not-found request)))
         (not-found request))))
 
-(define (run-publish-server port store)
+(define (run-publish-server socket store)
   (run-server (make-request-handler store)
               'http
-              `(#:addr ,INADDR_ANY
-                #:port ,port)))
+              `(#:socket ,socket)))
+
+(define (open-server-socket addr port)
+  "Return a TCP socket bound to ADDR and PORT."
+  (let ((sock (socket PF_INET SOCK_STREAM 0)))
+    (setsockopt sock SOL_SOCKET SO_REUSEADDR 1)
+    (bind sock AF_INET addr port)
+    sock))
+
+(define (gather-user-privileges user)
+  "Switch to the identity of USER, a user name."
+  (catch 'misc-error
+    (lambda ()
+      (let ((user (getpw user)))
+        (setgroups #())
+        (setgid (passwd:gid user))
+        (setuid (passwd:uid user))))
+    (lambda (key proc message args . rest)
+      (leave (_ "user '~a' not found: ~a~%")
+             user (apply format #f message args)))))
+
+
+;;;
+;;; Entry point.
+;;;
 
 (define (guix-publish . args)
   (with-error-handling
-    (let* ((opts (args-fold* args %options
-                             (lambda (opt name arg result)
-                               (leave (_ "~A: unrecognized option~%") name))
-                             (lambda (arg result)
-                               (leave (_ "~A: extraneuous argument~%") arg))
-                             %default-options))
-           (port (assoc-ref opts 'port))
+    (let* ((opts   (args-fold* args %options
+                               (lambda (opt name arg result)
+                                 (leave (_ "~A: unrecognized option~%") name))
+                               (lambda (arg result)
+                                 (leave (_ "~A: extraneuous argument~%") arg))
+                               %default-options))
+           (port   (assoc-ref opts 'port))
+           (user   (assoc-ref opts 'user))
+           (socket (open-server-socket INADDR_ANY port))
            (repl-port (assoc-ref opts 'repl)))
+      ;; Read the key right away so that (1) we fail early on if we can't
+      ;; access them, and (2) we can then drop privileges.
+      (force %private-key)
+      (force %public-key)
+
+      (when user
+        ;; Now that we've read the key material and opened the socket, we can
+        ;; drop privileges.
+        (gather-user-privileges user))
+
+      (when (zero? (getuid))
+        (warning (_ "server running as root; \
+consider using the '--user' option!~%")))
       (format #t (_ "publishing ~a on port ~d~%") %store-directory port)
       (when repl-port
         (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port)))
       (with-store store
-        (run-publish-server (assoc-ref opts 'port) store)))))
+        (run-publish-server socket store)))))