import os import optparse import socket import tempfile import shutil import errno import csv import shell HOST = socket.gethostname() PROD_GUESTS = frozenset([ 'bees-knees', 'cats-whiskers', 'busy-beaver', 'pancake-bunny', 'whole-enchilada', 'real-mccoy', 'old-faithful', 'better-mousetrap', 'shining-armor', 'golden-egg', 'miracle-cure', 'lucky-star', ]) WIZARD_GUESTS = frozenset([ 'not-backward', ]) COMMON_CREDS = {} # Format here assumes that we always chmod $USER:$USER, # but note the latter refers to group... # # Important: no leading slashes! COMMON_CREDS['all'] = [ ('root', 0o600, 'root/.bashrc'), ('root', 0o600, 'root/.screenrc'), ('root', 0o600, 'root/.ssh/authorized_keys'), ('root', 0o600, 'root/.ssh/authorized_keys2'), ('root', 0o600, 'root/.vimrc'), ('root', 0o600, 'root/.k5login'), ] COMMON_CREDS['prod'] = [ ('root', 0o600, 'root/.ldapvirc'), ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'), ('root', 0o600, 'etc/ssh/ssh_host_key'), ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'), ('root', 0o600, 'etc/pki/tls/private/scripts-1024.key'), ('root', 0o600, 'etc/pki/tls/private/scripts.key'), ('root', 0o600, 'etc/whoisd-password'), ('afsagent', 0o600, 'etc/daemon.keytab'), ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'), ('root', 0o644, 'etc/ssh/ssh_host_key.pub'), ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'), ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), # technically doesn't have to be secret anymore ('sql', 0o600, 'etc/sql-password'), ('signup', 0o600, 'etc/signup-ldap-pw'), ('logview', 0o600, 'home/logview/.k5login'), # XXX user must be created in Kickstart ] # note that these are duplicates with 'prod', but the difference # is that the files DIFFER between wizard and prod COMMON_CREDS['wizard'] = [ ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'), ('root', 0o600, 'etc/ssh/ssh_host_key'), ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'), ('afsagent', 0o600, 'etc/daemon.keytab'), ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'), ('root', 0o644, 'etc/ssh/ssh_host_key.pub'), ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'), ] MACHINE_CREDS = {} MACHINE_CREDS['all'] = [ # XXX NEED TO CHECK THAT THE CONTENTS ARE SENSIBLE ('root', 0o600, 'etc/krb5.keytab'), ] MACHINE_CREDS['prod'] = [ ('fedora-ds', 0o600, 'etc/dirsrv/keytab'), ] MACHINE_CREDS['wizard'] = [] # Works for passwd and group, but be careful! They're different things! def lookup(filename): # Super-safe to assume and volume IDs (expensive to check) r = { 'root': 0, 'sql': 537704221, } with open(filename, 'rb') as f: reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE) for row in reader: r[row[0]] = int(row[2]) return r def drop_caches(): with open("/proc/sys/vm/drop_caches", 'w') as f: f.write("1") def mkdir_p(path): # it's like mkdir -p try: os.makedirs(path) except OSError as e: if e.errno == errno.EEXIST: pass else: raise # XXX This code is kind of dangerous, because we are directly using the # kernel modules to manipulate possibly untrusted disk images. This # means that if an attacker can corrupt the disk, and exploit a problem # in the kernel vfs driver, he can escalate a guest root exploit # to a host root exploit. Ultimately we should use libguestfs # which makes this attack harder to pull off, but at the time of writing # squeeze didn't package libguestfs. # # We try to minimize attack surface by explicitly specifying the # expected filesystem type. class WithMount(object): """Context for running code with an extra mountpoint.""" guest = None types = None # comma separated, like the mount argument -t mount = None dev = None def __init__(self, guest, types): self.guest = guest self.types = types def __enter__(self): drop_caches() self.dev = "/dev/%s/%s-root" % (HOST, self.guest) mapper_name = shell.eval("kpartx", "-l", self.dev).split()[0] shell.call("kpartx", "-a", self.dev) mapper = "/dev/mapper/%s" % mapper_name # this is why bracketing functions and hanging lambdas are a good idea try: self.mount = tempfile.mkdtemp("-%s" % self.guest, 'vm-', '/mnt') # no trailing slash try: shell.call("mount", "--types", self.types, mapper, self.mount) except: os.rmdir(self.mount) raise except: shell.call("kpartx", "-d", self.dev) raise return self.mount def __exit__(self, _type, _value, _traceback): shell.call("umount", self.mount) os.rmdir(self.mount) shell.call("kpartx", "-d", self.dev) drop_caches() def main(): usage = """usage: %prog [push|pull] [common|machine] GUEST""" parser = optparse.OptionParser(usage) # ext3 will probably supported for a while yet and a pretty # reasonable thing to always try parser.add_option('-t', '--types', dest="types", default="ext4,ext3", help="filesystem type(s)") # same arg as 'mount' parser.add_option('--creds-dir', dest="creds_dir", default="/root/creds", help="directory to store/fetch credentials in") options, args = parser.parse_args() if not os.path.isdir(options.creds_dir): raise Exception("%s does not exist" % options.creds_dir) # XXX check owned by root and appropriately chmodded os.umask(0o077) # overly restrictive if len(args) != 3: parser.print_help() raise Exception("Wrong number of arguments") command = args[0] files = args[1] guest = args[2] if guest in PROD_GUESTS: mode = 'prod' elif guest in WIZARD_GUESTS: mode = 'wizard' else: raise Exception("Unrecognized guest %s" % guest) with WithMount(guest, options.types) as tmp_mount: uid_lookup = lookup("%s/etc/passwd" % tmp_mount) gid_lookup = lookup("%s/etc/group" % tmp_mount) def push_files(files, type): for (usergroup, perms, f) in files: dest = "%s/%s" % (tmp_mount, f) mkdir_p(os.path.dirname(dest)) # useful for .ssh # assuming OK to overwrite # XXX we could compare the files before doing anything... shutil.copyfile("%s/%s/%s" % (options.creds_dir, type, f), dest) try: os.chown(dest, uid_lookup[usergroup], gid_lookup[usergroup]) os.chmod(dest, perms) except: # never ever leave un-chowned files lying around os.unlink(dest) raise def pull_files(files, type): for (_, _, f) in files: dest = "%s/%s/%s" % (options.creds_dir, type, f) mkdir_p(os.path.dirname(dest)) # error if doesn't exist shutil.copyfile("%s/%s" % (tmp_mount, f), dest) # XXX ideally we should check these *before* we mount, but Python # makes that pretty annoying to do if command == "push": run = push_files elif command == "pull": run = pull_files else: raise Exception("Unknown command %s, valid values are 'push' and 'pull'" % command) if files == 'common': run(COMMON_CREDS['all'], 'all') run(COMMON_CREDS[mode], mode) elif files == 'machine': run(MACHINE_CREDS['all'], 'machine/%s' % guest) run(MACHINE_CREDS[mode], 'machine/%s' % guest) else: raise Exception("Unknown file set %s, valid values are 'common' and 'machine'" % files) if __name__ == "__main__": main()