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
--- 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);
}