Today, I’m going to show you how to drop from the root user to an unprivileged user in Python for the purpose of running a Tornado app. First make a system user for your project to run as. In my example, I’ll be using projectuser as the username. Creating this user can be done like so:


sudo useradd --system --user-group projectuser

Now, in your script that is responsible for starting your Tornado app, you likely have something that probably looks like the following:


if __name__ == "__main__": 
    http_server = tornado.httpserver.HTTPServer(application) 
    http_server.listen(port) 
    tornado.ioloop.IOLoop.instance().start()

What we need to do now is define a user to run as and then drop privileges using a call to setuid. We can do this by replacing the above with:


if __name__ == "__main__":
    import os
    import pwd

    # define user to run as
    run_as_user = "projectuser"

    # drop privileges
    uid = pwd.getpwnam(run_as_user)[2]
    os.setuid(uid)

    # start tornado app
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(port)
    tornado.ioloop.IOLoop.instance().start()

And voila, your app should now run as the user you defined! Do note that only the root user can call setuid. As a result, your script now needs to be run using sudo or from an upstart startup script, for example.

One caveat is that you won’t be able to use port numbers below 1024 since you are dropping to an unprivileged user before binding to the port. I think there’s a way to get around this by replacing http_server.listen() with http_server.bind(), http_server.start(), and dropping privileges between those calls, but this remains untested for now. Alternatively, you could use the respective proxy modules for Lighttpd or nginx to listen on privileged ports.



6 Comments to “Dropping Privileges in Python for Tornado Apps”

  1. George says:

    You might want to read the following paper (Setuid Demystified): http://www.eecs.berkeley.edu/~daw/papers/setuid-usenix02.pdf

  2. Charles Hooper says:

    Bookmarked and saved, I will read this soon!

  3. Charles Hooper says:

    George,Are there any sections of the paper Setuid Demystified that I should look over in regards to this blog post? I still plan on reading the paper but my reading queue is backed up a bit at the moment

  4. Japhy Bartlett says:

    have you tried calling the setuid after .listen() but before .start() ?

  5. Charles Hooper says:

    @Japhy, I hadn’t, but I just tried now. socket.error gets raised with permission denied as the message

  6. Aaron Zinman says:

    I’m able to start as root and bind to <1024 before dropping UID.

    My starting code with process forking…

    def startServer():
      logging.info("Starting HTTP listening loop on port %d" % options.port)
      http_server = tornado.httpserver.HTTPServer(Application(), no_keep_alive=True, xheaders=True)
      http_server.bind(options.port)
      http_server.start(num_processes=None)
      import os, pwd
      try:
        uid = pwd.getpwnam("www-data")[2]
        os.setuid(uid)
        logging.info("Set uid to www-data")
      except:
        logging.error("Could not set uid to www-data")
      tornado.ioloop.IOLoop.instance().start()

Leave a Reply