From 84c08e4ee04cadbaf7d7be8bc1c3b9023918e276 Mon Sep 17 00:00:00 2001
From: Balazs Lecz <leczb@google.com>
Date: Wed, 26 May 2010 16:53:02 +0100
Subject: [PATCH] Add KVM chroot feature

This patch adds a new boolean hypervisor parameter to the KVM hypervisor,
named 'use_chroot'.
If it's turned on for an instance, than KVM is started in "chroot mode":
Ganeti creates an empty directory for the instance and passes the path
of this dir to KVM via the -chroot flag.
KVM changes its root to this directory after starting up.

It also adds a "quarantine" feature for moving any unexpected files to
a separate directory for later analysis.

Signed-off-by: Balazs Lecz <leczb@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 lib/constants.py         |  3 +++
 lib/hypervisor/hv_kvm.py | 47 ++++++++++++++++++++++++++++++++++++++--
 man/gnt-instance.sgml    | 19 ++++++++++++++++
 3 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/lib/constants.py b/lib/constants.py
index 7392a9556..a6cdcd6b7 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -496,6 +496,7 @@ HV_SECURITY_MODEL = "security_model"
 HV_SECURITY_DOMAIN = "security_domain"
 HV_KVM_FLAG = "kvm_flag"
 HV_VHOST_NET = "vhost_net"
+HV_KVM_USE_CHROOT = "use_chroot"
 
 HVS_PARAMETER_TYPES = {
   HV_BOOT_ORDER: VTYPE_STRING,
@@ -527,6 +528,7 @@ HVS_PARAMETER_TYPES = {
   HV_SECURITY_DOMAIN: VTYPE_STRING,
   HV_KVM_FLAG: VTYPE_STRING,
   HV_VHOST_NET: VTYPE_BOOL,
+  HV_KVM_USE_CHROOT: VTYPE_BOOL,
   }
 
 HVS_PARAMETERS = frozenset(HVS_PARAMETER_TYPES.keys())
@@ -795,6 +797,7 @@ HVC_DEFAULTS = {
     HV_SECURITY_DOMAIN: '',
     HV_KVM_FLAG: "",
     HV_VHOST_NET: False,
+    HV_KVM_USE_CHROOT: False,
     },
   HT_FAKE: {
     },
diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py
index af04dcacb..a962a0b6d 100644
--- a/lib/hypervisor/hv_kvm.py
+++ b/lib/hypervisor/hv_kvm.py
@@ -23,6 +23,7 @@
 
 """
 
+import errno
 import os
 import os.path
 import re
@@ -51,7 +52,16 @@ class KVMHypervisor(hv_base.BaseHypervisor):
   _UIDS_DIR = _ROOT_DIR + "/uid" # contains instances reserved uids
   _CTRL_DIR = _ROOT_DIR + "/ctrl" # contains instances control sockets
   _CONF_DIR = _ROOT_DIR + "/conf" # contains instances startup data
-  _DIRS = [_ROOT_DIR, _PIDS_DIR, _UIDS_DIR, _CTRL_DIR, _CONF_DIR]
+  # KVM instances with chroot enabled are started in empty chroot directories.
+  _CHROOT_DIR = _ROOT_DIR + "/chroot" # for empty chroot directories
+  # After an instance is stopped, its chroot directory is removed.
+  # If the chroot directory is not empty, it can't be removed.
+  # A non-empty chroot directory indicates a possible security incident.
+  # To support forensics, the non-empty chroot directory is quarantined in
+  # a separate directory, called 'chroot-quarantine'.
+  _CHROOT_QUARANTINE_DIR = _ROOT_DIR + "/chroot-quarantine"
+  _DIRS = [_ROOT_DIR, _PIDS_DIR, _UIDS_DIR, _CTRL_DIR, _CONF_DIR,
+           _CHROOT_DIR, _CHROOT_QUARANTINE_DIR]
 
   PARAMETERS = {
     constants.HV_KERNEL_PATH: hv_base.OPT_FILE_CHECK,
@@ -87,6 +97,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     constants.HV_KVM_FLAG:
       hv_base.ParamInSet(False, constants.HT_KVM_FLAG_VALUES),
     constants.HV_VHOST_NET: hv_base.NO_CHECK,
+    constants.HV_KVM_USE_CHROOT: hv_base.NO_CHECK,
     }
 
   _MIGRATION_STATUS_RE = re.compile('Migration\s+status:\s+(\w+)',
@@ -229,6 +240,13 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """
     return utils.PathJoin(cls._CONF_DIR, "%s.runtime" % instance_name)
 
+  @classmethod
+  def _InstanceChrootDir(cls, instance_name):
+    """Returns the name of the KVM chroot dir of the instance
+
+    """
+    return utils.PathJoin(cls._CHROOT_DIR, instance_name)
+
   @classmethod
   def _TryReadUidFile(cls, uid_file):
     """Try to read a uid file
@@ -246,7 +264,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
   @classmethod
   def _RemoveInstanceRuntimeFiles(cls, pidfile, instance_name):
-    """Removes an instance's rutime sockets/files.
+    """Removes an instance's rutime sockets/files/dirs.
 
     """
     utils.RemoveFile(pidfile)
@@ -258,6 +276,24 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     utils.RemoveFile(uid_file)
     if uid is not None:
       uidpool.ReleaseUid(uid)
+    try:
+      chroot_dir = cls._InstanceChrootDir(instance_name)
+      utils.RemoveDir(chroot_dir)
+    except OSError, err:
+      if err.errno == errno.ENOTEMPTY:
+        # The chroot directory is expected to be empty, but it isn't.
+        new_chroot_dir = tempfile.mkdtemp(dir=cls._CHROOT_QUARANTINE_DIR,
+                                          prefix="%s-%s-" %
+                                          (instance_name,
+                                           utils.TimestampForFilename()))
+        logging.warning("The chroot directory of instance %s can not be"
+                        " removed as it is not empty. Moving it to the"
+                        " quarantine instead. Please investigate the"
+                        " contents (%s) and clean up manually",
+                        instance_name, new_chroot_dir)
+        utils.RenameFile(chroot_dir, new_chroot_dir)
+      else:
+        raise
 
   def _WriteNetScript(self, instance, seq, nic):
     """Write a script to connect a net interface to the proper bridge.
@@ -529,6 +565,9 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     if hvp[constants.HV_USE_LOCALTIME]:
       kvm_cmd.extend(['-localtime'])
 
+    if hvp[constants.HV_KVM_USE_CHROOT]:
+      kvm_cmd.extend(['-chroot', self._InstanceChrootDir(instance.name)])
+
     # Save the current instance nics, but defer their expansion as parameters,
     # as we'll need to generate executable temp files for them.
     kvm_nics = instance.nics
@@ -644,6 +683,10 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         raise errors.HypervisorError("Failed to open VNC password file %s: %s"
                                      % (vnc_pwd_file, err))
 
+    if hvp[constants.HV_KVM_USE_CHROOT]:
+      utils.EnsureDirs([(self._InstanceChrootDir(name),
+                         constants.SECURE_DIR_MODE)])
+
     if security_model == constants.HT_SM_POOL:
       ss = ssconf.SimpleStore()
       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\n")
diff --git a/man/gnt-instance.sgml b/man/gnt-instance.sgml
index 2cf24af14..3d3563fc4 100644
--- a/man/gnt-instance.sgml
+++ b/man/gnt-instance.sgml
@@ -658,6 +658,25 @@
               </listitem>
             </varlistentry>
 
+            <varlistentry>
+              <term>use_chroot</term>
+              <listitem>
+                <simpara>Valid for the KVM hypervisor.</simpara>
+
+                <simpara>This boolean option determines wether to run the KVM
+                instance in a chroot directory.
+                </simpara>
+                <para>If it is set to <quote>true</quote>, an empty directory
+                is created before starting the instance and its path is passed via
+                the -chroot flag to kvm.
+                The directory is removed when the instance is stopped.
+                </para>
+
+                <simpara>It is set to <quote>false</quote> by default.</simpara>
+
+              </listitem>
+            </varlistentry>
+
           </variablelist>
 
         </para>
-- 
GitLab