Dipping my toes in the Xen pool
In my previous home setup I had two physical computers, one acting as my firewall and one acting as a DMZ system. Within the DMZ system there was no separation of functionalities and the need for slightly better hardware was starting to get apparent. Combine that with the need for consolidation to cut on electricity costs and we enter the wonderful world of virtualization.
This is what the configuration looked like:

I first took a look at vserver but that was lacking a proper virtualization of the network. I like the idea of running a system on the host's kernel as a set of jailed processes, but since network security was the point of focus, the possibilities did not suffice. I read up on UserMode Linux and Xen and settled for the latter because of better performance and better overall possibilities of separating the functionalities. The next image shows the final configuration (the VOIP part still needs to be done):

The purpose of this configuration is to create small and tightly locked DMZ systems that will host related network services and be disallowed to start any other network service. No internet initiated connection can get directly onto the firewall unless I temporarily allow that (see the later chapter on the Sesame web front-end). The DMZ systems are locked down with the lcap tool to prevent the possibility of adding modules to the system or changing critical files. The fact that the system disks are visible from the firewall (which is the Xen Dom0) enables periodical scans for malware that the DMZ hosts are completely unaware of.
Hardware
Not all that much actually. I have a dual P3/1GHz with 1.5 GiB memory, 2 160GB ATA disks in RAID1 all in a 4U rack-mountable that generates way too much noise. It does suffice nicely for the job, partly because of the efficiency of the Xen hypervisor on standard hardware.
Xen configuration
In this setup there was no need for multiple domU's to share the same virtual network, so I opted for routing i.o. bridging scripts in xend-config.sxp. I modified the vif script to assign a C-net to the various internal interfaces. This is the "diff" with the vif-route script:
-
antares:/etc/xen% diff -wuN scripts/vif-{route,custom}
-
--- scripts/vif-route 2007-11-09 14:58:32.000000000 +0100
-
+++ scripts/vif-custom 2007-11-09 14:58:29.000000000 +0100
-
@@ -23,11 +23,17 @@
-
dir=$(dirname "$0")
-
. "$dir/vif-common.sh"
-
-
-main_ip=$(dom0_ip)
-
+if [ "${ip}" ] ; then
-
+ main_ip="${ip%.*}.24"
-
+ netmask="255.255.255.0"
-
+else
-
+ main_ip=${main_ip:-$(dom0_ip)}
-
+ netmask="255.255.255.255"
-
+fi
-
-
case "$command" in
-
online)
-
- ifconfig ${vif} ${main_ip} netmask 255.255.255.255 up
-
+ ifconfig ${vif} ${main_ip} netmask ${netmask} up
-
echo 1>/proc/sys/net/ipv4/conf/${vif}/proxy_arp
-
ipcmd='add'
-
cmdprefix=''
-
@@ -39,13 +45,13 @@
-
;;
-
esac
-
-
-if [ "${ip}" ] ; then
-
- # If we've been given a list of IP addresses, then add routes from dom0 to
-
- # the guest using those addresses.
-
- for addr in ${ip} ; do
-
- ${cmdprefix} ip route ${ipcmd} ${addr} dev ${vif} src ${main_ip}
-
- done
-
-fi
The domU's each have two LVM volumes assigned as swap and as the main disk. Here's the configuration of elektra as an example:
-
#
-
# Kernel + memory size
-
#
-
kernel = '/boot/vmlinuz-2.6.18-4-xen-686'
-
ramdisk = '/boot/initrd.img-2.6.18-4-xen-686'
-
-
memory = '256'
-
-
#
-
# Disk device(s).
-
#
-
root = '/dev/sda1 ro'
-
-
disk = [ 'phy:vg-ant/elektra-disk,sda1,w', 'phy:vg-ant/elektra-swap,sda2,w' ]
-
-
#
-
# Hostname
-
#
-
name = 'elektra'
-
-
#
-
# Networking
-
#
-
vif = [ 'ip=xx.xx.xx.1, vifname=ueth0, mac=aa:26:26:f9:a4:f4' ]
-
-
#
-
# Behaviour
-
#
-
on_poweroff = 'destroy'
-
on_reboot = 'restart'
-
on_crash = 'restart'
Each domU is "debootstrapped" with a minimal version of debian etch and the packages specific for each domU are added. The firewall runs on dom0 (also debian etch). I actually planned to run the firewall in a domU but there is a hardcoded limit of 3 network devices per domU in xen 3.0.
General concept of the firewall rules
- The internal net is white-listed to allow a few protocols (like HTTP and FTP). A few trusted clients are completely white-listed.
- The wireless net is exclusively allowed to start an OpenVPN connection (this is actually the way I protect the wireless net, I don't do WEP or WPA). If an OpenVPN connection is established, that connection is treated like an internal net.
- Internet-initiated connections that are allowed are send directly to the DMZ. With the exception of temporarily allowed connections (see the part on Sesame), no direct connections from the internet or the DMZ to the firewall are allowed (with the exception of UDP connections for OpenVPN sessions).
- The DMZ hosts have varying rules (enforced via the FORWARD table):
- All hosts can send NTP packets to the internet and send syslog packets to the firewall
- The mailhost can receive IMAPS, and SMTP from anywhere and can send SMTP to the internet
- The webhost can receive HTTP, HTTPS and DNS from anywhere and send DNS to the internet, and send a "sesame" string to the firewall.
- The Proxy host can start any connection to the internet (for SOCKS purposes) and receive SOCKS connections from internal and VPN hosts and SQUID from internal, VPN and DMZ hosts.
- The database server can receive mysql and LDAP connections from internal, VPN and DMZ hosts
The firewall script can be found here.
Sesame web-frontend
There is a conflict of not allowing direct connections to the firewall and needing remote administration. I worked around this by creating a CGI page on the webserver that verifies a one-time password (OPIE) and if the verification succeeds, send the originating IP address to a specific port on the firewall.
There are a few requirements on the webserver for this to work. At the very least, the opie-server tools and the Authen::OPIE perl module are required. The latter is not packaged by default in debian, so this needs to be created with dh_perl (part of debhelper). The opie libraries (package libopie-dev) are required for making the package. My ready-built package is available here. The CGI script also makes use of the HTML::Template module (package libhtml-template-perl). I created a special user named opie and changes the ownership of /etc/opiekeys to that user. The CGI script runs at that user via the suexec mechanism of Apache. Except the OPIE challenge response, the user can also override the IP address that should be added in the table. This is done to prevent the address of a proxy server to be added to the rules i.o. your own. By default, the IP address of the connecting system is used. This is the content of the CGI script:
-
#!/usr/bin/perl -w
-
# vim:ai:filetype=perl:sta:sw=4:et:
-
#
-
# This CGI script will use the S/Key One-Time
-
# Password mechanism for verification and if
-
# sucessfull, will pass an IP address (default:
-
# $ENV{REMOTE_ADDR}) to a socket for inclusion
-
# in a firewall rulebase
-
#
-
use strict;
-
use CGI qw /:standard/;
-
use CGI::Carp 'fatalsToBrowser';
-
use HTML::Template;
-
use IO::Socket;
-
-
# var declarations
-
our ($cgi, $template);
-
our $opie_user="opie";
-
our $fwhost="192.168.1.1";
-
our $fwport="54321";
-
-
$cgi=CGI->new();
-
-
$cgi->param(-name=>"login") ne "Open Sesame") {
-
#
-
# print challenge screen
-
#
-
my $opiechalstr=&opie_challenge($opie_user);
-
&Barf2Browser("Unknown OPIE user: $opie_user")
-
if ($opiechalarr[0] ne "otp-md5") {
-
&Barf2Browser("Crazy challenge: $opiechalstr");
-
}
-
my $response=$cgi->textfield(-name=>"response",
-
-value=>"",
-
-size=>40);
-
my $ipaddr=$cgi->textfield(-name=>"ipaddr",
-
-value=>"",
-
-size=>16);
-
my $submitbutton=$cgi->submit(-name=>"login",
-
-value=>"Open Sesame");
-
$template=HTML::Template->new(filename =>"sesame.tmpl",
-
path => "/home/$opie_user/templates");
-
$template->param(chalbool => 1);
-
$template->param(formstart => $cgi->start_form);
-
$template->param(sequence => $opiechalarr[1]);
-
$template->param(seed => $opiechalarr[2]);
-
$template->param(response => $response);
-
$template->param(ipaddr => $ipaddr);
-
$template->param(curraddr => $ENV{REMOTE_ADDR});
-
$template->param(submit => $submitbutton);
-
}
-
else {
-
#
-
# Verify the challenge
-
#
-
my $response=$cgi->param(-name=>"response");
-
my $ipaddr=$cgi->param(-name=>"ipaddr");
-
&Barf2Browser("Empty response")
-
my $verifyval=&opie_verify($opie_user,$response);
-
&Barf2Browser("<span class=red>Athentication attempt FAILED</span>");
-
}
-
else {
-
#
-
# OTP challenge succeeded, send IP address to firewall
-
#
-
my $socket= new IO::Socket::INET (PeerAddr => $fwhost,
-
PeerPort => $fwport,
-
Proto => "tcp",
-
Type => SOCK_STREAM)
-
or &Barf2Browser("Authentication succeeded but "
-
."<span class=red>network connection failed</span>");
-
close $socket;
-
my $submitbutton=$cgi->submit(-name=>"ok",
-
-value=>"OK");
-
$template=HTML::Template->new(filename =>"sesame.tmpl",
-
path => "/home/$opie_user/templates");
-
$template->param(msgbool => 1);
-
$template->param(formstart => $cgi->start_form);
-
$template->param(msghdr => "<span class=blue>Authentication succeeded!</span>");
-
$template->param(message => "Sent $ipaddr to the firewall");
-
$template->param(submit => $submitbutton);
-
}
-
}
-
exit 0;
-
-
sub Barf2Browser() {
-
# output error
-
my ($string)=@_;
-
my $submitbutton=$cgi->submit(-name=>"ok",
-
-value=>"OK");
-
$template=HTML::Template->new(filename =>"sesame.tmpl",
-
path => "/home/$opie_user/templates");
-
$template->param(msgbool => 1);
-
$template->param(formstart => $cgi->start_form);
-
$template->param(msghdr => "Sesame error:");
-
$template->param(message => $string);
-
$template->param(submit => $submitbutton);
-
exit 0;
-
}
The template file is very straight-forward:
-
<!DOCTYPE html
-
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
-
<title>Sesame</title>
-
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
-
<style type="text/css">
-
-
BODY {background-color: #b0c4ef; color: black}
-
A:link {color: #000040}
-
A:external {color: #000040}
-
A:active {color: #000040}
-
A:visited {color: #000040}
-
SPAN.blue {color: #0000c0}
-
SPAN.red {color: #c00000}
-
</style>
-
</head>
-
-
<tmpl_var name="formstart">
-
<h1>Sesame Verifyer</h1>
-
<tmpl_if name="chalbool">
-
<tmpl_var name="sequence"> <tmpl_var name="seed"></h3>
-
<br/>
-
Current IP address:
-
</td><td>
-
Optionally override with:
-
</td><td>
-
<tmpl_var name="ipaddr">
-
</td></tr></table>
-
<tmpl_else>
-
<tmpl_if name="msgbool">
-
</tmpl_if>
-
</tmpl_if>
-
</form>
-
</body>
-
</html>
On the firewall end we have inetd listen to the specified port and send any incoming string to the script to add firewall rules. Because of the latter task this script needs to run as root. You can specify multiple tcp and/or udp ports in this script and for each of these, a firewall rule will be added to allow packets for that rule from the specified IP address, then the script will sleep for some time (default 5 minutes) and then all added rules are deleted again. Existing sessions stay active because of the stateful checks of iptables. The script:
-
#!/usr/bin/perl
-
# vim:ai:filetype=perl:sta:sw=4:et:
-
#
-
# This script will read a line from STDIN, expecting
-
# expecting an IP address and will use that address
-
# as a source for a firewall rule that temporarily
-
# opens one or more ports in the INPUT chain
-
#
-
# ports must be specified as /^[tu]\d+$/ (the first
-
# character specifies the tcp or udp protocol
-
-
use strict;
-
-
our @ports = ("t22","u5000");
-
our $timeslot = 300; #5 minutes
-
-
my $addr=<STDIN>;
-
chomp $addr;
-
# check if we received a correct IP address
-
if ($addr !~ /^(\d{1,3}\.){3}\d{1,3}$/) {
-
&LogText(LOG_WARNING, "WARNING: someone tried something nasty!");
-
exit 0;
-
}
-
foreach my $port (@ports) {
-
&IPTrule("-I",$port,$addr);
-
}
-
sleep $timeslot;
-
foreach my $port (@ports) {
-
&IPTrule("-D",$port,$addr);
-
}
-
exit 0;
-
-
sub LogText() {
-
my ($level, $text)=@_;
-
openlog("sesamed","ndelay,pid",LOG_DAEMON);
-
syslog($level,$text);
-
closelog;
-
}
-
-
sub IPTrule() {
-
my ($act,$protoport,$addr)=@_;
-
my ($proto,$port)=();
-
if ($protoport =~ /^([tu])(\d+)$/) {
-
$port=$2;
-
$proto = ($1 eq "t") ? "tcp" : "udp";
-
}
-
&LogText(LOG_NOTICE, "BUG: wrong protoport specified");
-
}
-
my @cmdline=("/sbin/iptables",$act,"INPUT","-p",$proto,"--dport");
-
}
Security issues
The S/Key system (which OPIE implements) is not terribly secure. It uses MD5 hashes on top of each other and then splits the hash in two 64-bit parts and "xor"s the two parts together. Cracking the system means doing a preimage attack on 64 bits with the MD5 algorithm. If you are limited to brute-force, a modern server (2 quad-core CPUs with 5kMIPS per core) runs for 2500 years. This may sound as much but really isn't. Last I heard there was a less-than brute force system available for finding MD5 collisions (I'm not talking about a birthday attack here, just the algorithm) and using multiple parallel systems which could bring down the time quite a bit.
Personally, I don't think we need to resort to any paranoia in this particular case. Even if a hash collision is found, the only thing a black hat can gain is opening up ports where another level of authentication waits (or should wait). The security can also be enhanced by having the HTTP traffic encrypted by SSL to prevent a black hat of sniffing the net. I wouldn't use S/Key for logging into a system on its own, but combined with a second authentication vector (e.g. using a challenge response sent via SMS to a cell phone) would create a nice 2-vector authentication mechanism (handy for situations like Internet Cafe's where you run the risk of being subjected to a key-logger).

Leave a Reply