Dropping Root Permissions In Python

18,866

Solution 1

You won't be able to open a server on port 80 without root privileges, this is a restriction on the OS level. So the only solution is to drop root privileges after you have opened the port.

Here is a possible solution to drop root privileges in Python: Dropping privileges in Python. This is a good solution in general, but you'll also have to add os.setgroups([]) to the function to ensure that the group membership of the root user is not retained.

I copied and cleaned up the code a little bit, and removed logging and the exception handlers so it is left up to you to handle OSError properly (it will be thrown when the process is not allowed to switch its effective UID or 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)

Solution 2

I recommend using authbind to start your Python program, so none of it has to run as root.

https://en.wikipedia.org/wiki/Authbind

Solution 3

It is not a good idea to ask the user to enter his/her user-name and group whenever I need to drop privileges. Here is a slightly modified version of Tamás's code which will drop privileges and switch to the user who initiated the sudo command. I am assuming you are using sudo (if not, use Tamás's code).

#!/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())

Solution 4

  1. systemd can do it for you, if you start your program through systemd, systemd can hand off the already-open listening socket to it, and it can also activate your program on first connection. and you don't even need to daemonize it.

  2. If you are going to go with the standalone approach, you need the capability CAP_NET_BIND_SERVICE (check capabilities man page). This can be done on a program-by-program basis with the correct command line tool, or by making your application (1) be suid root (2) start up (3) listen to the port (4) drop privileges / capabilities immediately.

Remember that suid root programs come with lots of security considerations (clean and secure environment, umask, privileges, rlimits, all those things are things that your program is going to have to set up correctly). If you can use something like systemd, all the better then.

Solution 5

The following is a further adaptation of Tamás's answer, with the following changes:

  • Use the python-prctl module to drop Linux capabilities to a specified list of capabilities to preserve.
  • The user can optionally be passed as a parameter (it defaults to looking up the user who ran sudo).
  • It sets all the user's groups and HOME.
  • It optionally changes directory.

(I'm relatively new to using this functionality, however, so I may have missed something. It might not work on older kernels (<3.8) or kernels with filesystem capabilities disabled.)

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)

It can be used as follows:

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

Related videos on Youtube

Jeremy
Author by

Jeremy

████ ████ ███████ ██ ███ ██████ █ ████ █ ███ ██ ████ ██ ██ ███ █ ████ ████ ███████ █ ███ █████ ██ █ ███ ████ ████ ███████ ███████ ███ ██ ██████ [mailto:[email protected]](https://mailto.jeremy.ca)

Updated on June 22, 2020

Comments

  • Jeremy
    Jeremy almost 4 years

    I'd like to have a Python program start listening on port 80, but after that execute without root permissions. Is there a way to drop root or to get port 80 without it?

    • Ian Bicking
      Ian Bicking about 14 years
    • ctrl-alt-delor
      ctrl-alt-delor over 10 years
      On modern Linux you only need capability CAP_NET_BIND_SERVICE to bind to port 80, you DO NOT need to be root, not even at application startup. Capabilities is a POSIX standard, 1003.1e, that is a partitioning of the all powerful root privilege into a set of distinct privileges. See: python-cap-ng And /sbin/setcap, /sbin/getcap (these are equivalent to chmod setuid, and ls –l)
    • duanev
      duanev about 10 years
      For Python2 and perhaps other interpreters, gaining capabilities is the part you want to be careful with -- libcap-ng can drop caps but it doesn't grant them. This answer to the question Ian referenced is a relatively safe way to dole out one cap at a time for specific projects: stackoverflow.com/a/21895123/1724577
  • Daniel F
    Daniel F about 10 years
    Keep in mind that the HOME directory will still be /root and not /home/uid_name, and that uid_name won't be able to do anything with ~/, which will then expand to /root/. This can affect modules like matplotlib which store configuration data in the HOME directory. authbind seems to be the correct way to handle this issue.
  • Daniel F
    Daniel F about 10 years
    And the HOME variable won't be the only one still thinking that the current user is root.
  • starlocke
    starlocke about 9 years
    An example of how to use authbind - goo.gl/fxFde6 - Replace NodeJS with whatever you like (ex: python)
  • jww
    jww almost 7 years
    "You won't be able to open a server on port 80 without root privileges..." - That's not necessarily true (maybe anymore?). Also see Allow non-root process to bind to port 80 and 443? on SuperUser and Is there a way for non-root processes to bind to “privileged” ports on Linux?
  • mwfearnley
    mwfearnley about 6 years
    The example at mutelight.org/authbind does a good job of explaining the ownership/permissions on /etc/authbind/byport/80.
  • nevelis
    nevelis almost 5 years
    I don't like this. This approach assumes the existence of SUDO_USER, which is only set if you run via sudo. It's not suitable for dropping privileges when starting via systemd or if you run directly via root. Yeah you could make it work by setting SUDO_USER=someone but this is a hack. It's much more common for daemons to start as root, do the privileged work they need to set things up, then drop to 'nobody' or a user specified via their configuration for their day job.
  • Luc
    Luc about 2 years
    starlocke's link goes to: web.archive.org/web/20141205194844/http://… and it's just about binding to a port, not about dropping root permissions.