← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~jmwilson/duplicity/capabilities into lp:duplicity

 

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


Follow ups