Best Way to secure WordPress Installation

UPDATE: See bottom of post for a Apache 2.4 upgrade.

As I’ve said before, the key to good security is to be unique and not special. Here’s one particular way to be unique with your wordpress installation.

The management UI
The management UI, thanks to the beauty of php.

Try it out!

If you try to log in to my login page (visit wp-login.php), you’ll be met with a very unhappy looking “Access Denied” message. Basically, my web server is configured to only allow certain IP addresses to connect (and no I won’t tell you what those are…).

Why did I have to do this?

Well, within a few hours of running a wordpress installation on my server (a Amazon EC2 Micro Instance), I was brutally attacked by a ferocious clan of spambots. They very quickly brought the server to its knees with a bagillion requests on the php login page.

Were they able to gain access? Absolutely not. But that didn’t stop them from DDoSing my tiny server. I immediately began searching for the best way to prevent this from happening again. The effort required by apache (the web server) to give “Access Denied” is much less than the effort required to spawn a PHP page, even if that PHP page automatically blocks login requests.

Originally I used plain apache configs, but getting that to update dynamically is basically impossible. So I found a dynamic solution!

How do I do this?

First, make sure your apache configuration has at least “AllowOverride Limit” for the web root and wp-admin/ and create two .htaccess files owned by the webroot user, one in “/.htaccess” and another in “/wp-admin/.htaccess” (of course relative to your path root).

Then install this script in your webroot as ip.php or some such:

<?php
###################################
# This script is designed to maintain two .htaccess files.
# These files are located in your webroot and in wp-admin/
#
# You must put "AllowOverride Limit" on both your webroot and
# your wp-admin/ directory.
# #################################
function parse($file = '.htaccess') {
  $txt_file = file_get_contents($file);
  $lines = explode("\n",$txt_file);
  $ips = array();
  for( $i = 0; $i < count($lines); $i++) {
    if (preg_match("/^#/",$lines[$i])) {
      $comment = substr($lines[$i],1);
      $ip = explode(" ",$lines[$i+1]);
      $ips[$comment] = $ip[2];
    }
  }
  return $ips;
}
function write($text, $file = '.htaccess') {
  file_put_contents($file,$text);
  return;
}
function format($ips) {
  $text = "order deny,allow\ndeny from all\n";
  foreach($ips as $k => $v)
    $text .= "#$k\nallow from $v\n";
  return $text;
}
if (@$_GET['pass'] == "notforyou") {
  if (isset($_POST['delete'])) {
    $ips = parse();
    unset($ips[$_POST['todelete']]);
    $text = format($ips);
    write("<Files wp-login.php>\n${text}</Files>\n");
    write($text,"wp-admin/.htaccess");
  }
  if (isset($_POST['add'])) {
    $ips = parse();
    $ips[$_POST['comment']] = $_POST['ip'];
    $text = format($ips);
    write("<Files wp-login.php>\n${text}</Files>\n");
    write($text,"wp-admin/.htaccess");
  }
  echo "<p><form method='post' action=''>";
  echo "<select name='todelete'>";
  foreach (parse() as $k => $v) {
    echo "<option value='$k'>$k ($v)</option>";
  }
  echo "</select><input type='submit' value='Delete' name='delete'></p>";
?>
<p><input name="comment" placeholder="Comment"></p>
<p><input name="ip" value="<?php echo $_SERVER['REMOTE_ADDR'];?>"></p>
<p><input type="submit" value="Add" name="add">
</form></p>
<?php
} else {
  header('HTTP/1.1 404 Not Found');
  echo "<h1>Error 404: Page Not Found</h1><p>Please kindly go away. Yes you.</p>";
}

Afterwards, visit http://axfp.org/ip.php?pass=notforyou and if the password matches you’ll be greeted with the management page.

If you get it wrong, the server tells you the page is not there with a friendly 404 error. Heh. Classic.

Pro Tip: You can specify an IP address range such as 192.168.0.0/16 or 192.168.10.0/24 or any other subnet skulldiggery.

Apache 2.4

Apache 2.4 introduces a new method of access control using the Require directive. For more information see the apache docs. You’ll need to adjust your virtualhost config and update both the parse and format functions:

<?php
###################################
# This script is designed to maintain two .htaccess files.
# These files are located in your webroot and in wp-admin/
#
# You must put "AllowOverride AuthConfig" on both your webroot and
# your wp-admin/ directory.
# #################################
function parse($file = '.htaccess') {
  $txt_file = file_get_contents($file);
  $lines = explode("\n",$txt_file);
  $ips = array();
  for ($i = 0; $i < count($lines); $i++) {
    if (preg_match("/require/",$lines[$i])) {
      $ip = explode(" ",$lines[$i]);
    }
  }
  $j = 2;
  for( $i = 0; $i < count($lines); $i++) {
    if (preg_match("/^#/",$lines[$i])) {
      $comment = substr($lines[$i],1);
      $ips[$comment] = $ip[$j++];
    }
  }

  return $ips;
}
function write($text, $file = '.htaccess') {
  file_put_contents($file,$text);
  return;
}

function format($ips) {
  $text = "";
  $ip = "require ip";
  foreach($ips as $k => $v){
    $text .= "#$k\n";
    $ip .= " $v";
  }
  return $text . $ip . "\n";
}

Leave a Reply