Cover V13, i06

Article

jun2004.tar

Apache User Maintenance with PHP

Russell J.T. Dyer

For a sys admin managing a Web site, especially an intranet site, the maintenance of Apache users can be bothersome. Sometimes it may be preferable to allow department managers to maintain their own users: adding and deleting usernames, changing passwords on the Linux server, etc. Unfortunately, it is not feasible to give non-technical managers server access to Apache's htpasswd program or to the user file for managing usernames. However, by configuring Apache and writing a simple set of PHP scripts, you can set up a fairly secure Web interface through which non-technical administrators can maintain Apache users.

As part of this series on PHP, I'll go through the process of creating such an interface not only to explain how to program with PHP, but also to give systems administrators ideas on how to professionally and securely delegate tasks to co-workers with various skill levels and responsibilities.

Security Concerns

It's generally not good practice to allow any user password file to be accessible through Apache, through a Web connection, despite how insignificant the files being restricted may be. However, there are a few methods of tightening the security for an Apache user maintenance program that may be sufficient. One good precaution to take for the PHP scripts that I present is to make them accessible only by a certain user and from a specific internal workstation. This can be done in the main Apache configuration file (i.e., httpd.conf) or a .htaccess file located within the same directory as the PHP scripts. I'll use the latter choice for purposes of this article, but the directives will be identical for either configuration file.

The following is an example of how such a .htaccess file might look for a fictitious marketing department manager named Bob Smith who is the only person with access to the directory in which it's placed:

AllowOverride None
Options None
Order deny,allow
Deny from all
AuthType Basic
AuthName "Marketing Department"
AuthUserFile /var/www/users/dept_admin
Require user bob_smith
Allow from 10.1.1.50
Satisfy all
The last four directives here relate to the primary security concerns for the PHP scripts that follow. The AuthUserFile directive provides the absolute file system path and the file name of the user file for the department administrators. It does not provide the user file for the marketing department that our script will be editing, though. This directory should be outside of the site's document root (i.e., the directory named in the argument of the DocumentRoot in httpd.conf) so that outsiders cannot download the file and crack it.

The next directive requires that the user "bob_smith" provide the proper password, which is found in the file named in the previous directive. Although the user file may contain several administrative users, only the user Bob Smith will be able to access the protected directory. Incidentally, Bob should choose a complex password and not something simple like his dog's name, considering the authority his username carries. The next directive instructs Apache to access the user directory only from a workstation with the internal IP address of 10.1.1.50, which is located on Bob's desk. A 10.x.x.x TCP/IP address cannot go through the Internet. This provides added protection from external intrusions. Finally, the last directive states that all of the directives in this .htaccess file must be satisfied. Here, this refers primarily to the Require and the Allow directives.

Despite these precautions, an employee could change his own network card's IP address to 10.1.1.50 while Bob Smith was on vacation and then sit at his own desk and try to guess Bob's password or run a cracking program on it. So, these are not absolute security precautions for the PHP scripts we're building. However, the security may be sufficient if the files being protected are not extremely sensitive.

I did not discuss the other Apache directives shown previously and other options available because this is primarily a PHP article. However, I wrote an article called Apache Authentication, published by UnixReview.com (May 2004), which you can read online for more details on this topic.

Apache User List

The first PHP script (apache-users-mkt.php) will need to open the Apache user file for which Bob Smith is responsible. Once the file is open, the script should extract just a list of usernames without their passwords. The list of usernames will be displayed in a Web page from which the administrator can choose the one that he or she wants to change. We'll also provide a form for adding a new username. We'll start the script with the following lines before laying out the Web page portion:

<?php

$user_dir = '/var/www/users/';
$user_file = 'marketing_users';

$FILE = fopen("$user_dir$user_file", 'r')
        or die("Could not open $user_file");
?>
This opening excerpt first sets up the initial variables -- the variable for the directory containing the Apache user file, and the variable for the name of the user file. Then it establishes the file handle with the initial variable, or the script dies trying. Next, we can begin the Web page with its headings:
<html>
<body>
<h3>Apache Users</h3>

<form method='post' action='apache-user-mkt.php'>
These are simple HTML tags that will suffice for now. They can always be improved later. Instead, let's focus on the PHP code and extract the contents of the Apache user file:
<?php 

   $line = rtrim(fgets($FILE, 4096));

while(!feof($FILE))
{
   list($user) = split('\:', $line, 2);
   print "<a href='php-apache-user.php?user_chg=$user'>
          $user</a><br/>\n";
   $line = rtrim(fgets($FILE, 4096));
}
fclose($FILE);

?>
Here, I used a while statement to loop through the user file, one line at a time. The test condition is "not end of file", invoked by the feof() function with a negator (!) in front of it. Before the while statement, the first line employs an fgets() function to grab the first line (4096 bytes or 4k) of data from the user file indicated by the file handle (i.e., $FILE). This is wrapped in an rtrim() function that trims off the right-most character of the line, which is the line-feed. The result is temporarily saved in the variable $line. This line appears again at the end of the while statement block.

On the next line, a split() function is used to separate the username from the password found in $line, which contains only these two elements. The list() function helps to sort out the list of elements found. Again, we're actually only temporarily saving the first element, the username in the variable $user. The last line of the statement block above prints the username within an HTML hyperlink tag, which will take the user to the next script called apache-user-mkt.php. Finally, we close the file handle with an fclose() before finishing off the HTML:

New User: <input type='text' name='new_user'/>

<input type='submit' value='Add User'/>
</form>

</body>
</html>
To allow the addition of a new username, the previous section also creates a form input box for the administrator to enter the new username. This is all basic HTML, so I'll move on to the next PHP script.

Changing or Adding Users

This next PHP script presents a form for the administrator to enter the user's new password. It starts like the previous one with the same initial variables, except I'll add one more for discussion purposes. The HTML tags are the same except for the form tag at the beginning:

<?php

$user_dir = '/var/www/users/';
$user_file = 'marketing_users';
$user_chg = $_GET[user_chg];
$new_user = $_POST[new_user];

?>

<html>
<body>
<h3>Apache Users</h3>

<form method='post' action='apache-user-chg-mkt.php'>

<?
print "<input type='hidden' name='user_chg' value='$user_chg'/>
       <input type='hidden' name='new_user'
value='$new_user'/>";
This script sets up a variable to capture the username to be changed. It extracts the value from the .GET and the .POST hashes that were formed when the script was called, using the keys user_chg and new_user. It uses the .GET hash for the user_chg because that key was given at the end of the hyperlink that brought the administrator here. It uses the .POST hash because the key new_user was sent from the form input box with a type of post.

These variables will already be set up without these lines if the magic_quotes_gpc option is enabled in php.ini. If it is, you can just use the variable names of $user_chg and $new_user based on the name from the calling script with the hashes. Incidentally, this script could easily be consolidated into the first one. I split the choosing or entering of the username from the entering of the password so that I could illustrate these hashes.

The HTML form tag above directs the user to the next script, apache-user-chg-mkt.php, which will implement the password change from this script. The second and third tags are slightly hidden ones that will pass the existing or new username to the next script. We don't need to hide it; we just don't want the administrator to have to choose the username again.

With the Web page started, we should check whether we're dealing with a new username. This is done with an if statement, like so:

if($new_user) { $user_chg = $new_user; }
   print "Password for $user_chg:
          <input type='password' name='pwd_chg'/>";
?>

<input type='submit' value='Submit'/>
</form>

</body>
</html>
The test condition of the if statement tests whether there is a value in $new_user. If this is a new username, it will change the value of $user_chg to it. If not, $user_chg will contain the existing username selected. After that, it will print out a label and an input box for the administrator to enter the user's password. The input box is a password type. This displays asterisks when the administrator types in the password, but does not encrypt the password. It will send the password to the next script in plain text, which is another reason for restricting use of this script to internal hosts. Finally, we close out the form and the rest of the HTML code, ending this script.

Saving Changes

Because the Apache user file is a simple text file and not an interactive database like MySQL, we must retrieve all of the usernames and their respective passwords and then write the data back to the same file, one line at a time, making adjustments for new passwords or usernames. This third script begins with the same initial variables and opening HTML tags as the previous ones. To save space, I've left out those lines. So, here's the excerpt that retrieves the data again from the user file:

<?php 

$FILE = fopen("$user_dir$user_file", 'r')
        or die("Could not open $list");
$line = rtrim(fgets($FILE, 4096));

while(!feof($FILE))
{
   list($user,$pw) = split('\:', $line, 2);
   $users[$user] = $pw;

   $line = rtrim(fgets($FILE, 4096));
}
fclose($FILE);
A couple of these lines are different from the first script. The second line inside the while is slightly different. Note that we're capturing the password this time because we'll need to write that back to the user file later. Although the password will be encrypted, it's still coming out of a simple text file. So, for passwords that aren't being changed, we can rewrite the same text back without losing its integrity. Instead of printing out the usernames as in the first script, we store the pairs of information in a hash or an associative array called $users[]. The key is the username and value is the password. This will allow us to close the user file before we write to it in this next excerpt:
$PWF = fopen("$user_dir$user_file", 'w')
       or die("Could not open $list");

if($new_user) {
   $pwd_chg = crypt("$pwd_chg"); 
   $users[$new_user] = $pwd_chg;
}
foreach ($users as $username => $password) {
   if($username == $user_chg) {
      $password = crypt("$pwd_chg");
   fwrite($PWF, "$username:$password\n");
      print "Password changed for $username</i>.";
   }
   else {
      fwrite($PWF, "$username:$password\n");
   }
}
   fclose($PWF);

?>
At the start of this section of code, we create a new file handle, but in write mode (i.e., w). Next, we utilize an if statement to test whether we have a new username to process. If so, then the new username and password is added to the associative array. We then deploy a foreach statement to loop through the associative array $users set up earlier. With the foreach, we can loop through each element of the associative array and write each key and its associated value. With the foreach statement block, there's an if statement to test whether the username is the same one we're changing. If so, that username is written to the file with the new password. Otherwise, the username is written to the file with the old password per the else statement. We end the script by closing the file and including the usual closing HTML tags (not shown here).

Conclusion

This article presented some fairly basic PHP scripts, but they illustrate how to create a PHP script that will access a local file, how to process the data it contains, and how to change that data. There's much more that can be done, such as allowing administrators to delete usernames or change multiple Apache user files for each user. I hope these examples will help you think of how you might create PHP scripts for similar server and network maintenance. Just be mindful of security when you do this.

Russell Dyer is a Perl programmer, a MySQL developer, and a Web designer living and working on a consulting basis in New Orleans. He is also an adjunct instructor at a technical college where he teaches Linux and other open source software. He can be reached at: russell@dyerhouse.com.