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:

CODE:
  1. antares:/etc/xen% diff -wuN  scripts/vif-{route,custom}
  2. --- scripts/vif-route   2007-11-09 14:58:32.000000000 +0100
  3. +++ scripts/vif-custom  2007-11-09 14:58:29.000000000 +0100
  4. @@ -23,11 +23,17 @@
  5.  dir=$(dirname "$0")
  6.  . "$dir/vif-common.sh"
  7.  
  8. -main_ip=$(dom0_ip)
  9. +if [ "${ip}" ] ; then
  10. +    main_ip="${ip%.*}.24"
  11. +    netmask="255.255.255.0"
  12. +else
  13. +    main_ip=${main_ip:-$(dom0_ip)}
  14. +    netmask="255.255.255.255"
  15. +fi
  16.  
  17.  case "$command" in
  18.      online)
  19. -        ifconfig ${vif} ${main_ip} netmask 255.255.255.255 up
  20. +        ifconfig ${vif} ${main_ip} netmask ${netmask} up
  21.          echo 1>/proc/sys/net/ipv4/conf/${vif}/proxy_arp
  22.          ipcmd='add'
  23.          cmdprefix=''
  24. @@ -39,13 +45,13 @@
  25.          ;;
  26.  esac
  27.  
  28. -if [ "${ip}" ] ; then
  29. -    # If we've been given a list of IP addresses, then add routes from dom0 to
  30. -    # the guest using those addresses.
  31. -    for addr in ${ip} ; do
  32. -      ${cmdprefix} ip route ${ipcmd} ${addr} dev ${vif} src ${main_ip}
  33. -    done
  34. -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:

CODE:
  1. #
  2. #  Kernel + memory size
  3. #
  4. kernel  = '/boot/vmlinuz-2.6.18-4-xen-686'
  5. ramdisk = '/boot/initrd.img-2.6.18-4-xen-686'
  6.  
  7. memory  = '256'
  8.  
  9. #
  10. #  Disk device(s).
  11. #
  12. root    = '/dev/sda1 ro'
  13.  
  14. disk    = [ 'phy:vg-ant/elektra-disk,sda1,w', 'phy:vg-ant/elektra-swap,sda2,w' ]
  15.  
  16. #
  17. #  Hostname
  18. #
  19. name    = 'elektra'
  20.  
  21. #
  22. #  Networking
  23. #
  24. vif  = [ 'ip=xx.xx.xx.1, vifname=ueth0, mac=aa:26:26:f9:a4:f4' ]
  25.  
  26. #
  27. #  Behaviour
  28. #
  29. on_poweroff = 'destroy'
  30. on_reboot   = 'restart'
  31. 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 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:

PERL:
  1. #!/usr/bin/perl -w
  2. # vim:ai:filetype=perl:sta:sw=4:et:
  3. #
  4. # This CGI script will use the S/Key One-Time
  5. # Password mechanism for verification and if
  6. # sucessfull, will pass an IP address (default:
  7. # $ENV{REMOTE_ADDR}) to a socket for inclusion
  8. # in a firewall rulebase
  9. #
  10. use strict;
  11. use CGI qw /:standard/;
  12. use CGI::Carp 'fatalsToBrowser';
  13. use HTML::Template;
  14. use IO::Socket;
  15. use Authen::OPIE qw(opie_challenge opie_verify);
  16.  
  17. # var declarations
  18. our ($cgi, $template);
  19. our $opie_user="opie";
  20. our $fwhost="192.168.1.1";
  21. our $fwport="54321";
  22.  
  23. $cgi=CGI->new();
  24. print $cgi->header();
  25.  
  26. if (not defined $cgi->param(-name=>"login") or
  27.     $cgi->param(-name=>"login") ne "Open Sesame") {
  28.     #
  29.     # print challenge screen
  30.     #
  31.     my $opiechalstr=&opie_challenge($opie_user);
  32.     &Barf2Browser("Unknown OPIE user: $opie_user")
  33.         if (not defined $opiechalstr);
  34.     my @opiechalarr=split(/ /, $opiechalstr);
  35.     if ($opiechalarr[0] ne "otp-md5") {
  36.         &Barf2Browser("Crazy challenge: $opiechalstr");
  37.     }
  38.     my $response=$cgi->textfield(-name=>"response",
  39.         -value=>"",
  40.         -size=>40);
  41.     my $ipaddr=$cgi->textfield(-name=>"ipaddr",
  42.         -value=>"",
  43.         -size=>16);
  44.     my $submitbutton=$cgi->submit(-name=>"login",
  45.         -value=>"Open Sesame");
  46.     $template=HTML::Template->new(filename =>"sesame.tmpl",
  47.         path => "/home/$opie_user/templates");
  48.     $template->param(chalbool => 1);
  49.     $template->param(formstart => $cgi->start_form);
  50.     $template->param(sequence => $opiechalarr[1]);
  51.     $template->param(seed => $opiechalarr[2]);
  52.     $template->param(response => $response);
  53.     $template->param(ipaddr => $ipaddr);
  54.     $template->param(curraddr => $ENV{REMOTE_ADDR});
  55.     $template->param(submit => $submitbutton);
  56.     print $template->output;
  57. }
  58. else {
  59.     #
  60.     # Verify the challenge
  61.     #
  62.     my $response=$cgi->param(-name=>"response");
  63.     my $ipaddr=$cgi->param(-name=>"ipaddr");
  64.     &Barf2Browser("Empty response")
  65.         if (not defined $response or $response eq "");
  66.     my $verifyval=&opie_verify($opie_user,$response);
  67.     if (not defined $verifyval or $verifyval != 0) {
  68.         &Barf2Browser("<span class=red>Athentication attempt FAILED</span>");
  69.     }
  70.     else {
  71.         #
  72.         # OTP challenge succeeded, send IP address to firewall
  73.         #
  74.         $ipaddr=$ENV{REMOTE_ADDR} if (not defined $ipaddr or $ipaddr eq "");
  75.         my $socket= new IO::Socket::INET (PeerAddr => $fwhost,
  76.                                           PeerPort => $fwport,
  77.                                           Proto    => "tcp",
  78.                                           Type     => SOCK_STREAM)
  79.             or &Barf2Browser("Authentication succeeded but "
  80.             ."<span class=red>network connection failed</span>");
  81.         print $socket "$ipaddr";
  82.         close $socket;
  83.         my $submitbutton=$cgi->submit(-name=>"ok",
  84.             -value=>"OK");
  85.         $template=HTML::Template->new(filename =>"sesame.tmpl",
  86.             path => "/home/$opie_user/templates");
  87.         $template->param(msgbool => 1);
  88.         $template->param(formstart => $cgi->start_form);
  89.         $template->param(msghdr => "<span class=blue>Authentication succeeded!</span>");
  90.         $template->param(message => "Sent $ipaddr to the firewall");
  91.         $template->param(submit => $submitbutton);
  92.         print $template->output;
  93.     }
  94. }
  95. exit 0;
  96.  
  97. sub Barf2Browser() {
  98.     # output error
  99.     my ($string)=@_;
  100.     my $submitbutton=$cgi->submit(-name=>"ok",
  101.         -value=>"OK");
  102.     $string="undefined" if (not defined $string);
  103.     $template=HTML::Template->new(filename =>"sesame.tmpl",
  104.         path => "/home/$opie_user/templates");
  105.     $template->param(msgbool => 1);
  106.     $template->param(formstart => $cgi->start_form);
  107.     $template->param(msghdr => "Sesame error:");
  108.     $template->param(message => $string);
  109.     $template->param(submit => $submitbutton);
  110.     print $template->output;
  111.     exit 0;
  112. }


The template file is very straight-forward:

HTML:
  1. <!DOCTYPE html
  2.         PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  3.          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  4. <html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
  5. <title>Sesame</title>
  6. <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
  7. <style type="text/css">
  8.  
  9. BODY {background-color: #b0c4ef; color: black}
  10. A:link {color: #000040}
  11. A:external {color: #000040}
  12. A:active {color: #000040}
  13. A:visited {color: #000040}
  14. SPAN.blue {color: #0000c0}
  15. SPAN.red {color: #c00000}
  16. </style>
  17. </head>
  18.  
  19. <tmpl_var name="formstart">
  20. <h1>Sesame Verifyer</h1>
  21. <tmpl_if name="chalbool">
  22.     <h3>Challenge:<br/>
  23.     <tmpl_var name="sequence"> <tmpl_var name="seed"></h3>
  24.     <br/>
  25.     <strong>Response:</strong><br/>
  26.     <tmpl_var name="response"><br/><br/>
  27.     Current IP address:
  28.     </td><td>
  29.     <tt><strong><tmpl_var name="curraddr"></strong></tt>
  30.     </td></tr><tr><td>
  31.     Optionally override with:
  32.     </td><td>
  33.     <tmpl_var name="ipaddr">
  34.     </td></tr></table>
  35. <tmpl_else>
  36.     <tmpl_if name="msgbool">
  37.         <h3><tmpl_var name=msghdr></h3>
  38.         <strong><tmpl_var name="message"></strong>
  39.     </tmpl_if>
  40. </tmpl_if>
  41. <br/><br/><tmpl_var name="submit">
  42. </form>
  43. </body>
  44. </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:

PERL:
  1. #!/usr/bin/perl
  2. # vim:ai:filetype=perl:sta:sw=4:et:
  3. #
  4. # This script will read a line from STDIN, expecting
  5. # expecting an IP address and will use that address
  6. # as a source for a firewall rule that temporarily
  7. # opens one or more ports in the INPUT chain
  8. #
  9. # ports must be specified as /^[tu]\d+$/ (the first
  10. # character specifies the tcp or udp protocol
  11.  
  12. use strict;
  13. use Sys::Syslog qw(:standard :macros);
  14.  
  15. our @ports = ("t22","u5000");
  16. our $timeslot = 300; #5 minutes
  17.  
  18. my $addr=<STDIN>;
  19. chomp $addr;
  20. # check if we received a correct IP address
  21. if ($addr !~ /^(\d{1,3}\.){3}\d{1,3}$/) {
  22.     &LogText(LOG_WARNING, "WARNING: someone tried something nasty!");
  23.     exit 0;
  24. }
  25. foreach my $port (@ports) {
  26.     &IPTrule("-I",$port,$addr);
  27. }
  28. sleep $timeslot;
  29. foreach my $port (@ports) {
  30.     &IPTrule("-D",$port,$addr);
  31. }
  32. exit 0;
  33.  
  34. sub LogText() {
  35.     my ($level, $text)=@_;
  36.     openlog("sesamed","ndelay,pid",LOG_DAEMON);
  37.     syslog($level,$text);
  38.     closelog;
  39. }
  40.  
  41. sub IPTrule() {
  42.     my ($act,$protoport,$addr)=@_;
  43.     my ($proto,$port)=();
  44.     if ($protoport =~ /^([tu])(\d+)$/) {
  45.         $port=$2;
  46.         $proto = ($1 eq "t") ? "tcp" : "udp";
  47.     }
  48.     if (not defined $proto) {
  49.         &LogText(LOG_NOTICE, "BUG: wrong protoport specified");
  50.         return;
  51.     }
  52.     my @cmdline=("/sbin/iptables",$act,"INPUT","-p",$proto,"--dport");
  53.     push @cmdline, ($port,"-s",$addr,"-j","ACCEPT");
  54.     &LogText(LOG_INFO, join(" ", @cmdline));
  55.     system(@cmdline);
  56. }


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

November 15, 2007 • Posted in: code, security

Leave a Reply