hgmo: acquire and drop capabilities (bug 1263973); r?kang draft
authorGregory Szorc <gps@mozilla.com>
Wed, 26 Oct 2016 12:20:50 -0700
changeset 9777 fe5adb65467be950f00cd6ebeef67933c51a1fba
parent 9776 2c0367c51a095268a0b258ec011635ccafb28fb1
push id1324
push userbmo:gps@mozilla.com
push dateWed, 26 Oct 2016 19:21:50 +0000
reviewerskang
bugs1263973
hgmo: acquire and drop capabilities (bug 1263973); r?kang Previously, the `hg` user had a sudoers policy to execute mozbuild-eval. This was fine under CentOS 6. But under CentOS 7, `mozbuild-eval` can't clone(2) with new namespaces without CAP_SYS_ADMIN. We teach ansible to grant CAP_SYS_ADMIN, CAP_SYS_CHROOT, CAP_SETUID, and CAP_SETGID to the binary. We also teach `mozbuild-eval` to drop capabilities explicitly in the parent and child processes once the elevated capabilities are no longer needed. The group owner of mozbuild-eval is changed to `hg` so hgweb processes can execute it. We also drop the old execution via sudo, as capabilities provide this functionality. MozReview-Commit-ID: EGCpFBHDhXB
ansible/roles/hg-web/files/hgrc
ansible/roles/hg-web/files/sudoers-mozbuild-eval
ansible/roles/hg-web/tasks/main.yml
testing/docker/builder-hgweb-chroot/Dockerfile
testing/docker/builder-hgweb-chroot/mozbuild-eval.c
--- a/ansible/roles/hg-web/files/hgrc
+++ b/ansible/roles/hg-web/files/hgrc
@@ -62,15 +62,15 @@ bugzilla = s|((?:bug[\s#]*|b=#?|#)(\d{4,
 pullmanifest=True
 
 [obshacks]
 # Enable the user that runs hgweb and performs replication to exchange
 # obsolescence markers, even if not enabled for regular users.
 obsolescenceexchangeusers = hg
 
 [hgmo]
-mozbuildinfowrapper = /usr/bin/sudo /usr/local/bin/mozbuild-eval %repo%
+mozbuildinfowrapper = /usr/local/bin/mozbuild-eval %repo%
 awsippath = /etc/mercurial/aws-ip-ranges.json
 
 # Disable new repos being created with generaldelta because it can cause
 # performance issues when serving large repos to old clients.
 [format]
 usegeneraldelta=false
deleted file mode 100644
--- a/ansible/roles/hg-web/files/sudoers-mozbuild-eval
+++ /dev/null
@@ -1,5 +0,0 @@
-# Enable the hg user (presumably from running hgweb processes) to
-# execute `mozbuild-eval` as root. Root permissions are needed in
-# order to perform chroot(). The executable is hard-coded to drop
-# permissions to the "mozbuild" user.
-hg ALL=NOPASSWD: /usr/local/bin/mozbuild-eval
--- a/ansible/roles/hg-web/tasks/main.yml
+++ b/ansible/roles/hg-web/tasks/main.yml
@@ -331,31 +331,32 @@
 # problematic. Ignore it until it becomes a problem.
 - name: upload and extract chroot archive
   unarchive: src={{ vct }}/chroot_mozbuild/chroot.tar.gz
              dest=/repo/hg/chroot_mozbuild
   when: chroot_mozbuild_exists
 
 # It is important for this binary to be located *outside* the chroot
 # because if code inside the chroot is able to modify the binary, it
-# will be able to execute as root given the sudo policy below.
+# will be able to execute as root given the caps policy
 - name: upload chroot evaluator binary
   copy: src={{ vct }}/chroot_mozbuild/mozbuild-eval
         dest=/usr/local/bin/mozbuild-eval
         owner=root
-        group=root
-        mode=0755
+        # Group ownership allows hgweb processes to run.
+        group=hg
+        mode=0750
   when: chroot_mozbuild_exists
 
-- name: install sudoers policy for mozbuild-eval
-  copy: src=sudoers-mozbuild-eval
-        dest=/etc/sudoers.d/mozbuild-eval
-        owner=root
-        group=root
-        mode=0440
+- name: give mozbuild-eval elevated privileges
+  command: setcap 'cap_sys_admin,cap_sys_chroot,cap_setuid,cap_setgid=+ep' /usr/local/bin/mozbuild-eval
+
+# TODO remove after it has been deployed.
+- name: remove old sudoers policy for mozbuild-eval
+  file: path=/etc/sudoers.d/mozbuild-eval state=absent
 
 - name: mount point for repos
   file: path={{ item }} state=directory owner=hg group=hg mode=0755
   with_items:
     - /repo/hg/chroot_mozbuild/repo/hg/mozilla
 
 # In order to get a read-only bind mount, we have to first do a regular
 # bind mount then do a remount. We can't work this magic with the
--- a/testing/docker/builder-hgweb-chroot/Dockerfile
+++ b/testing/docker/builder-hgweb-chroot/Dockerfile
@@ -4,17 +4,17 @@
 
 # Builds a Python chroot suitable for hgweb.
 
 FROM secure:mozsecure:centos7:sha256 874fae9bdbb7efd5c052d15330200184f71e36809157c2cf003c36df2eb467c7:https://s3-us-west-2.amazonaws.com/moz-packages/docker-images/centos-7-20160818-docker.tar.xz
 
 RUN yum update -y
 
 # Install build dependencies.
-RUN yum install -y bzip2-devel gcc libcgroup-devel make openssl-devel rsync sqlite-devel tar wget zlib-devel
+RUN yum install -y bzip2-devel gcc libcap-devel libcgroup-devel make openssl-devel rsync sqlite-devel tar wget zlib-devel
 
 # Download and verify Python source code.
 RUN wget https://www.python.org/ftp/python/2.7.12/Python-2.7.12.tgz
 ADD Python-2.7.12.tgz.asc /Python-2.7.12.tgz.asc
 ADD signer.gpg /signer.gpg
 RUN gpg --import /signer.gpg
 RUN gpg --verify /Python-2.7.12.tgz.asc
 
--- a/testing/docker/builder-hgweb-chroot/mozbuild-eval.c
+++ b/testing/docker/builder-hgweb-chroot/mozbuild-eval.c
@@ -13,16 +13,17 @@
 #define _GNU_SOURCE
 
 #include <grp.h>
 #include <mntent.h>
 #include <pwd.h>
 #include <sched.h>
 #include <stdio.h>
 #include <string.h>
+#include <sys/capability.h>
 #include <sys/mount.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
 #include <libcgroup.h>
 
 #define CHROOT "/repo/hg/chroot_mozbuild"
@@ -34,16 +35,26 @@ const char* chroot_env[] = {
     "HGENCODING=utf-8",
     NULL,
 };
 
 const char* hostname = "mozbuildeval";
 
 static char stack[1048576];
 
+static void drop_caps() {
+    struct __user_cap_header_struct header = { _LINUX_CAPABILITY_VERSION_3, 0 };
+    struct __user_cap_data_struct data[2] = { { 0 } };
+
+    if (-1 == capset(&header, data)) {
+        fprintf(stderr, "capset failed\n");
+        _exit(1);
+    }
+}
+
 /**
  * Child process that does all the work. This process is disassociated
  * from the parent. But it still has root privileges.
  */
 static int call_mozbuildinfo(void* repo_path) {
     struct passwd* user = NULL;
     FILE* fmount;
     struct mntent* mnt;
@@ -212,16 +223,20 @@ static int call_mozbuildinfo(void* repo_
     }
 
     err = setresuid(user->pw_uid, user->pw_uid, user->pw_uid);
     if (err) {
         fprintf(stderr, "unable to setresuid\n");
         return 1;
     }
 
+    /* We're done performing privileged operations. Drop capabilities
+     * we won't need. */
+    drop_caps();
+
     /* And now that we've dropped all privileges, do our moz.build
      * evaluation. Since we are in a PID namespace and we are PID 1, we
      * do a fork first because PID 1 is special and we don't want our
      * Python process possibly getting tangled in those properties. */
     pid = fork();
     if (pid == -1) {
         fprintf(stderr, "unable to fork\n");
         return 1;
@@ -294,16 +309,19 @@ int main(int argc, const char* argv[]) {
                 stack + sizeof(stack),
                 clone_flags,
                 (void*)argv[1]);
     if (pid < 1) {
         fprintf(stderr, "clone failed\n");
         return 1;
     }
 
+    /* We don't need elevated capabilities to wait on the child. So drop. */
+    drop_caps();
+
     if (waitpid(pid, &child_status, 0) == -1) {
         fprintf(stderr, "failed to wait on child\n");
         return 1;
     }
 
     if (WIFEXITED(child_status)) {
         return WEXITSTATUS(child_status);
     }