【问题标题】:Run child processes as different user from a long running Python process以与长时间运行的 Python 进程不同的用户身份运行子进程
【发布时间】:2010-12-18 17:14:34
【问题描述】:

我有一个长时间运行的守护程序 Python 进程,当某些事件发生时,它使用子进程生成新的子进程。长时间运行的进程由具有超级用户权限的用户启动。我需要它生成的子进程作为不同的用户(例如“nobody”)运行,同时保留父进程的超级用户权限。

我正在使用

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

但这似乎是重量级的,并没有死得很干净。

有没有办法以编程方式而不是使用 su 来完成此任务?我正在查看 os.set*uid 方法,但 Python std lib 中的文档在该区域非常稀疏。

【问题讨论】:

    标签: python fork subprocess setuid


    【解决方案1】:

    既然你提到了一个守护进程,我可以断定你是在一个类 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
    

    当子进程退出时,我们回到父进程的root ...

    (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
    

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

    【讨论】:

    • “很少等待子进程退出”可能会导致许多僵尸进程(长寿命的父进程,短寿命的子进程)。
    • 这可能很明显,(不适合我),但是...您应该首先更改gid,如示例所示!
    • 注意上面的例子只使用了用户的primary gid,如果你想使用ALL的用户组,那么你可以使用os .initgroups(user_name, user_gid) 而不是 os.setgid。这需要将用户名传递给 demote()
    • 是否还可以在我自己的账户下运行python脚本,在进程内切换到root,然后以另一个用户的身份调用命令?
    • 我不得不重申@Jamie 的评论。如果遇到异常:SubprocessError Exception occurred in preexec_fn,请确保在def result() 函数中os.setgid(user_gid) 出现在之前 os.setuid(user_uid)。在运行Python 3.7.7Fedora 上,顺序无关紧要(即无论哪种方式都有效);但是在 Ubuntu Bionic 运行 Python 3.7.7 时,这很重要!奇怪但真实。所以要安全,只需使用上面的顺序。希望对其他搜索者有所帮助。
    【解决方案2】:

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

    一种解决方案是,在孩子开始的地方,调用os.setuid()os.setgid() 来更改用户和组ID,然后调用os.exec* 方法之一来生成一个新孩子。新生成的孩子将与较弱的用户一起运行,而无法再次成为更强大的用户。

    另一种方法是在守护进程(主进程)启动时执行此操作,然后所有新生成的进程都将在同一用户下运行。

    有关信息,请查看manpage for setuid

    【讨论】:

    • 如果您要切换到具有其他 gid 的用户,您可能还需要 os.setgroups()。除此之外,是的,它非常简单。
    • 跟进:旨在以无人身份运行的进程是不受信任的第三方应用程序。我不能指望他们切换到另一个 uid/gid。我也无法在守护进程启动时将其永久切换到另一个 uid/gid,因为除了启动这些子进程之外,它仍然需要某些操作的超级用户权限。以下可能吗? 1. 以超级用户身份启动守护进程。 2. 当守护进程即将启动一个子进程时,放到nobody 用户。确保孩子不能再次成为超级用户。 3.启动子进程后,将守护进程切换回超级用户权限。
    • 没有。一旦你成为一个不那么强大的用户,就没有回头路了。我已经编辑了上面应该适合你的帖子 - 看看第一个选项。
    • subprocess.Popen 函数有一个 preexec_func 参数,可用于完成 Emil 建议的双子生成。 preexec_func 可以在第一个启动的孩子的上下文中调用 os.setgid 和 os.setuid,然后以该用户的身份启动第二个。
    • 这在 Linux 上是不正确的。设置有效的 UID,然后在完成后将其设置回真实的用户 ID。
    【解决方案3】:

    实际上,preexec_fn 的示例对我不起作用。
    我的解决方案可以很好地从另一个用户运行一些 shell 命令并获取其输出:

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

    那么,如果需要从进程stdout中读取:

    cond=True
    while (cond):
      line=apipe.stdout.getline()
      if (....):
        cond=False
    

    希望,它不仅对我有用。

    【讨论】:

      【解决方案4】:

      Python 的新版本(3.9 以上)支持usergroup 选项out of the box

      process = subprocess.Popen(args, user=username)
      

      新版本还提供了subprocess.run 功能。它是subprocess.Popen 的简单包装器。 suprocess.Popen 在后台运行命令,subprocess.run 运行命令等待它们完成。

      因此我们也可以这样做:

      subprocess.run(args, user=username)
      

      【讨论】:

        【解决方案5】:

        在 sudo 上启用用户不需要密码

        username ALL=(ALL) NOPASSWD: ALL
        

        然后用sudo调用根函数,例如:

        import pexpect
        child = pexpect.spawn('sudo apachectl restart')
        for i in child: print i #if you want to see the output from the process
        

        【讨论】:

        • 这很危险。 执行具有受限用户权限的程序,然后为该用户提供sudo 对所有内容的权限是没有意义的;这并没有提供任何防止滥用进程的安全性,并在控制台上为滥用打开了一个漏洞。如果需要sudo,您至少应该将其限制为本地主机和所需的命令/二进制文件,例如。 G。 &lt;username&gt; &lt;hostname&gt; = (root) NOPASSWD: /sbin/mkfs.ext4
        • 你说的原则上没问题,权限限制是可能的,正如你所展示的。有时你需要做一些事情,比如从一个受信任的非 root 帐户重新启动 Apache;这是一般方法,您的改进改进了它。谢谢。
        • 极度危险!
        猜你喜欢
        • 2012-11-29
        • 1970-01-01
        • 1970-01-01
        • 2020-08-01
        • 2020-11-16
        • 2010-10-03
        • 1970-01-01
        • 1970-01-01
        • 2011-08-31
        相关资源
        最近更新 更多