Securing Against Internal Network Threats with IPTABLES

Securing Against Internal Network Threats with IPTABLES

PacketFence is a great Network Access Control (NAC) system that will monitor internal network traffic and try to take action in the event of someone attempting something nefarious. It's more of a detective and corrective control that will take remediation against a machine on your network that violates a set of rules by hacking it off the network.

Given that most security professionals would agree that a large portion of successful hacks come from internal sources, it's a good practice to consider your own client network to be only marginally more secure (from your server's perspective) than the Internet facing ports are. I wanted something that would be more of a preventative control to stop the traffic from crossing the network to begin with. I began to realize that certain network traffic is expected from certain workstations, but other traffic wouldn't be, while a different class of machine might have the reverse true. This takes it from the perspective of "Block everything, allow what we want."

For instance

A typical administrative desktop might get Internet access and have access to the network shares for the customer contracts in addition to our workflow system. A "production" desktop might not need Internet access or customer contracts, but would need to talk to the servers that deal with our customer's sensitive data along with the workflow system. A third class might be a mobile Android device that only needs access to our internal workflow system.

While you could try to deal with this using VLAN's and subnetting, it can become very difficult to manage over time. What I've been able to do is a combination of PacketFence along with a custom bridge server running iptables that separates clients from all other network devices. Any client device, be it admin desktop, wifi or production must cross the bridge before gaining access to any network server or Internet.

How to monitor your devices with PacketFence

I started by configuring a server with PacketFence that has a sniffing adapter connected to a port on the primary client switch that mirrors all traffic through it. This ensures that PacketFence will see all new network devices in addition to being able to monitor for traffic that we shouldn't be seeing such as network scans or rogue DHCP servers.

I'm not going to go into the details of configuring PacketFence though I will say that it is probably a lot easier to get it working correctly in RedHat than it is in Ubuntu. Once you have a working PacketFence configuration, you can then begin to do some custom firewall work based on it's configurations.

Using PacketFence data for your bridge

Next I configured a transparent bridge machine between the client network switch and the server network switch. All traffic from clients must pass this bridge to get to the servers or the Internet. This gives me a location to run a separate iptables firewall specifically designed to control client network activity. For help on configuring a transparent bridge see my previous post here.

Create a custom firewall script

I wanted my helpdesk to be required to get involved for all new devices attached to the network. This means that my basic concept includes the following:

  • All devices can get DHCP packets from our DHCP server.
  • Devices not registered with PacketFence get no further network services.
  • Devices that have violated a PacketFence rule get completely blocked at the bridge (including DHCP).
  • Devices registered with PacketFence get a list of services as pre-determined for their class of device.
  • The firewall script should be able to run almost instantly and only make iptables changes to nodes that have changed in PacketFence.

To accomplish these goals I wrote a PHP script that runs every 5 minutes on the bridge. It connects to the PacketFence database and generates a firewall configuration script based on the available data. If that data has changed, it replaces and reloads the firewall script. This means the firewall isn't truly restarted every 5 minutes, only if there's been a change. The nodes are sent by MAC Address into specific pre-defined chains that don't need to be destroyed and re-created with each run. That keeps the number of iptables rules to a minimum despite handling hundreds of registered devices.

Here's some example code to show what I mean. NOTE: This code is NOT designed to run as-is but to give you ideas on how to do something similar with your setup.

First, setup some defines and basic stuff
<?php
/** PacketFence Firewall Bridge Script **/
define("IPTABLES","/sbin/iptables");

$packetfence = "192.168.1.20"; // the machine running packetfence
$dbuser = "pfreadonly";
$dbpass = "mypassword";
$dbname = "pf";

$ldbuser = "mybridgerw";
$ldbpass = "mypassword";
$ldbname = "clientbridge";

define("TCP","tcp");
define("UDP","udp");
define("BOTH","both");

define("LOCALNET","192.168.0.0/16");

define("SAMBASERVER","192.168.1.12");
define("WORKFLOW","192.168.1.13");
define("DHCPDNS","192.168.1.14");

// **** The database link for PacketFence itself ****
$packetlink = new mysqli($packetfence, $dbuser, $dbpass, $dbname);
if ($packetlink->connect_error) {
	exit();
}
// *** A local database that I can keep other information in ***
$bridgelink = new mysqli("localhost", $ldbuser, $ldbpass, $ldbname);
if ($bridgelink->connect_error) {
	exit();
}
Now see if the script itself changed, if so wipe the entire firewall and start over:
// has this script changed since last run?  If so flush all rules.  Stores MD5 of the script in my database.
// You could also store the md5 in a text file if you were so inclined
$md5 = md5_file(__FILE__);
$query = "SELECT category FROM nodes WHERE mac = 'MD5'";
$changed = false;
if ($myresult = $bridgelink->query($query)) {
	if ($res = mysqli_fetch_object($myresult)) {
		if ($res->category != $md5) {
			$changed = true;
			$query = "UPDATE nodes SET category='$md5' WHERE mac='MD5'";
			$myresult = $bridgelink->query($query);
		}
	} else {
		// doesn't exist in db
		$query = "INSERT INTO nodes SET category='$md5', mac='MD5'";
		$myresult = $bridgelink->query($query);
		$changed = true;
	}
} else {
	// db error
	exit();
}

if ($changed) {
	echo "Flushing...\n";
	clean_iptables();
	$query = "DELETE FROM nodes WHERE mac <> 'MD5'";
	if (!$myres = $bridgelink->query($query)) {
		// db error
		exit();
	}
	echo "Flushed.\n";
}
Read in all the nodes, check existing status and run iptables to insert them
$getallnodes = "SELECT node.mac, node.status, node.computername, node_category.name
		FROM node 
		LEFT JOIN node_category 
		ON node.category_id = node_category.category_id
		ORDER BY node.computername";
if ($myresult = $packetlink->query($getallnodes)) {
	while($myobj = mysqli_fetch_object($myresult)) {
		$query = "SELECT * FROM nodes WHERE mac='" . strtoupper($myobj->mac) . "' LIMIT 1";
		$todo = "none";
		if ($myresult2 = $bridgelink->query($query)) {
			if ($localobj = mysqli_fetch_object($myresult2)) {
				// found it locally, does it match?
				if ($myobj->status == 'unreg') {
					// it's now unregistered so remove it
					$todo = 'remove';
					//echo "Removing $myobj->mac \n";
				} else {
					// changed category?
					if ($myobj->name <> $localobj->category) $todo = 'update';
				}
			} else {
				// didn't find it locally...
				if ($myobj->status !== 'unreg') {
					$todo = 'add';
				}
			} // while localobj
		} // if myresult2
		// see if we're blacklisted (except the autoreg event)
		$query3 = "SELECT status FROM violation WHERE UPPER(mac)='" . strtoupper($myobj->mac) . "' AND vid <> 1200003";
		if ($myresult3 = $packetlink->query($query3)) {
			if ($res3 = mysqli_fetch_object($myresult3)) {
				// yep, it's blackballed so do NOT add it, infact remove it from all allows if exists
				$todo = 'remove';
			}
		}
			
		// look at the todo field to see what next
		if (($todo == 'remove') or ($todo == 'update')) {
			$query2 = "DELETE FROM nodes WHERE UPPER(mac)='" . strtoupper($myobj->mac) . "'";
			$myresult2 = $bridgelink->query($query2);
			// remove from iptables
			remove_mac($myobj->mac);
		}
		// Here is where we add our various classes that get sent to different chains
		if (($todo == 'add') or ($todo == 'update')) {
			if ($myobj->name == 'productiondesk') {
				// restrict to production desktop machine, no Internet, only a few services
				add_production_desk($myobj->mac);
				$query = "INSERT INTO nodes SET mac='" . strtoupper($myobj->mac) . "', category='$myobj->name'";
				$myresult2 = $bridgelink->query($query);
			}
			if ($myobj->name == 'desktop') {
				// Admin desktop, Internet, workflow but no production services
				add_desktop($myobj->mac);
				$query = "INSERT INTO nodes SET mac='" . strtoupper($myobj->mac) . "', category='desktop'";
				$myresult2 = $bridgelink->query($query);
			}
		}
	}
} else {
	//db error
	exit();
}
Functions for cleaning the iptables ruleset back to blank, etc.
/*** Clean the iptables ruleset back to basic defaults ****/
function clean_iptables() {
	exec("/sbin/modprobe nf_conntrack_ftp");
	exec("/sbin/modprobe nf_nat_ftp");
	exec("/sbin/iptables -F");
	exec("/sbin/iptables -P INPUT DROP");
	exec("/sbin/iptables -P OUTPUT ACCEPT");
	exec("/sbin/iptables -P FORWARD DROP");
	exec("/sbin/iptables -A INPUT -i lo -j ACCEPT");

	// Bootp / dhcp
	exec("/sbin/iptables -A FORWARD -p udp --dport 67:68 --sport 67:68 -j ACCEPT");
	// ntp
	exec("/sbin/iptables -A FORWARD -p udp --dport 123 -j ACCEPT");
	// ping, etc.  Probably should restrict ICMP allowed...
	exec("/sbin/iptables -A FORWARD -p icmp -j ACCEPT");
	create_chains();
}

/**
 * Return from this chain to do other checks
 * @param unknown_type $chain
 */
function add_return($chain) {
	exec(IPTABLES . " -A $chain -j RETURN");
}


/**
 * Add a protocol allow for a given chain
 * @param String $chain
 * @param String $type
 * @param String $port
 * @param String $dest
 */
function add_proto($chain, $type, $port, $dest = "") {
	if ($type == BOTH) {
		if ($dest != "") {
			exec(IPTABLES . " -A $chain -p tcp --dport $port -d $dest -j ACCEPT");
			exec(IPTABLES . " -A $chain -p udp --dport $port -d $dest -j ACCEPT");
		} else {			
			exec(IPTABLES . " -A $chain -p tcp --dport $port -j ACCEPT");
			exec(IPTABLES . " -A $chain -p udp --dport $port -j ACCEPT");
		}
	} else {
		if ($dest != "") {
			exec(IPTABLES . " -A $chain -p $type --dport $port -d $dest -j ACCEPT");
		} else {
			exec(IPTABLES . " -A $chain -p $type --dport $port -j ACCEPT");
		}
	} // if type == both
}

/**
 * Remove all references to a given MAC from iptables
 * @param String $mac
 * @return Array
 */
function remove_mac($mac) {
	$result = array();
	$mac = strtoupper($mac);
	// skip down to the FORWARD chain
	$done = false;
	while (!$done) {
		$shell = IPTABLES . " -n -L --line-numbers";
		$result = shell_exec($shell);
		$lines = explode("\n",$result);
		$found = false;
		foreach ($lines as $line) {
			if (strpos(strtoupper($line),"MAC $mac") > 0) {
				// found it
				$linenum = trim(substr($line,0,5));
				$exec = IPTABLES . " -D FORWARD " . $linenum;
				shell_exec($exec);
				$found = true;
				break;
			}
		}
		if (!$found) break;
	}
	return $result;
}

/**
 * Send this mac address through the given chain
 * @param string $chain
 * @param string $mac
 */
function send_to_chain($chain, $mac) {
	exec(IPTABLES . " -A FORWARD -m mac --mac-source $mac -m state --state NEW -j $chain");
}
Now create the chains we want for our various classes:
/**
 * Create the additional chains
 */
function create_chains() {
	echo "Creating chains...\n";
	create_chain_production_desk();
	create_chain_desktop();
}

/**
 * Production desk minimum allows
 */
function create_chain_production_desk() {
	$chain = "production_desk";

	exec(IPTABLES . " -F $chain");
	exec(IPTABLES . " -X $chain");
	exec(IPTABLES . " -N $chain");
	# LDAP
	add_proto($chain, BOTH, 389, SAMBASERVER); // first LDAP server
	add_proto($chain, BOTH, 389, DHCPDNS); // second LDAP server
	# Time server
	add_proto($chain, UDP,"123","192.168.1.254");
	# Workflow web interface
	add_proto($chain, TCP, "443", WORKFLOW);
	add_return($chain);
}

/**
 * Admin desk
 */
function create_chain_desktop() {
	$chain = "desktop";

	exec(IPTABLES . " -F $chain");
	exec(IPTABLES . " -X $chain");
	exec(IPTABLES . " -N $chain");
	# LDAP
	add_proto($chain, BOTH, 389, SAMBASERVER); // first LDAP server
	add_proto($chain, BOTH, 389, DHCPDNS); // second LDAP server
	# Time server
	add_proto($chain, UDP,"123","192.168.1.254");
	# File shares
	add_proto($chain, BOTH, 137, SAMBASERVER);
	add_proto($chain, BOTH, 138, SAMBASERVER);
	add_proto($chain, BOTH, 139, SAMBASERVER);
	add_proto($chain, BOTH, 445, SAMBASERVER);
	# Allow Internet
	exec(IPTABLES . " -A $chain -p tcp --dport 443 ! -d " . LOCALNET . " -j ACCEPT");
	exec(IPTABLES . " -A $chain -p tcp --dport 80 ! -d " . LOCALNET . " -j ACCEPT");
	add_return($chain);
}


/**
 * Add a production desktop PC, limit access to which servers we need.
 * @param $mac
 * @return unknown_type
 */
function add_production_desk($mac) {
	$mac = strtoupper($mac);
	send_to_chain("production_desk",$mac);
}

/**
 * Add a desktop desktop PC
 * @param $mac
 * @return unknown_type
 */
function add_desktop($mac) {
	$mac = strtoupper($mac);
	send_to_chain("desktop",$mac);
}

That should give you an idea of how you can create a class of device in PacketFence and then use that class definition to specifically allow or deny traffic at your bridge. You can even build class on top of class with each having more permissions that the one below it that it's based on by calling "send_to_chain()" multiple times for an add_whatever() command. For instance if my "desktop" class got everything that the "production_desk" class got, I could just call send_to_chain() for both classes within the "add_desktop()" function.

Yes, arp-spoofing would still get around this. The only way you could counter that would be with higher-end network switches that would integrate with your NAC system. But, when you couple this along with VLAN's, constant network monitoring, and RADIUS authentication you get a level of in-depth security you can't even come close to for only the cost of a built-it-yourself Linux server or two. That just saved you about $25,000.

Posted by Tony on Apr 06, 2015 | Network Security