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.
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.
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.
There is an os.setuid()
method. You can use it to change the current user for this script.
One solution is, somewhere where the child starts, to call os.setuid()
and os.setgid()
to change the user and group id and after that call one of the os.exec* methods to spawn a new child. The newly spawned child will run with the less powerful user without the ability to become a more powerful one again.
Another is to do it when the daemon (the master process) starts and then all newly spawned processes will have run under the same user.
For information look at the manpage for setuid.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With