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