Browse Source

Optionally drop priviliges in the boss process.

This involves adding a "-u" flag, then invoking setuid() if passed.

A set of tests for this were also added.

See Trac ticket #180 for more details:

http://bind10.isc.org/ticket/180



git-svn-id: svn://bind10.isc.org/svn/bind10/trunk@2330 e5f2f494-b856-4b98-b285-d166d9295462
Shane Kerr 15 years ago
parent
commit
dfc50e2853
3 changed files with 245 additions and 30 deletions
  1. 109 29
      src/bin/bind10/bind10.py.in
  2. 134 0
      src/bin/bind10/tests/args_test.py
  3. 2 1
      src/bin/bind10/tests/bind10_test.in

+ 109 - 29
src/bin/bind10/bind10.py.in

@@ -57,6 +57,9 @@ import time
 import select
 import random
 from optparse import OptionParser, OptionValueError
+import io
+import pwd
+import posix
 
 import isc.cc
 
@@ -108,21 +111,38 @@ to avoid being restarted at exactly 10 seconds."""
             when = time.time()
         return max(when, self.restart_time)
 
+class ProcessInfoError(Exception): pass
+
 class ProcessInfo:
     """Information about a process"""
 
     dev_null = open(os.devnull, "w")
 
     def __init__(self, name, args, env={}, dev_null_stdout=False,
-                 dev_null_stderr=False):
+                 dev_null_stderr=False, uid=None, username=None):
         self.name = name 
         self.args = args
         self.env = env
         self.dev_null_stdout = dev_null_stdout
         self.dev_null_stderr = dev_null_stderr
         self.restart_schedule = RestartSchedule()
+        self.uid = uid
+        self.username = username
         self._spawn()
 
+    def _setuid(self):
+        """Function used before running a program that needs to run as a
+        different user."""
+        if self.uid is not None:
+            try:
+                posix.setuid(self.uid)
+            except OSError as e:
+                if e.errno == errno.EPERM:
+                    # if we failed to change user due to permission report that
+                    raise ProcessInfoError("Unable to change to user %s (uid %d)" % (self.username, self.uid))
+                else:
+                    # otherwise simply re-raise whatever error we found
+                    raise
 
     def _spawn(self):
         if self.dev_null_stdout:
@@ -138,14 +158,15 @@ class ProcessInfo:
         # on construction (self.env).
         spawn_env = os.environ
         spawn_env.update(self.env)
-        if not 'B10_FROM_SOURCE' in os.environ:
+        if 'B10_FROM_SOURCE' not in os.environ:
             spawn_env['PATH'] = "@@LIBEXECDIR@@:" + spawn_env['PATH']
         self.process = subprocess.Popen(self.args,
                                         stdin=subprocess.PIPE,
                                         stdout=spawn_stdout,
                                         stderr=spawn_stderr,
                                         close_fds=True,
-                                        env=spawn_env,)
+                                        env=spawn_env,
+                                        preexec_fn=self._setuid)
         self.pid = self.process.pid
         self.restart_schedule.set_run_start_time()
 
@@ -155,7 +176,8 @@ class ProcessInfo:
 class BoB:
     """Boss of BIND class."""
     
-    def __init__(self, msgq_socket_file=None, auth_port=5300, verbose=False):
+    def __init__(self, msgq_socket_file=None, auth_port=5300, verbose=False,
+                 setuid=None, username=None):
         """Initialize the Boss of BIND. This is a singleton (only one
         can run).
         
@@ -171,6 +193,8 @@ class BoB:
         self.processes = {}
         self.dead_processes = {}
         self.runnable = False
+        self.uid = setuid
+        self.username = username
 
     def config_handler(self, new_config):
         if self.verbose:
@@ -225,12 +249,14 @@ class BoB:
                 sys.stdout.write("[bind10] Starting b10-msgq\n")
         try:
             c_channel = ProcessInfo("b10-msgq", ["b10-msgq"], c_channel_env,
-                                    True, not self.verbose)
+                                    True, not self.verbose, uid=self.uid,
+                                    username=self.username)
         except Exception as e:
             return "Unable to start b10-msgq; " + str(e)
         self.processes[c_channel.pid] = c_channel
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-msgq (PID %d)\n" % c_channel.pid)
+            sys.stdout.write("[bind10] Started b10-msgq (PID %d)\n" % 
+                             c_channel.pid)
 
         # now connect to the c-channel
         cc_connect_start = time.time()
@@ -250,7 +276,8 @@ class BoB:
             sys.stdout.write("[bind10] Starting b10-cfgmgr\n")
         try:
             bind_cfgd = ProcessInfo("b10-cfgmgr", ["b10-cfgmgr"],
-                                    c_channel_env)
+                                    c_channel_env, uid=self.uid,
+                                    username=self.username)
         except Exception as e:
             c_channel.process.kill()
             return "Unable to start b10-cfgmgr; " + str(e)
@@ -272,23 +299,6 @@ class BoB:
         if self.verbose:
             sys.stdout.write("[bind10] ccsession started\n")
 
-        # start the xfrout before auth-server, to make sure every xfr-query can
-        # be processed properly.
-        xfrout_args = ['b10-xfrout']
-        if self.verbose:
-            sys.stdout.write("[bind10] Starting b10-xfrout\n")
-            xfrout_args += ['-v']
-        try:
-            xfrout = ProcessInfo("b10-xfrout", xfrout_args, 
-                                 c_channel_env )
-        except Exception as e:
-            c_channel.process.kill()
-            bind_cfgd.process.kill()
-            return "Unable to start b10-xfrout; " + str(e)
-        self.processes[xfrout.pid] = xfrout
-        if self.verbose:
-            sys.stdout.write("[bind10] Started b10-xfrout (PID %d)\n" % xfrout.pid)
-
         # start b10-auth
         # XXX: this must be read from the configuration manager in the future
         authargs = ['b10-auth', '-p', str(self.auth_port)]
@@ -308,6 +318,28 @@ class BoB:
         if self.verbose:
             sys.stdout.write("[bind10] Started b10-auth (PID %d)\n" % auth.pid)
 
+        # everything after the authoritative server can run as non-root
+        if self.uid is not None:
+            posix.setuid(self.uid)
+
+        # start the xfrout before auth-server, to make sure every xfr-query can
+        # be processed properly.
+        xfrout_args = ['b10-xfrout']
+        if self.verbose:
+            sys.stdout.write("[bind10] Starting b10-xfrout\n")
+            xfrout_args += ['-v']
+        try:
+            xfrout = ProcessInfo("b10-xfrout", xfrout_args, 
+                                 c_channel_env )
+        except Exception as e:
+            c_channel.process.kill()
+            bind_cfgd.process.kill()
+            return "Unable to start b10-xfrout; " + str(e)
+        self.processes[xfrout.pid] = xfrout
+        if self.verbose:
+            sys.stdout.write("[bind10] Started b10-xfrout (PID %d)\n" % 
+                             xfrout.pid)
+
         # start b10-xfrin
         xfrin_args = ['b10-xfrin']
         if self.verbose:
@@ -324,7 +356,8 @@ class BoB:
             return "Unable to start b10-xfrin; " + str(e)
         self.processes[xfrind.pid] = xfrind
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-xfrin (PID %d)\n" % xfrind.pid)
+            sys.stdout.write("[bind10] Started b10-xfrin (PID %d)\n" % 
+                             xfrind.pid)
 
         # start the b10-cmdctl
         # XXX: we hardcode port 8080
@@ -344,7 +377,8 @@ class BoB:
             return "Unable to start b10-cmdctl; " + str(e)
         self.processes[cmd_ctrld.pid] = cmd_ctrld
         if self.verbose:
-            sys.stdout.write("[bind10] Started b10-cmdctl (PID %d)\n" % cmd_ctrld.pid)
+            sys.stdout.write("[bind10] Started b10-cmdctl (PID %d)\n" % 
+                             cmd_ctrld.pid)
 
         self.runnable = True
 
@@ -435,11 +469,16 @@ class BoB:
                 sys.stdout.write("[bind10] Unknown child pid %d exited.\n" % pid)
 
     def restart_processes(self):
-        """Restart any dead processes."""
+        """Restart any dead processes.
+        Returns the time when the next process is ready to be restarted. 
+          If the server is shutting down, returns 0.
+          If there are no processes, returns None.
+        The values returned can be safely passed into select() as the 
+        timeout value."""
         next_restart = None
         # if we're shutting down, then don't restart
         if not self.runnable:
-            return next_restart
+            return 0
         # otherwise look through each dead process and try to restart
         still_dead = {}
         now = time.time()
@@ -510,6 +549,10 @@ def check_port(option, opt_str, value, parser):
 def main():
     global options
     global boss_of_bind
+    # Enforce line buffering on stdout, even when not a TTY
+    sys.stdout = io.TextIOWrapper(sys.stdout.detach(), line_buffering=True)
+
+
     # Parse any command-line options.
     parser = OptionParser(version=__version__)
     parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
@@ -520,7 +563,42 @@ def main():
     parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
                       type="string", default=None,
                       help="UNIX domain socket file the b10-msgq daemon will use")
+    parser.add_option("-u", "--user", dest="user",
+                      type="string", default=None,
+                      help="Change user after startup (must run as root)")
     (options, args) = parser.parse_args()
+    if args:
+        parser.print_help()
+        sys.exit(1)
+
+    # Check user ID.
+    setuid = None
+    username = None
+    if options.user:
+        # Try getting information about the user, assuming UID passed.
+        try:
+            pw_ent = pwd.getpwuid(int(options.user))
+            setuid = pw_ent.pw_uid
+            username = pw_ent.pw_name
+        except ValueError:
+            pass
+        except KeyError:
+            pass
+
+        # Next try getting information about the user, assuming user name 
+        # passed.
+        # If the information is both a valid user name and user number, we
+        # prefer the name because we try it second. A minor point, hopefully.
+        try:
+            pw_ent = pwd.getpwnam(options.user)
+            setuid = pw_ent.pw_uid
+            username = pw_ent.pw_name
+        except KeyError:
+            pass
+
+        if setuid is None:
+            sys.stderr.write("bind10: invalid user: '%s'\n" % options.user)
+            sys.exit(1)
 
     # Announce startup.
     if options.verbose:
@@ -543,11 +621,12 @@ def main():
 
     # Go bob!
     boss_of_bind = BoB(options.msgq_socket_file, int(options.auth_port),
-                       options.verbose)
+                       options.verbose, setuid, username)
     startup_result = boss_of_bind.startup()
     if startup_result:
         sys.stderr.write("[bind10] Error on startup: %s\n" % startup_result)
         sys.exit(1)
+    sys.stdout.write("[bind10] BIND 10 started\n")
 
     # In our main loop, we check for dead processes or messages 
     # on the c-channel.
@@ -584,6 +663,7 @@ def main():
     # shutdown
     signal.signal(signal.SIGCHLD, signal.SIG_DFL)
     boss_of_bind.shutdown()
+    sys.exit(0)
 
 if __name__ == "__main__":
     main()

+ 134 - 0
src/bin/bind10/tests/args_test.py

@@ -0,0 +1,134 @@
+"""
+This program tests the boss process to make sure that it runs while
+dropping permissions. It must be run as a user that can set permission.
+"""
+import unittest
+import os
+import sys
+import subprocess
+import select
+import time
+import pwd
+
+# Set to a valid user name on the system to run setuid() test
+#SUID_USER=None
+SUID_USER="shane"
+
+BIND10_EXE="../run_bind10.sh"
+TIMEOUT=3
+
+class TestBossArgs(unittest.TestCase):
+    def _waitForString(self, bob, s):
+        found_string = False
+        start_time = time.time()
+        while time.time() < start_time + TIMEOUT:
+            (r,w,x) = select.select((bob.stdout,), (), (), TIMEOUT) 
+            if bob.stdout in r:
+                s = bob.stdout.readline()
+                if s == '':
+                    break
+                if s.startswith(s): 
+                    found_string = True
+                    break
+        return found_string
+
+    def testNoArgs(self):
+        """Run bind10 without any arguments"""
+        bob = subprocess.Popen(args=(BIND10_EXE,),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        started_ok = self._waitForString(bob, '[bind10] BIND 10 started')
+        time.sleep(0.1)
+        bob.terminate()
+        bob.wait()
+        self.assertTrue(started_ok)
+
+    def testBadOption(self):
+        """Run bind10 with a bogus option"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "--badoption"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, 'bind10: error: no such option: --badoption')
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 2)
+        self.assertTrue(failed)
+
+    def testArgument(self):
+        """Run bind10 with an argument (this is not allowed)"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "argument"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, 'Usage: bind10 [options]')
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testBadUser(self):
+        """Run bind10 with a bogus user"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "bogus_user"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "bind10: invalid user: 'bogus_user'")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testBadUid(self):
+        """Run bind10 with a bogus user ID"""
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "999999999"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "bind10: invalid user: '999999999'")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testFailSetUser(self):
+        """Try the -u option when we don't run as root"""
+        global SUID_USER
+        if SUID_USER is None:
+            self.skipTest("test needs a valid user (set when run)")
+        if os.getuid() == 0:
+            self.skipTest("test must not be run as root (uid is 0)")
+        # XXX: we depend on the "nobody" user
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", "nobody"),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        failed = self._waitForString(bob, "[bind10] Error on startup: Unable to start b10-msgq; Unable to change to user nobody")
+        time.sleep(0.1)
+        bob.terminate()
+        self.assertTrue(bob.wait() == 1)
+        self.assertTrue(failed)
+
+    def testSetUser(self):
+        """Try the -u option"""
+        global SUID_USER
+        if SUID_USER is None:
+            self.skipTest("test needs a valid user (set when run)")
+        if os.getuid() != 0:
+            self.skipTest("test must run as root (uid is not 0)")
+        if os.geteuid() != 0:
+            self.skipTest("test must run as root (euid is not 0)")
+
+        bob = subprocess.Popen(args=(BIND10_EXE, "-u", SUID_USER),
+                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        started_ok = self._waitForString(bob, '[bind10] BIND 10 started')
+        self.assertTrue(started_ok)
+        ps = subprocess.Popen(args=("ps", "axo", "user,pid"),
+                              stdout=subprocess.PIPE)
+        s = ps.stdout.readline()
+        ps_user = None
+        while True:
+            s = ps.stdout.readline()
+            if s == '': break
+            (user, pid) = s.split()
+            if int(pid) == bob.pid:
+                ps_user = user.decode()
+                break
+        self.assertTrue(ps_user is not None)
+        self.assertTrue(ps_user == SUID_USER)
+        time.sleep(0.1)
+        bob.terminate()
+        x = bob.wait()
+        self.assertTrue(bob.wait() == 0)
+
+if __name__ == '__main__':
+    unittest.main()

+ 2 - 1
src/bin/bind10/tests/bind10_test.in

@@ -27,5 +27,6 @@ PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_srcdir@/src/bin/bind10
 export PYTHONPATH
 
 cd ${BIND10_PATH}/tests
-exec ${PYTHON_EXEC} -O bind10_test.py $*
+${PYTHON_EXEC} -O bind10_test.py $*
+exec ${PYTHON_EXEC} -O args_test.py $*