Skip to content

Dropping Privileges in Python for Tornado Apps

by Charles Hooper on May 8th, 2010

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.

 
Ad: Secret Server
6 Comments
  1. George permalink

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

  2. Charles Hooper permalink

    Bookmarked and saved, I will read this soon!

  3. Charles Hooper permalink

    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 permalink

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

  5. Charles Hooper permalink

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

  6. 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

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS