Running Network Services under User-Mode Linux, Part I
In my May 2006 Paranoid Penguin column, I expounded on the virtues of Debian 3.1's excellent support for virtualization environments, including User-Mode Linux. In that same issue, in the article “User-Mode Linux”, Matthew Hoskins gave a quick-and-dirty recipe for test-driving User-Mode Linux using prebuilt UML kernels and root filesystem images.
Did those articles whet your appetite for a more comprehensive and security-focused look at using UML? If so, you're in luck; for the next couple of columns, we're going to dive into the User-Mode Linux experience and cover every step (including every command) for creating your very own User-Mode Linux containers for network services.
So, why are we doing this, and what do we hope to achieve? As I've said before in this space, virtualization is similar to the concept of the chroot (changed root) jail. It encapsulates a process or dæmon into a subset of the operating environment in which it resides, in a manner that makes it much harder for attackers to get at the underlying environment should they succeed in compromising that process or dæmon.
Whereas chrooting restricts a process to a subset of the host system's real filesystem, virtualization restricts the process to a complete virtual machine running within the host (real) machine. This includes a completely virtualized hard disk, memory and kernel, and even virtualized system devices, such as network and sound cards. In the case of User-Mode Linux, this is achieved by running a guest (virtual) kernel as a user-space process within the host (real) kernel.
Because both guest and host kernel are Linux kernels, virtualization in User-Mode Linux is fast and efficient. And, because the guest kernel does not need to run as root under the host kernel, even attackers who compromise some dæmon on the guest system and escalate their privileges to root (on the guest system) and somehow manage to gain shell access to the underlying host system will have achieved only unprivileged access to that host system.
This does not make it impossible to gain root access to the host system. If attackers do make it as far as shell access on the host, they may be able to escalate their privileges via some local privilege escalation vulnerability in the host's kernel or some user-space program on the host. (Remember: no vulnerability is strictly local on any networked system!) It does mean, however, that it's more difficult for attackers to get to the point of being able to exploit such a vulnerability, especially if it isn't also present on the guest (virtual) system.
This brings us to our design goals. The guest machine should be as bare-bones as possible with respect to installed software—both to minimize resource utilization and to minimize its potential for compromise (its attack surface). If, for example, the guest machine is to act as a DNS server, it should have basic network support, BIND (or some other DNS server package) and very little else. No X Window System and no Apache—nothing else not directly related to DNS services.
If you're really paranoid, you even can skip the Secure Shell dæmon and instead administer the system via a virtual serial console. (Though allowing SSH from only authorized IP addresses, such as that of the host system, might be a more reasonable middle ground.) You could also run User-Mode Linux under SELinux; however, that's beyond the scope of this series of articles.
If a single bastion server is to host multiple network services—for example, Apache and BIND—you could run two different guest systems on the same host: one containing only Apache and its dependent packages and another containing only BIND et al. In this way, a vulnerability in BIND would not lead directly to Web site defacement. Conversely, a poorly coded Web application would not necessarily lead to DNS tampering.
In summary, our two design principles will be to run one virtual machine per major network service and to make each virtual machine as minimal and secure as possible. The end result will (hopefully) be a very compartmentalized bastion server that places as much defensive abstraction as possible between attackers and total root compromise.
For the remainder of this series of articles, I use the example of a single guest system running BIND. Both guest and host system are based on Debian 3.1, because Debian is so popular for UML guests (it lends itself to stripped-down installations—a trait it shares with Slackware). However, most of what follows also applies to other Linux distributions on both host and guest.
Our tasks are:
Build a host kernel optimized for hosting User-Mode Linux guests.
Build one or more guest kernels to run on top of the host.
Obtain and customize a prebuilt root filesystem for our guests.
Run, configure and harden our guest system for secure DNS services.