James Wilson has proposed merging lp:~jmwilson/duplicity/capabilities into 
lp:duplicity.

Requested reviews:
  duplicity-team (duplicity-team)

For more details, see:
https://code.launchpad.net/~jmwilson/duplicity/capabilities/+merge/257488

Proposal is to add an unprivileged "duplicity" user during installation that is 
used to limit the capabilities when duplicity is run as root. To manage 
capabilities, I'm using the python bindings for libcap-ng (which needs its own 
updating since at least the current vivid package is empty for some reason, but 
is correct when built from source).

I've been interested in using duplicity for system-wide backups, but one thing 
that is troubling is that it must be run as root. As an interpreted program 
that communicates on the network, this exposes the host to possible bugs in 
python or any other packages imported in duplicity.

First, examining the code in bin/duplicity:
    # if python is run setuid, it's only partway set,
    # so make sure to run with euid/egid of root
    if os.geteuid() == 0:
        # make sure uid/gid match euid/egid
        os.setuid(os.geteuid())
        os.setgid(os.getegid())

I'm not sure what this is for; if you're root (i.e., os.geteuid() == 0) then 
there's no need to switch the real uid. When the machination of 
"setuid(geteuid())" is used it is typically when the ruid=0 and we want to 
irrevocably drop privileges to a euid!=0 normal user. That's not the case here, 
so this doesn't help or harm us.

Then there's the issue of running duplicity as root. Since it's an interpreted 
program, the normal ways of expanding privileges (SUID executable or setcap on 
the script) are unavailable, and the other options are to run it as root, or 
put SUID or file capabilities on the python interpreter.

The only reason to run duplicity as root is get read access to the whole file 
system. We can safely drop all capabilities other than CAP_DAC_READ_SEARCH. 
Since we're still root, we'll get all capabilities back if we do execve, so we 
could also change the bounding set or lock the securebits to prevent the kernel 
from re-granting privileges on execve. Nonetheless, we're still root, and lots 
of important files are owned by and writable to root, so the best choice is to 
change uid to an unprivileged user who maintains only the ability to read the 
whole file system.

It's hard to test the change directly, since it doesn't change the output or 
actions of duplicity. The following python script mimics what the code does and 
demonstrates the reduction of capabilities:

#!/usr/bin/env python

from __future__ import print_function
from capng import *
import fcntl
import os
import pwd
import signal
import sys

if os.geteuid() != 0:
    sys.exit()

user = pwd.getpwnam("nobody")
capng_clear(CAPNG_SELECT_CAPS)
capng_update(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED, CAP_DAC_READ_SEARCH)
capng_change_id(user.pw_uid, user.pw_gid, CAPNG_DROP_SUPP_GRP)

print("getuid = {}, geteuid = {}".format(os.getuid(), os.geteuid()))
print("After dropping capabilities:")
capng_get_caps_process()
capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_BOTH)
print()

pread, pwrite = os.pipe()
pid = os.fork()
if pid == 0:
    os.close(pread)
    fcntl.fcntl(pwrite, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
    os.execl('/usr/bin/tail', '-f', '/dev/null')
else:
    os.close(pwrite)
    os.read(pread, 1)
    capng_setpid(pid)
    capng_get_caps_process()
    print("getuid = {}, geteuid = {}".format(os.getuid(), os.geteuid()))
    print("Capabilities after execve:")
    caps = capng_print_caps_numeric(CAPNG_PRINT_STDOUT, CAPNG_SELECT_BOTH)
    os.kill(pid, signal.SIGTERM)

which when run as root (sudo python script.py) should produce this output:
getuid = 65534, geteuid = 65534
After dropping capabilities:
Effective:    00000000, 00000004
Permitted:    00000000, 00000004
Inheritable:  00000000, 00000000
Bounding Set: 0000003F, FFFFFFFF

getuid = 65534, geteuid = 65534
Capabilities after execve:
Effective:    00000000, 00000000
Permitted:    00000000, 00000000
Inheritable:  00000000, 00000000
Bounding Set: 0000003F, FFFFFFFF

-- 
Your team duplicity-team is requested to review the proposed merge of 
lp:~jmwilson/duplicity/capabilities into lp:duplicity.
=== modified file 'README'
--- README	2014-10-18 19:44:29 +0000
+++ README	2015-04-27 00:30:33 +0000
@@ -23,6 +23,7 @@
  * librsync v0.9.6 or later
  * GnuPG v1.x for encryption
  * python-lockfile for concurrency locking
+ * python-capng for managing capabilities when run as root
  * for scp/sftp -- python-paramiko and python-pycryptopp
  * for ftp -- lftp version 3.7.15 or later
  * Boto 2.0 or later for single-processing S3 or GCS access (default)

=== modified file 'bin/duplicity'
--- bin/duplicity	2015-03-09 18:50:58 +0000
+++ bin/duplicity	2015-04-27 00:30:33 +0000
@@ -38,6 +38,8 @@
 import resource
 import re
 import threading
+import capng
+import pwd
 from datetime import datetime
 from lockfile import FileLock
 
@@ -1340,12 +1342,18 @@
 See https://bugs.launchpad.net/duplicity/+bug/931175
 """), log.ErrorCode.pythonoptimize_set)
 
-    # if python is run setuid, it's only partway set,
-    # so make sure to run with euid/egid of root
+    # if python is running as root, then drop all capabilities except
+    # unrestricted read access and then change user to prevent regaining
+    # capabilities via execve.
     if os.geteuid() == 0:
-        # make sure uid/gid match euid/egid
-        os.setuid(os.geteuid())
-        os.setgid(os.getegid())
+        user = pwd.getpwnam("duplicity")
+        capng.capng_clear(capng.CAPNG_SELECT_CAPS)
+        if (capng.capng_update(capng.CAPNG_ADD,
+                              capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
+                              capng.CAP_DAC_READ_SEARCH)
+            or capng.capng_change_id(user.pw_uid, user.pw_gid,
+                                     capng.CAPNG_DROP_SUPP_GRP)):
+            log.FatalError("Unable to drop root privileges.")
 
     # set the current time strings (make it available for command line processing)
     dup_time.setcurtime()

=== modified file 'debian/control'
--- debian/control	2014-10-27 14:15:52 +0000
+++ debian/control	2015-04-27 00:30:33 +0000
@@ -27,6 +27,7 @@
          gnupg,
          python-lockfile,
          python-pexpect,
+         python-capng,
 Suggests: ncftp,
           python-boto,
           python-paramiko,

=== added file 'debian/duplicity.postinst'
--- debian/duplicity.postinst	1970-01-01 00:00:00 +0000
+++ debian/duplicity.postinst	2015-04-27 00:30:33 +0000
@@ -0,0 +1,7 @@
+#!/bin/sh -e
+
+if [ "$1" = "configure" ]; then
+  if ! getent passwd duplicity >/dev/null; then
+    adduser --quiet --system --no-create-home --home /nonexistant --shell /usr/sbin/nologin duplicity
+  fi
+fi

_______________________________________________
Mailing list: https://launchpad.net/~duplicity-team
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~duplicity-team
More help   : https://help.launchpad.net/ListHelp

Reply via email to