Secure Database Access using PHP4, Apache, suexec, CGI, PostgreSQL, and peer sameuser

  1. The web server runs as its own user id, www-data.
  2. When a UNIX process runs another process, the child runs as the same user ID as the calling process.
  3. UNIX has a permissions flag on executable files called the setuid bit. If an executable is setuid, the program is run as the owner of the file, NOT as the user ID of the calling process. If a program is setuid and the owner is root, the program runs as root. Only root is able to affect the setuid bit on any file.

    host:~# chmod u+s program
    host:~# ls -l program
    -rwsr-xr-x    1 root     root        9999 Jan 01 00:00 program*
    
  4. CGI is Common Gateway Interface, a standard for passing arguments and other data from the web server to other programs. It's a protocol. It is used by the webserver to call programs run from disk, and to call interpreters built into the webserver. I use external CGI to mean the programs from disk.
  5. We use a program called suexec, which is a setuid wrapper program for external CGI. It allows a properly configured virtual hosted web site to execute CGI programs as a userid other than the one the webserver runs as. Suexec is setuid root. Anytime the webserver wants to run a CGI, it runs suexec with a parameter that indicates the path to the actual CGI program. Suexec performs a large number of tests on the CGI program to be run, making sure it is owned by the user, the directory it is in is owned by the user, it has the exact right permissions (not writable by anyone other than the owner), and many other things. Then suexec drops privileges and changes its userID to the userID of the program and runs it as that user.
  6. We require all external CGI on the server to use Suexec, which means all users that want to use CGI need a virtual host. This is because suexec is enabled by the User and Group apache config options which only work in a VirtualHost section, and cannot be specified based on Directory.

    <VirtualHost xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx:nnnn>
    ServerName username.domain.org
    DocumentRoot /users/username/html
    ScriptAlias /cgi-bin/ /var/www/htdocs/cgi-bin/username/
    User username
    Group username
    TransferLog /var/log/apache/username.domain.org/access.log
    ErrorLog /var/log/apache/username.domain.org/error.log
    UserDir disabled
    </VirtualHost>
    
  7. PHP is a programming language with easy support for embedding inside HTML pages. The webserver calls a PHP interpreter which is either loaded into the webserver as a module (mod_php.so) or as an external program (command: php4).
  8. We use the mod_php loaded into the webserver for speed reasons. Any .php file on the server is interpreted by default by mod_php.
  9. Since mod_php is loaded into the apache process which runs as www-data, mod_php also runs as www-data.
  10. You can access a PostgreSQL database using PHP. To connect to the database the traditional authentication used is username and password. However, the database connection protocols are primitive and only allow plain text passwords, or identd with no password.
  11. The plain text password is not vulnerable to packet sniffers since we don't allow remote database connections anyway.
  12. .php files in a user's html directory are owned by that user. Since the webserver runs as www-data, the .php files need to be world-readable so the webserver, with mod_php, can read them.
  13. If you want to use a database using .php, you are going to need to store the plain text username and password in your .php file. But this needs to be world readable. So any user or any program running on the machine can obtain your database password.
  14. This problem is a result of using mod_php, which has to run as www-data. So for users who want to use database with PHP, we set them up so that requests for .php files in their website, are handled using the external program, php4, run as an external CGI. This is wrapped by suexec causing the php4 interpreter to run as the user ID owning the website.
  15. In theory you could with this setup, put your plaintext database passwords into your .php files, make the .php files readable only by your username, and it would work. But there's a better way.
  16. Debian provides a modified version of the PostgreSQL database server with a new authentication type called peer sameuser. Using local connections between processes on the same machine, peer sameuser verifies that the UID of the database client matches the UID who owns the database being connected to. The connection is authenticated without the need for any password.

    From /etc/postgresql/pg_hba.conf:

    # AUTH type "peer" is a Debian improvement where it
    # looks at the actual userid on the other side of the socket,
    # which is more secure than using "ident".
    local   all                                     peer sameuser
    
  17. So to summarize, if someone wants PHP and database access, we set up a virtual host, as a subdomain of our domain, or another domain that the user owns. We give them a suexec-protected CGI directory. We give them their own copy of the PHP4 interpreter (required since suexec needs the CGI program, php4, to be owned by that user). We set .php in their html directory to run through that CGI'ed php4 interpreter. They specify the database connection to use Unix socket and not TCP/IP (by leaving out the hostname and port).

    Example php code:

    $database=pg_connect("dbname=trilstuff user=tril");
    

    Here's what to add to the VirtualHost section in the Apache config:

    # Location can be / for entire site to have php db access, or a subfolder.
    # You may want a subfolder only, so the faster mod_php will interpret
    # the rest of the site.
    <Location /path-with-db-access>
    Action username_php_cgi /cgi-bin/php4
    AddHandler username_php_cgi php
    </Location>
    

    I copy the php4 interpreter to everyone's CGI directory using GNU cfengine with the following cfengine.conf:

    control:
    
      actionsequence = ( shellcommands )
      script_path = ( "$(HOME)/bin" )
      split = ( " " )
      php4_cgi_users = ( "user1 user2 user3" )
    
    shellcommands:
    
      "$(script_path)/installcgi php4 $(php4_cgi_users)"
    

    And here's the source to the installcgi script:

    #!/bin/bash
    name=$1
    binary=/usr/lib/cgi-bin/${name}
    user=$2
    cgi_dir=/var/www/htdocs/cgi-bin
    echo installing ${name} in ${user}\'s CGI directory...
    install -o ${user} -g ${user} -m 755 ${binary} ${cgi_dir}/${user}/
    
  18. This setup works fine as long as they don't get too much traffic. If their site gets popular, it is too inefficient to launch a copy of the CGI PHP interpreter for every page request. This has happened with one of our sites, and to solve it I set up a separate copy of Apache which runs as the user's userID, instead of www-data. Then mod_php runs as the userID, and authenticates to the database with peer sameuser without having to launch a separate process to run suexec and then a new php interpreter. It uses some additional RAM for the extra apache instance, of course (our user is considering donating RAM to our server to make up for his use).
  19. Only problem is if people want to access the other copy of Apache using port 80, there's already the main Apache using port 80. So we used a new IP address for the second copy. If we had several popular sites with database access and PHP, more than the number of IPs we have, we'd need to ask some of them to accept a base URL of their site which includes an alternate port number.


Navigation: [ Back to Tril's Domain ] About this page: [ Copyright | Privacy Policy | Mail Tril ]