从一个长时间运行的Python进程中以不同用户身份运行子进程

55

我有一个长时间运行的、守护进程的 Python 进程,当发生某些事件时,它使用 subprocess 来生成新的子进程。长时间运行的进程是由具有超级用户特权的用户启动的。我需要它生成的子进程以另一个用户(例如"nobody")身份运行,同时为父进程保留超级用户权限。

我目前正在使用

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

但这种方法看起来很笨重,而且退出不够干净。

是否有一种编程方式可以实现这个目标,而不是使用su?我正在查看os.set*uid方法,但Python标准库中该领域的文档非常稀少。

4个回答

100

由于您提到了一个守护进程,我可以推断出您正在运行类Unix操作系统。这很重要,因为如何做这取决于操作系统的类型。此答案仅适用于Unix,包括Linux和Mac OS X。

  1. 定义一个函数,该函数将设置正在运行的进程的gid和uid。
  2. 将此函数作为preexec_fn参数传递给subprocess.Popen

subprocess.Popen将使用fork/exec模型来使用您的preexec_fn。这相当于按顺序调用os.fork()、preexec_fn()(在子进程中)和os.exec()(在子进程中)。由于os.setuid、os.setgid和preexec_fn都仅受支持于Unix,因此此解决方案不可移植到其他类型的操作系统。

以下代码是一个脚本(Python 2.4+),演示了如何执行此操作:

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()
你可以这样调用这个脚本:

以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

如何在子进程中降低权限,避免成为root用户?

(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

当子进程退出时,父进程会回到根目录。

(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

注意,让父进程等待子进程退出仅供演示目的。我这样做是为了让父进程和子进程可以共享一个终端。守护程序将没有终端,也很少等待子进程退出。


1
“很少等待子进程退出”可能导致许多僵尸进程(长时间运行的父进程,短暂的子进程)。 - jfs
5
可能很明显(至少对我来说不是),但是......像示例中所示,您应该先更改“gid”! - Jamie
6
请注意上述示例仅使用用户的主要gid。如果您想要使用用户的所有组,则可以使用**os.initgroups(user_name, user_gid)**而不是os.setgid。这需要将用户名传递到demote()函数中。 - storm_m2138
是否有可能在我的账户下运行Python脚本,在进程中切换到root,然后以另一个用户的身份调用命令? - whatsnext
我感到有必要重申 @Jamie 的评论。如果你遇到了异常:SubprocessError Exception occurred in preexec_fn,请确保在 def result() 函数中,os.setgid(user_gid)os.setuid(user_uid) 之前 。在运行 Python 3.7.7Fedora 中,顺序无关紧要(即两种方式都可以工作);但是在运行 Python 3.7.7Ubuntu Bionic 上,这一点很重要! 真奇怪。所以请安全起见,只使用上述顺序。希望这能帮助其他搜索者。 - NYCeyes

16
Python的新版本(从3.9开始)内置支持usergroup选项,详情请参见文档
process = subprocess.Popen(args, user=username)

新版本还提供了一个subprocess.run函数,它是对subprocess.Popen的简单包装。而suprocess.Popen在后台运行命令,subprocess.run则运行命令并等待它们完成。
因此我们也可以执行以下操作:
subprocess.run(args, user=username)

12

有一个 os.setuid() 方法,你可以使用它来为脚本更改当前用户。

其中一种解决方案是,在子进程开始的某个位置调用os.setuid()os.setgid()方法来更改用户和组ID,然后调用os.exec*方法之一来生成一个新的子进程。新生成的子进程将以较低权限的用户运行,无法再次成为更高权限的用户。

另一种解决方案是在守护进程(主进程)启动时执行设置,然后所有新生成的进程都将在同一个用户下运行。

如需了解更多信息,请参阅setuid的man页


3
如果您要切换到具有其他gid的用户,则可能还需要使用os.setgroups()。除此之外,这很简单明了。 - bobince
1
跟进:旨在以nobody身份运行的进程是不受信任的第三方应用程序。我不能指望它们切换到另一个uid/gid。当守护进程启动时,我也不能永久地将其切换到另一个uid/gid,因为除了启动这些子进程之外,它仍然需要超级用户权限来执行其他操作。以下方案可行吗?
  1. 以超级用户身份启动守护进程。
  2. 当守护进程即将启动子进程时,降为nobody用户。确保子进程无法再次成为超级用户。
  3. 启动子进程后,将守护进程切换回超级用户权限。
- Peter Parente
1
不,一旦您成为一个权限较低的用户,就没有回头路了。我已经编辑了上面的帖子,应该适合您——看看第一个选项。 - Emil Ivanov
2
subprocess.Popen函数有一个preexec_func参数,可以用于实现Emil建议的双重子进程生成。preexec_func可以在第一个启动的子进程的上下文中调用os.setgid和os.setuid,然后以该用户身份启动第二个子进程。 - Peter Parente
2
在 Linux 上不是这样的。在完成后设置有效 UID,然后将其设置回真实用户 ID。 - Matt Joiner
我已经使用多进程模块从守护程序完成了这个任务,以运行一个Python函数作为子进程,然后调用subprocess.Popen()。请查看这里的多进程示例:https://dev59.com/gnE85IYBdhLWcg3w2HXs。在target()函数中,添加您对os.setgid()和os.setuid()的调用。 - tahoar

3

实际上,使用preexec_fn的示例对我不起作用。
我的解决方案是从另一个用户运行一些shell命令并获取其输出,它目前可以正常工作:

apipe=subprocess.Popen('sudo -u someuser /execution',shell=True,stdout=subprocess.PIPE)

然后,如果您需要从进程的标准输出中读取:
cond=True
while (cond):
  line=apipe.stdout.getline()
  if (....):
    cond=False

希望这不仅对我有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接