duplicity-team team mailing list archive
-
duplicity-team team
-
Mailing list archive
-
Message #02907
[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