pythonforksubprocesssetuid

Run child processes as different user from a long running Python process


I've got a long running, daemonized Python process that uses subprocess to spawn new child processes when certain events occur. The long running process is started by a user with super user privileges. I need the child processes it spawns to run as a different user (e.g., "nobody") while retaining the super user privileges for the parent process.

I'm currently using

su -m nobody -c <program to execute as a child>

but this seems heavyweight and doesn't die very cleanly.

Is there a way to accomplish this programmatically instead of using su? I'm looking at the os.set*uid methods, but the doc in the Python std lib is quite sparse in that area.


Solution

  • Since you mentioned a daemon, I can conclude that you are running on a Unix-like operating system. This matters, because how to do this depends on the kind operating system. This answer applies only to Unix, including Linux, and Mac OS X.

    1. Define a function that will set the gid and uid of the running process.
    2. Pass this function as the preexec_fn parameter to subprocess.Popen

    subprocess.Popen will use the fork/exec model to use your preexec_fn. That is equivalent to calling os.fork(), preexec_fn() (in the child process), and os.exec() (in the child process) in that order. Since os.setuid, os.setgid, and preexec_fn are all only supported on Unix, this solution is not portable to other kinds of operating systems.

    The following code is a script (Python 2.4+) that demonstrates how to do this:

    import os
    import pwd
    import subprocess
    import sys
    
    
    def main(my_args=None):
        if my_args is None: my_args = sys.argv[1:]
        user_name, cwd = my_args[:2]
        args = my_args[2:]
        pw_record = pwd.getpwnam(user_name)
        user_name      = pw_record.pw_name
        user_home_dir  = pw_record.pw_dir
        user_uid       = pw_record.pw_uid
        user_gid       = pw_record.pw_gid
        env = os.environ.copy()
        env[ 'HOME'     ]  = user_home_dir
        env[ 'LOGNAME'  ]  = user_name
        env[ 'PWD'      ]  = cwd
        env[ 'USER'     ]  = user_name
        report_ids('starting ' + str(args))
        process = subprocess.Popen(
            args, preexec_fn=demote(user_uid, user_gid), cwd=cwd, env=env
        )
        result = process.wait()
        report_ids('finished ' + str(args))
        print 'result', result
    
    
    def demote(user_uid, user_gid):
        def result():
            report_ids('starting demotion')
            os.setgid(user_gid)
            os.setuid(user_uid)
            report_ids('finished demotion')
        return result
    
    
    def report_ids(msg):
        print 'uid, gid = %d, %d; %s' % (os.getuid(), os.getgid(), msg)
    
    
    if __name__ == '__main__':
        main()
    

    You can invoke this script like this:

    Start as root...

    (hale)/tmp/demo$ sudo bash --norc
    (root)/tmp/demo$ ls -l
    total 8
    drwxr-xr-x  2 hale  wheel    68 May 17 16:26 inner
    -rw-r--r--  1 hale  staff  1836 May 17 15:25 test-child.py
    

    Become non-root in a child process...

    (root)/tmp/demo$ python test-child.py hale inner /bin/bash --norc
    uid, gid = 0, 0; starting ['/bin/bash', '--norc']
    uid, gid = 0, 0; starting demotion
    uid, gid = 501, 20; finished demotion
    (hale)/tmp/demo/inner$ pwd
    /tmp/demo/inner
    (hale)/tmp/demo/inner$ whoami
    hale
    

    When the child process exits, we go back to root in parent ...

    (hale)/tmp/demo/inner$ exit
    exit
    uid, gid = 0, 0; finished ['/bin/bash', '--norc']
    result 0
    (root)/tmp/demo$ pwd
    /tmp/demo
    (root)/tmp/demo$ whoami
    root
    

    Note that having the parent process wait around for the child process to exit is for demonstration purposes only. I did this so that the parent and child could share a terminal. A daemon would have no terminal and would seldom wait around for a child process to exit.