Python中放弃root权限

49
我希望能够编写一个Python程序,并在侦听80端口后以非root权限执行。有没有方法在不使用root权限的情况下获得端口80或降低权限?

3
有没有一种方法让非root进程在Linux上绑定到特权端口(1024及以下)? - Ian Bicking
5
在现代Linux上,您只需要capability CAP_NET_BIND_SERVICE就可以绑定到端口80,不需要使用root权限,甚至在应用程序启动时也是如此。Capabilities是POSIX标准1003.1e的一部分,它将强大的root特权分割成一组不同的特权。请参见:python-cap-ng和/sbin/setcap、/sbin/getcap(这些与chmod setuid和ls -l相当)。 - ctrl-alt-delor
对于 Python2 和其他一些解释器,你需要小心地获取权限 -- libcap-ng 可以删除能力,但它不能授予能力。回答 Ian 引用的问题的方法比较安全,可以逐个分配特定项目所需的能力:https://dev59.com/_nRC5IYBdhLWcg3wFdBx#21895123 - duanev
6个回答

58

如果没有 root 权限,你将无法在端口 80 上打开服务器,这是操作系统级别的限制。因此,唯一的解决方案是在打开端口后降低权限。

以下是 Python 中降低权限的可能解决方案:Python 中的降低权限。这通常是一个不错的解决方案,但你还需要将 os.setgroups([]) 添加到函数中,以确保不保留 root 用户的组成员身份。

我稍微修改了代码,并删除了日志记录和异常处理程序,因此你需要妥善处理 OSError(当进程不允许切换有效 UID 或 GID 时会引发该异常):

import os, pwd, grp

def drop_privileges(uid_name='nobody', gid_name='nogroup'):
    if os.getuid() != 0:
        # We're not root so, like, whatever dude
        return

    # Get the uid/gid from the name
    running_uid = pwd.getpwnam(uid_name).pw_uid
    running_gid = grp.getgrnam(gid_name).gr_gid

    # Remove group privileges
    os.setgroups([])

    # Try setting the new uid/gid
    os.setgid(running_gid)
    os.setuid(running_uid)

    # Ensure a very conservative umask
    old_umask = os.umask(077)

2
请注意,HOME目录仍将是/root而不是/home/uid_name,uid_name将无法对~/执行任何操作,这将扩展为/root/。这可能会影响像matplotlib这样在HOME目录中存储配置数据的模块。 authbind似乎是处理此问题的正确方法。 - Daniel F
1
而且 HOME 变量不会是唯一一个仍然认为当前用户是 root。 - Daniel F
2
“如果没有 root 权限,您将无法在端口 80 上打开服务器…” - 这并不一定是真的(也许不再是了?)。请参阅 SuperUser 上的 允许非 root 进程绑定到端口 80 和 443 吗?Linux 上有没有办法让非 root 进程绑定到“特权”端口? - jww

13

使用authbind的示例 - http://goo.gl/fxFde6 - 将NodeJS替换为您喜欢的任何内容(例如:python) - starlocke
https://mutelight.org/authbind 上的示例很好地解释了 /etc/authbind/byport/80 上的所有权/权限。 - mwfearnley
starlocke的链接指向:http://web.archive.org/web/20141205194844/http://respectthecode.tumblr.com/post/16461876216/using-authbind-to-run-node-js-on-port-80-with,它只是关于绑定端口,而不是关于放弃root权限。 - Luc

5

我需要降低权限时,要求用户每次输入用户名和组不是个好主意。这里是Tamás代码稍作修改版,它将降低权限并切换到启动sudo命令的用户。我假设您正在使用sudo(如果没有,请使用Tamás的代码)。

#!/usr/bin/env python3

import os, pwd, grp

#Throws OSError exception (it will be thrown when the process is not allowed
#to switch its effective UID or GID):
def drop_privileges():
    if os.getuid() != 0:
        # We're not root so, like, whatever dude
        return

    # Get the uid/gid from the name
    user_name = os.getenv("SUDO_USER")
    pwnam = pwd.getpwnam(user_name)

    # Remove group privileges
    os.setgroups([])

    # Try setting the new uid/gid
    os.setgid(pwnam.pw_gid)
    os.setuid(pwnam.pw_uid)

    #Ensure a reasonable umask
    old_umask = os.umask(0o22)


#Test by running...
#./drop_privileges
#sudo ./drop_privileges
if __name__ == '__main__':
    print(os.getresuid())
    drop_privileges()
    print(os.getresuid())

1
我不喜欢这个方法。这种方法假定存在SUDO_USER,只有在通过sudo运行时才会设置。如果您通过systemd启动或直接通过root运行,则不适用于降低特权。是的,您可以通过设置“SUDO_USER = someone”使其工作,但这是一种hack。更常见的情况是守护程序以root身份启动,执行所需的特权工作来设置事物,然后降级为“nobody”或通过其配置指定的用户进行日常工作。 - nevelis

5
  1. 如果你通过systemd启动程序,systemd可以将已经打开的监听套接字移交给它,并且在第一次连接时激活你的程序,而且你甚至不需要将其作为守护进程。

  2. 如果你要选择独立的方法,你需要具备CAP_NET_BIND_SERVICE的能力(请查看capabilities man page)。这可以通过正确的命令行工具逐个程序完成,或者通过使你的应用程序(1)成为suid root (2)启动(3)监听端口(4)立即降低权限/能力来实现。

请记住,suid root程序有很多安全考虑因素(干净和安全的环境、umask、特权、资源限制等等都是你的程序必须正确设置的事情)。如果你可以使用类似systemd的东西,那就更好了。


4
以下是Tamás's answer的进一步改编,具体更改如下:
  • 使用python-prctl模块将Linux权限降为指定的权限列表。
  • 用户可以选择作为参数传递(默认查找运行sudo的用户)。
  • 设置所有用户的组和HOME
  • 可选更改目录。

(我相对于使用这个功能还比较新,可能会漏掉一些东西。它可能无法在旧内核(<3.8)或禁用文件系统权限的内核上工作。)

def drop_privileges(user=None, rundir=None, caps=None):
    import os
    import pwd

    if caps:
        import prctl

    if os.getuid() != 0:
        # We're not root
        raise PermissionError('Run with sudo or as root user')

    if user is None:
        user = os.getenv('SUDO_USER')
        if user is None:
            raise ValueError('Username not specified')
    if rundir is None:
        rundir = os.getcwd()

    # Get the uid/gid from the name
    pwnam = pwd.getpwnam(user)

    if caps:
        prctl.securebits.keep_caps=True
        prctl.securebits.no_setuid_fixup=True

    # Set user's group privileges
    os.setgroups(os.getgrouplist(pwnam.pw_name, pwnam.pw_gid))

    # Try setting the new uid/gid
    os.setgid(pwnam.pw_gid)
    os.setuid(pwnam.pw_uid)

    os.environ['HOME'] = pwnam.pw_dir

    os.chdir(os.path.expanduser(rundir))

    if caps:
        prctl.capbset.limit(*caps)
        try:
            prctl.cap_permitted.limit(*caps)
        except PermissionError:
            pass
        prctl.cap_effective.limit(*caps)

    #Ensure a reasonable umask
    old_umask = os.umask(0o22)

它可以按如下方式使用:

drop_privileges(user='www', rundir='~', caps=[prctl.CAP_NET_BIND_SERVICE])

2
大部分情况下,这个方法是有效的,除非你需要在完成其他不需要超级用户权限的任务后请求套接字。
我之前制作了一个名为tradesocket的项目。它允许在 POSIX 系统上的进程之间传递套接字。我的做法是在开始时启动一个保持超级用户权限的进程,而其余进程则降低权限并从该进程请求套接字。

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