Paranoid Penguin - Running Network Services under User-Mode Linux, Part II
Here in the Paranoid Penguin column, we're in the midst of building a virtual network server using User-Mode Linux. Last month, I explained why this is a good idea, how it works, how to prepare your host for optimized User-Mode Linux operation and how to build a kernel for your guest (virtual) system(s).
This month, we turn our attention to the guest system: how to obtain a prebuilt root filesystem image, how to configure networking on both your host and guest systems, and how to begin customizing the root filesystem image for your own purposes.
First, here's a quick review of what we're trying to do, in case you missed last month's column. Our objective is to use User-Mode Linux to create one or more virtual guest machines, each running a different network service. That way, if one application (for example, BIND) on one guest machine gets compromised somehow, Sendmail, Apache and whatever else you've got running on other guest systems (or on the underlying host system itself) won't be affected.
(Per User-Mode Linux convention, we're using the word host to denote a system on top of which virtual machines run and the word guest to denote a virtual system instance.)
Debian is our somewhat arbitrary choice here for both host and guest systems, due to the ease with which you can create bare-bones Debian installations, though User-Mode Linux itself is decidedly distribution-agnostic. We'll create a single guest system, running BIND software for DNS services.
On the strength of last month's procedures, hopefully you've got a skas-enabled host kernel and a guest kernel compiled for the um architecture. Now, it's time to acquire or build a root filesystem image.
When your Linux host starts up, it learns where / is via the root command-line switch; somewhere in lilo.conf or menu.lst is a kernel-invocation line containing something like root=/dev/hda1. That's how it works with User-Mode Linux too, except that rather than a physical hard disk, such as /dev/hda, we generally use a virtual disk in the form of a single flat file, called a root filesystem image.
The root filesystem image contains a complete Linux distribution. You've already created similar image files yourself if you've ever copied a CD-ROM to an ISO file (or vice versa). Using a filesystem that takes the form of a single file has two important ramifications for User-Mode Linux: first, it helps keep your guest system relatively compact and portable; second, it makes change control as simple as tracking changes to a single file, via the COW file method.
Suppose I start a User-Mode Linux guest with this command:
umluser@host:~> ./guestkernel ubd0=mycow,my_root_fs root=/dev/ubda
Note the umluser@host prompt. I'm executing this command from a shell session to which I'm logged in as a regular user, not root. guestkernel is my executable User-Mode Linux guest kernel; ubd0 is a virtual disk device I'm declaring to consist of the image file my_root_fs plus a change-on-write (COW) file called mycow. The root switch defines our root partition to be the virtual disk ubda (identified by its full path, /dev/ubda).
Somewhat confusingly, by convention, virtual disk declarations use numbered device names (ubd0, ubd1 and so on), but root filesystem definitions use the corresponding letters instead (ubda, ubdb and so on), which are synonymous. The command ./guestkernel ubda=mycow,my_root_fs root=/dev/ubda actually works just as well on my SUSE system as the above command, but your distribution of choice may behave differently.
Strictly speaking, the COW file is optional. If you specify one, changes you make to the image file during your User-Mode Linux session will be written to the COW file rather than to the disk image itself. If you omit the COW filename, the image file will be written to directly by the guest kernel—that is, any changes you make to your guest system will be “permanent”.
As far as I'm concerned, when using UML in security scenarios, COW files are mandatory. A key assumption in using User-Mode Linux for hosting a network service is that this service may be compromised in some way, and if it is, you'll want to be able to recover as quickly as possible. If you use a COW file, all you'll need to do to restore a guest system to its baseline state is delete the old COW file and create a new (empty) one.
Another key advantage of using COW files is that they allow you to use the same root filesystem image on more than one guest system simultaneously. All you need to do is specify a different COW file each time you bring up a guest kernel. In fact, you can use both the same image file and the same kernel for multiple guests. As you can guess, we're going to use a COW file in our example scenarios.
The procedure for building your own root filesystem image boils down to this:
Create an empty filesystem image file and mount it to some directory.
Install Linux into that directory.
Sounds simple, right? On Debian and SUSE it is—sort of. On other distributions, it's much less so. Regardless, I'm going to save a more-detailed discussion of that process for my next column, in which I'll cover what I consider to be advanced User-Mode Linux topics and techniques. In the interests of getting you up and running with User-Mode Linux in a gratifyingly quick manner, for now I recommend you download a prebuilt image.
My favorite source of these is Nagafix Ltd.'s “UML Resources” page (see the on-line Resources) from whence you can download root filesystem images for not only Debian guests, but also Gentoo, Slackware, Fedora, Ubuntu and others. Nagafix makes a reasonable effort to keep these images up to date with security patches, which is a nice touch.
In addition, Nagafix provides an MD5 and SHA hash of each image file it provides. You may miss them if you click directly on the x86 and AMD64 links on the page cited above; instead, use the OS-name links, each of which leads to a page containing links not only to images but also to build logs and hashes, plus handy tips on how to update the images yourself.
I obtained my Debian 3.1 image by navigating to uml.nagafix.co.uk, clicking on Debian 3.1, and then clicking on the root_fs and MD5 links to download the files Debian-3.1-x86-root_fs.bz2 and Debian-3.1-x86-root_fs.bz2.md5, respectively. After my downloads were complete (the filesystem image itself is 169MB!), I verified the MD5 signature from within a terminal window with the command:
md5sum -c ./Debian-3.1-x86-root_fs.bz2.md5
When in Doubt, Roll Your Own Image
Even if you use a root filesystem image from a trusted source and verify its integrity via an MD5, SHA or GPG hash/signature, the fact is, if you're truly worried about security (we are, aren't we?), you're much better off building your own filesystem image than using someone else's.
I'm indulging in just a little laziness and instant gratification by using a prebuilt image in this article, which I think is justifiable in the larger aim of encouraging UML experimentation and adoption. Just be sure to check your image's hash/signature, and the first time you mount it in UML, run apt-get dist-upgrade (or YaST Online Update, yum or whatever update mechanism your guest's distro supports).
Next time, I'll discuss the filesystem image build process in more depth, as well as how to use iptables both on your host and on your guest OSes to add another layer of protection to your virtual machines.
And, now we're ready to boot our virtual guest for the first time. We've got a guest kernel named uml-guestkernel-220.127.116.11 (from my previous column's example) and a root filesystem image named Debian-3.1-x86-root_fs.bz2. You should already be logged in to a terminal session as a nonroot user. Uncompress the filesystem image with the command:
Next, just as a sanity check, try booting your guest system:
umluser@host:~> ./uml-guestkernel-18.104.22.168 ↪ubd0=testcow,Debian-3.1-x86-root_fs root=/dev/ubda
If all is well, you should see some User-Mode Linux messages, followed by a longer string of Linux kernel startup messages, ending with a login prompt. Log in as root—you won't be prompted for a password. Feel free to poke around a bit; you won't hurt anything that can't be fixed later by starting with a fresh COW file.
To see a list of installed packages, enter the command dpkg -l |less. You may be surprised by how few Debian packages are present. Don't worry; you'll be able to install additional packets with apt-get, just like on a “real” Debian system. When you're done with your initial exploration, issue the command halt to shut down your guest system cleanly. We've got some things to do before your guest system can do any serious work—first and foremost is configuring networking.
There are a variety of ways to network UML guests, all of which are described in Rusty Russell's User-Mode Linux HOWTO (see Resources). The best option for using UML guests as network servers is bridging, in which your host system acts like an Ethernet bridge between itself, the UML guests running on it and the outside world.
In a nutshell, the procedure is this:
Configure your host's TCP/IP stack as a virtual bridge, and then define your “real” network interface as the first “port” on that bridge.
For each guest system you intend to run, create a local tunnel interface and define it as another port on the bridge.
When you start a guest system, define its virtual Ethernet interface (eth0) to be the tunnel interface you created in the previous step.
Listing 1 shows the precise series of commands this translates to, adapted from David Cannings' useful article “Networking UML Using Bridging”. All these commands must be executed as root.
Listing 1. Setting Up Bridged Networking
root@host# bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward' root@host# apt-get install bridge-utils uml-utilities root@host# ifconfig eth0 0.0.0.0 promisc up root@host# brctl addbr uml-bridge root@host# brctl setfd uml-bridge 0 root@host# brctl sethello uml-bridge 0 root@host# brctl stp uml-bridge off root@host# ifconfig uml-bridge 192.168.250.250 netmask 255.255.255.0 up root@host# brctl addif uml-bridge eth0 root@host# tunctl -u umluser -t uml-conn0 root@host# chgrp uml-net /dev/net/tun root@host# chmod 660 /dev/net/tun root@host# ifconfig uml-conn0 0.0.0.0 promisc up root@host# brctl addif uml-bridge uml-conn0
The first command enables IP forwarding on your host. Although, technically, bridging happens at a lower level than IP forwarding, they amount to the same thing from the kernel's perspective. Accordingly, if you have a local iptables policy on your host, you'll need to add rules to the FORWARD table to enable traffic to and from the tunnel interfaces you attach to the host's bridge.
The second command (apt-get install...), obviously, installs the Debian packages bridge-utils and uml-utilities. bridge-utilities provides the brctl command, and uml-utilities provides the tunctl command. For these commands to work, your host kernel needs to have been compiled with 802.1d Ethernet bridging, IP tunneling, Bridged IP/ARP packet filtering and Universal TUN/TAP device driver support.
The third command in Listing 1 (ifconfig eth0...) may seem a bit scary. It resets your host's Ethernet interface to a (temporarily) IP-free state. Be prepared for an interruption in local network functionality after you execute this command.
The subsequent six commands, however, will restore it by defining a new virtual bridge device (called uml-bridge), configuring it, assigning your host's IP address to it (192.168.250.250 in this example), and attaching eth0 to it as a virtual bridge port. If the IP address of eth0 on your host was 10.1.1.10 before you reset it to 0.0.0.0, after issuing the first four brctl commands you would use ifconfig uml-bridge 10.1.1.10 netmask 255.255.255.0 up. At this point, your host should be able to interact with the outside world in exactly the same way as it did before (unless of course your local iptables policy doesn't have appropriate FORWARD rules yet).
All right, our host system is now a bridge. All that remains is to attach a tunnel port to it. You should repeat the remaining steps in Listing 1 (starting with tunctl -u...) for each guest system you intend to run.
In the tunctl -u... command, umluser is the name of the unprivileged account you intend to use when executing guest kernels, and uml-conn0 is the name of the new tunnel interface you're creating.
In the subsequent chgrp and chmod commands, we're changing the permissions of the virtual tunnel device, always /dev/net/tun, to be readable and writable by our unprivileged account. In this example, therefore, the account umluser belongs to the group uml-net. (On my real-life test system, I instead used the the group wheel, which my unprivileged account mick belongs to.)
After setting the new tunnel interface's IP address to 0.0.0.0 (just like we did with eth0), we define it as another port on the local bridge with that last brctl command.
That's it! Now when we start the guest system, we add the option eth0=tuntap,uml-conn0 to our kernel command line, which tells the kernel to use the tunnel interface uml-conn0 as its virtual eth0. Our complete example command line, which unlike Listing 1, should be run by a nonprivileged user rather than root, looks like this:
umluser@host$ ./debkern ubd0=debcow,debroot root=/dev/ubda ↪eth0=tuntap,uml-conn0
After the virtual machine starts, you can assign an IP address to (virtual) eth0 via ifconfig, define a default route via route add... (using the same gateway IP that your host system uses), set DNS lookup information in /etc/resolv.conf, and, in short, configure it in precisely the same way that you'd configure a real Debian system.
Once your virtual machine is successfully communicating with your local LAN and beyond, you should immediately configure apt-get and use it to install the latest Debian patches on your virtual guest. You'll need apt-get working anyhow to install the network software you've just gone to all the trouble of building this virtual machine to run. In the case of our example virtual DNS server, these would probably be the Debian packages bind9 and maybe also bind9-doc. Remember, all of these changes will be made to your COW file, so be sure to specify the same COW file on subsequent startups (or merge it into your image via the uml_moo command).
Next time, we'll wrap up this series by discussing additional security controls you can use on your guest systems, a nifty COW file trick or two and, of course, how to create a custom root filesystem image. Until then, be safe!
Resources for this article: /article/9385.
Mick Bauer (firstname.lastname@example.org) is Network Security Architect for one of the US's largest banks. He is the author of the O'Reilly book Linux Server Security, 2nd edition (formerly called Building Secure Servers With Linux), an occasional presenter at information security conferences and composer of the “Network Engineering Polka”.