Skip to content

Instantly share code, notes, and snippets.

@typhonius
Last active May 9, 2017 18:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save typhonius/7524395 to your computer and use it in GitHub Desktop.
Save typhonius/7524395 to your computer and use it in GitHub Desktop.
An include file for the Acquia Cloud to be placed at the docroot/sites/acquia.inc location. May be optionally called from the site's settings.php with further details found on the Acquia Knowledgebase article.
<?php
/**
* @file
* Utilities for use in protecting an environment via basic auth or IP whitelist.
*/
function ac_protect_this_site() {
global $conf;
// Test if we are using drush (command-line interface)
$cli = drupal_is_cli();
// Is the user on the VPN? Default to FALSE.
$on_vpn = $cli ? TRUE : FALSE;
if (!empty($_SERVER['AH_Client_IP']) && !empty($conf['ah_whitelist'])) {
$on_vpn = ah_ip_in_list($_SERVER['AH_Client_IP'], $conf['ah_whitelist']);
}
// If the IP is not explicitly whitelisted check to see if the IP is blacklisted.
if (!$on_vpn && !empty($_SERVER['AH_Client_IP']) && !empty($conf['ah_blacklist'])) {
if (ah_ip_in_list($_SERVER['AH_Client_IP'], $conf['ah_blacklist'])) {
ah_page_403();
}
}
// Should we skip the auth check? Default to FALSE.
$skip_auth_check = FALSE;
// Check if we should skip auth check for this page.
if (ah_path_skip_auth()) {
$skip_auth_check = TRUE;
}
// Check if we should disable cache for this page.
if (ah_path_no_cache()) {
$conf['page_cache_maximum_age'] = 0;
}
// Is the page restricted to whitelist only? Default to FALSE.
$restricted_page = FALSE;
// Check to see whether this page is restricted.
if (!empty($conf['ah_restricted_paths']) && ah_paths_restrict()) {
$restricted_page = TRUE;
}
$protect_ip = !empty($conf['ah_whitelist']);
$protect_password = !empty($conf['ah_basic_auth_credentials']);
// Do not protect command line requests, e.g. Drush.
if ($cli) {
$protect_ip = FALSE;
$protect_password = FALSE;
}
// Un-comment to disable protection, e.g. for load tests.
// $skip_auth_check = TRUE;
// $on_vpn = TRUE;
// If not on whitelisted IP prevent access to protected pages.
if ($protect_ip && !$on_vpn && $restricted_page) {
ah_page_403();
}
// If not skipping auth, check basic auth.
if ($protect_password && !$skip_auth_check) {
ah_check_basic_auth();
}
}
/**
* Output a 403 (forbidden access) response.
*/
function ah_page_403() {
header('HTTP/1.0 403 Forbidden');
print "403 Forbidden: Access denied (". $_SERVER['AH_Client_IP'] .")";
exit;
}
/**
* Output a 401 (unauthorized) response.
*/
function ah_page_401() {
header('WWW-Authenticate: Basic realm="This site is protected"');
header('HTTP/1.0 401 Unauthorized');
print "401 Unauthorized: Access denied (". $_SERVER['AH_Client_IP'] .")";
exit;
}
/**
* Check basic auth against allowed values.
*/
function ah_check_basic_auth() {
global $conf;
$authorized = FALSE;
$php_auth_user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : NULL;
$php_auth_pw = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : NULL;
$credentials = isset($conf['ah_basic_auth_credentials']) ? $conf['ah_basic_auth_credentials'] : NULL;
if ($php_auth_user && $php_auth_pw && !empty($credentials)) {
if (isset($credentials[$php_auth_user]) && $credentials[$php_auth_user] == $php_auth_pw) {
$authorized = TRUE;
}
}
if ($authorized) {
return;
}
// Always fall back to 401.
ah_page_401();
}
/**
* Determine if the current path is in the list of paths to not cache.
*/
function ah_path_no_cache() {
global $conf;
$q = isset($_GET['q']) ? $_GET['q'] : NULL;
$paths = isset($conf['ah_paths_no_cache']) ? $conf['ah_paths_no_cache'] : NULL;
if (!empty($q) && !empty($paths)) {
foreach ($paths as $path) {
if ($q == $path || strpos($q, $path) === 0) {
return TRUE;
}
}
}
}
/**
* Determine if the current path is in the list of paths on which to not check
* auth.
*/
function ah_path_skip_auth() {
global $conf;
$q = isset($_GET['q']) ? $_GET['q'] : NULL;
$paths = isset($conf['ah_paths_skip_auth']) ? $conf['ah_paths_skip_auth'] : NULL;
if (!empty($q) && !empty($paths)) {
foreach ($paths as $path) {
if ($q == $path || strpos($q, $path) === 0) {
return TRUE;
}
}
}
}
/**
* Check whether a path has been restricted.
*
*/
function ah_paths_restrict() {
global $conf;
if (isset($_GET['q'])) {
// Borrow some code from drupal_match_path()
foreach ($conf['ah_restricted_paths'] as &$path) {
$path = preg_quote($path, '/');
}
$paths = preg_replace('/\\\\\*/', '.*', $conf['ah_restricted_paths']);
$paths = '/^(' . join('|', $paths) . ')$/';
// If this is a restricted path, return TRUE.
if (preg_match($paths, $_GET['q'])) {
return TRUE;
}
}
return FALSE;
}
/**
* Determine if the IP is within the ranges defined in the white/black list.
*/
function ah_ip_in_list($ip, $list) {
foreach ($list as $item) {
// Match IPs in CIDR format.
if (strpos($item, '/') !== false) {
list($range, $mask) = explode('/', $item);
// Take the binary form of the IP and range.
$ip_dec = ip2long($ip);
$range_dec = ip2long($range);
// Create the binary form of netmask.
$mask_dec = ~ (pow(2, (32 - $mask)) - 1);
// Run a bitwise AND to determine whether the IP and range exist
// within the same netmask.
if (($mask_dec & $ip_dec) == ($mask_dec & $range_dec)) {
return TRUE;
}
}
// Match against wildcard IPs or IP ranges.
elseif (strpos($item, '*') !== false || strpos($item, '-') !== false) {
// Construct a range from wildcard IPs
if (strpos($item, '*') !== false) {
$item = str_replace('*', 0, $item) . '-' . str_replace('*', 255, $item);
}
// Match against ranges by converting to long IPs.
list($start, $end) = explode('-', $item);
if (ip2long($start) <= ip2long($ip) && ip2long($ip) <= ip2long($end)) {
return TRUE;
}
}
// Match against single IPs
elseif ($ip === $item) {
return TRUE;
}
}
return FALSE;
}
@typhonius
Copy link
Author

Sample code for settings.php includes and $conf settings that help you quickly
lock down an Acquia Cloud environment using basic auth and / or IP whitelisting.

  • All site lockdown logic in located in acquia.inc
  • All settings are in $conf variables.
    • $conf['ah_basic_auth_credentials'] An array of basic auth username /
      password combinations
    • $conf['ah_whitelist'] An array of IP addresses to allow on to the site.
    • $conf['ah_blacklist'] An array of IP addresses that will be denied access to the site.
    • $conf['ah_paths_no_cache'] Paths we should explicitly never cache.
    • $conf['ah_paths_skip_auth'] Skip basic authentication for these paths.
    • $conf['ah_restricted_paths'] Paths which may not be accessed unless the user is on the IP whitelist.
  • The site lockdown process happens by calling ac_protect_this_site(); with defined $conf elements.
  • Whitelist / blacklist IPs may use any of the following syntax:
    • CIDR (107.0.255.128/27)
    • Range (121.91.2.5-121-121.91.3.4)
    • Wildcard (36.222.120.*)
    • Single (9.80.226.4)

Business Logic

  • With no $conf values set, ac_protect_this_site(); will do nothing.
  • If the path is marked as restricted, all users not on the whitelist will receive access denied.
  • If a user's IP is on the blacklist and not on the whitelist they will receive access denied.
  • Filling $conf['ah_basic_auth_credentials'] will result in all requests being requring an .htaccess log in.
  • Securing the site requires entries in both $conf['ah_whitelist'] and $conf['ah_restricted_paths']

Examples

Block access to non-whitelisted users on all pages of non-production environments.

$conf['ah_restricted_paths'] = array(
  '*',
);

$conf['ah_whitelist'] = array(
  '36.222.120.*',
  '107.0.255.128/27',
);

if (file_exists('/var/www/site-php')) {
  require('/var/www/site-php/{site}/{site}-settings.inc');

  if(!defined('DRUPAL_ROOT')) {
    define('DRUPAL_ROOT', getcwd());
  }

  if (file_exists(DRUPAL_ROOT . '/sites/acquia.inc')) {
    if (isset($_ENV['AH_NON_PRODUCTION']) && $_ENV['AH_NON_PRODUCTION']) {
      require DRUPAL_ROOT . '/sites/acquia.inc';
      ac_protect_this_site();
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment