| [1999] | 1 | import os | 
|---|
|  | 2 | import optparse | 
|---|
|  | 3 | import socket | 
|---|
|  | 4 | import tempfile | 
|---|
|  | 5 | import shutil | 
|---|
|  | 6 | import errno | 
|---|
| [2044] | 7 | import csv | 
|---|
| [1999] | 8 |  | 
|---|
|  | 9 | import shell | 
|---|
|  | 10 |  | 
|---|
|  | 11 | HOST = socket.gethostname() | 
|---|
|  | 12 |  | 
|---|
|  | 13 | # XXX test server and wizard server | 
|---|
|  | 14 |  | 
|---|
| [2044] | 15 | # UIDs (sketchy): | 
|---|
|  | 16 | #   signup 102 | 
|---|
|  | 17 | #   fedora-ds 103 (sketchy, not true for b-b) | 
|---|
|  | 18 | #   logview 501 (really sketchy, since it's in the dynamic range) | 
|---|
| [1999] | 19 |  | 
|---|
| [2044] | 20 | # Works for passwd and group, but be careful! They're different things! | 
|---|
|  | 21 | def lookup(filename): | 
|---|
|  | 22 | # Super-safe to assume and volume IDs (expensive to check) | 
|---|
|  | 23 | r = { | 
|---|
|  | 24 | 'root': 0, | 
|---|
|  | 25 | 'sql': 537704221, | 
|---|
|  | 26 | } | 
|---|
|  | 27 | with open(filename, 'rb') as f: | 
|---|
|  | 28 | reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE) | 
|---|
|  | 29 | for row in reader: | 
|---|
|  | 30 | r[row[0]] = int(row[2]) | 
|---|
|  | 31 | return r | 
|---|
|  | 32 |  | 
|---|
|  | 33 | # Format here assumes that we always chmod $USER:$USER ... | 
|---|
|  | 34 | # but note the latter refers to group... | 
|---|
| [1999] | 35 | COMMON_CREDS = [ | 
|---|
| [2044] | 36 | ('root', 0o600, 'root/.bashrc'), | 
|---|
|  | 37 | ('root', 0o600, 'root/.screenrc'), | 
|---|
|  | 38 | ('root', 0o600, 'root/.ssh/authorized_keys'), | 
|---|
|  | 39 | ('root', 0o600, 'root/.ssh/authorized_keys2'), | 
|---|
|  | 40 | ('root', 0o600, 'root/.vimrc'), | 
|---|
|  | 41 | ('root', 0o600, 'root/.k5login'), | 
|---|
| [1999] | 42 | # punted /root/.ssh/known_hosts | 
|---|
|  | 43 |  | 
|---|
| [2044] | 44 | # XXX user must be created in Kickstart | 
|---|
|  | 45 | ('logview', 0o600, 'home/logview/.k5login'), | 
|---|
| [1999] | 46 | ] | 
|---|
|  | 47 |  | 
|---|
|  | 48 | COMMON_PROD_CREDS = [ # important: no leading slashes! | 
|---|
| [2044] | 49 | ('root', 0o600, 'root/.ldapvirc'), | 
|---|
|  | 50 | ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'), | 
|---|
|  | 51 | ('root', 0o600, 'etc/ssh/ssh_host_key'), | 
|---|
|  | 52 | ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'), | 
|---|
| [2045] | 53 | ('root', 0o600, 'etc/pki/tls/private/scripts-1024.key'), | 
|---|
| [2044] | 54 | ('root', 0o600, 'etc/pki/tls/private/scripts.key'), | 
|---|
|  | 55 | ('root', 0o600, 'etc/whoisd-password'), | 
|---|
| [2049] | 56 | ('afsagent', 0o600, 'etc/daemon.keytab'), | 
|---|
| [1999] | 57 |  | 
|---|
| [2044] | 58 | ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'), | 
|---|
|  | 59 | ('root', 0o644, 'etc/ssh/ssh_host_key.pub'), | 
|---|
|  | 60 | ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'), | 
|---|
| [1999] | 61 |  | 
|---|
| [2269] | 62 | ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), # technically doesn't have to be secret anymore | 
|---|
|  | 63 | ('sql', 0o600, 'etc/sql-password'), | 
|---|
| [2044] | 64 | ('signup', 0o600, 'etc/signup-ldap-pw'), | 
|---|
| [1999] | 65 | ] | 
|---|
|  | 66 |  | 
|---|
|  | 67 | MACHINE_PROD_CREDS = [ | 
|---|
|  | 68 | # XXX NEED TO CHECK THAT THESE ARE SENSIBLE | 
|---|
| [2044] | 69 | ('root', 0o600, 'etc/krb5.keytab'), | 
|---|
|  | 70 | ('fedora-ds', 0o600, 'etc/dirsrv/keytab') | 
|---|
| [1999] | 71 | ] | 
|---|
|  | 72 |  | 
|---|
| [2246] | 73 | def drop_caches(): | 
|---|
|  | 74 | with open("/proc/sys/vm/drop_caches", 'w') as f: | 
|---|
|  | 75 | f.write("1") | 
|---|
|  | 76 |  | 
|---|
| [2044] | 77 | def mkdir_p(path): # it's like mkdir -p | 
|---|
| [1999] | 78 | try: | 
|---|
|  | 79 | os.makedirs(path) | 
|---|
| [2044] | 80 | except OSError as e: | 
|---|
|  | 81 | if e.errno == errno.EEXIST: | 
|---|
| [1999] | 82 | pass | 
|---|
|  | 83 | else: raise | 
|---|
|  | 84 |  | 
|---|
| [2044] | 85 | # XXX This code is kind of dangerous, because we are directly using the | 
|---|
|  | 86 | # kernel modules to manipulate possibly untrusted disk images.  This | 
|---|
|  | 87 | # means that if an attacker can corrupt the disk, and exploit a problem | 
|---|
|  | 88 | # in the kernel vfs driver, he can escalate a guest root exploit | 
|---|
|  | 89 | # to a host root exploit.  Ultimately we should use libguestfs | 
|---|
|  | 90 | # which makes this attack harder to pull off, but at the time of writing | 
|---|
|  | 91 | # squeeze didn't package libguestfs. | 
|---|
|  | 92 | # | 
|---|
|  | 93 | # We try to minimize attack surface by explicitly specifying the | 
|---|
|  | 94 | # expected filesystem type. | 
|---|
| [1999] | 95 | class WithMount(object): | 
|---|
|  | 96 | """Context for running code with an extra mountpoint.""" | 
|---|
|  | 97 | guest = None | 
|---|
| [2044] | 98 | types = None # comma separated, like the mount argument -t | 
|---|
| [1999] | 99 | mount = None | 
|---|
|  | 100 | dev = None | 
|---|
| [2044] | 101 | def __init__(self, guest, types): | 
|---|
| [1999] | 102 | self.guest = guest | 
|---|
| [2044] | 103 | self.types = types | 
|---|
| [1999] | 104 | def __enter__(self): | 
|---|
| [2246] | 105 | drop_caches() | 
|---|
| [1999] | 106 | self.dev = "/dev/%s/%s-root" % (HOST, self.guest) | 
|---|
|  | 107 |  | 
|---|
|  | 108 | mapper_name = shell.eval("kpartx", "-l", self.dev).split()[0] | 
|---|
|  | 109 | shell.call("kpartx", "-a", self.dev) | 
|---|
|  | 110 | mapper = "/dev/mapper/%s" % mapper_name | 
|---|
|  | 111 |  | 
|---|
|  | 112 | # this is why bracketing functions and hanging lambdas are a good idea | 
|---|
|  | 113 | try: | 
|---|
|  | 114 | self.mount = tempfile.mkdtemp("-%s" % self.guest, 'vm-', '/mnt') # no trailing slash | 
|---|
|  | 115 | try: | 
|---|
| [2044] | 116 | shell.call("mount", "--types", self.types, mapper, self.mount) | 
|---|
| [1999] | 117 | except: | 
|---|
|  | 118 | os.rmdir(self.mount) | 
|---|
|  | 119 | raise | 
|---|
|  | 120 | except: | 
|---|
|  | 121 | shell.call("kpartx", "-d", self.dev) | 
|---|
|  | 122 | raise | 
|---|
|  | 123 |  | 
|---|
|  | 124 | return self.mount | 
|---|
| [2044] | 125 | def __exit__(self, _type, _value, _traceback): | 
|---|
| [1999] | 126 | shell.call("umount", self.mount) | 
|---|
|  | 127 | os.rmdir(self.mount) | 
|---|
|  | 128 | shell.call("kpartx", "-d", self.dev) | 
|---|
| [2246] | 129 | drop_caches() | 
|---|
| [1999] | 130 |  | 
|---|
|  | 131 | def main(): | 
|---|
| [2044] | 132 | usage = """usage: %prog [push|pull|pull-common] GUEST""" | 
|---|
| [1999] | 133 |  | 
|---|
|  | 134 | parser = optparse.OptionParser(usage) | 
|---|
| [2044] | 135 | # ext3 will probably supported for a while yet and a pretty | 
|---|
|  | 136 | # reasonable thing to always try | 
|---|
|  | 137 | parser.add_option('-t', '--types', dest="types", default="ext4,ext3", | 
|---|
|  | 138 | help="filesystem type(s)") | 
|---|
|  | 139 | parser.add_option('--creds-dir', dest="creds_dir", default="/root/creds", | 
|---|
|  | 140 | help="directory to store/fetch credentials in") | 
|---|
|  | 141 | options, args = parser.parse_args() | 
|---|
| [1999] | 142 |  | 
|---|
| [2044] | 143 | if not os.path.isdir(options.creds_dir): | 
|---|
|  | 144 | raise Exception("/root/creds does not exist") # XXX STRING | 
|---|
|  | 145 | # XXX check owned by root and appropriately chmodded | 
|---|
| [1999] | 146 |  | 
|---|
|  | 147 | os.umask(0o077) # overly restrictive | 
|---|
|  | 148 |  | 
|---|
|  | 149 | if len(args) != 2: | 
|---|
| [2044] | 150 | parser.print_help() | 
|---|
| [1999] | 151 | raise Exception("Wrong number of arguments") | 
|---|
|  | 152 |  | 
|---|
|  | 153 | command = args[0] | 
|---|
|  | 154 | guest   = args[1] | 
|---|
|  | 155 |  | 
|---|
| [2044] | 156 | with WithMount(guest, options.types) as tmp_mount: | 
|---|
|  | 157 | uid_lookup = lookup("%s/etc/passwd" % tmp_mount) | 
|---|
|  | 158 | gid_lookup = lookup("%s/etc/group" % tmp_mount) | 
|---|
| [1999] | 159 | def push_files(files, type): | 
|---|
| [2044] | 160 | for (usergroup, perms, f) in files: | 
|---|
| [1999] | 161 | dest = "%s/%s" % (tmp_mount, f) | 
|---|
| [2044] | 162 | mkdir_p(os.path.dirname(dest)) # useful for .ssh | 
|---|
| [1999] | 163 | # assuming OK to overwrite | 
|---|
| [2044] | 164 | # XXX we could compare the files before doing anything... | 
|---|
|  | 165 | shutil.copyfile("%s/%s/%s" % (options.creds_dir, type, f), dest) | 
|---|
|  | 166 | try: | 
|---|
|  | 167 | os.chown(dest, uid_lookup[usergroup], gid_lookup[usergroup]) | 
|---|
|  | 168 | os.chmod(dest, perms) | 
|---|
|  | 169 | except: | 
|---|
|  | 170 | # never ever leave un-chowned files lying around | 
|---|
|  | 171 | os.unlink(dest) | 
|---|
|  | 172 | raise | 
|---|
| [1999] | 173 | def pull_files(files, type): | 
|---|
|  | 174 | for (_, _, f) in files: | 
|---|
| [2044] | 175 | dest = "%s/%s/%s" % (options.creds_dir, type, f) | 
|---|
| [1999] | 176 | mkdir_p(os.path.dirname(dest)) | 
|---|
|  | 177 | # error if doesn't exist | 
|---|
|  | 178 | shutil.copyfile("%s/%s" % (tmp_mount, f), dest) | 
|---|
|  | 179 |  | 
|---|
|  | 180 | if command == "push": | 
|---|
|  | 181 | push_files(COMMON_CREDS, 'common') | 
|---|
|  | 182 | push_files(COMMON_PROD_CREDS,  'common') | 
|---|
|  | 183 | push_files(MACHINE_PROD_CREDS, 'machine/%s' % guest) | 
|---|
|  | 184 | elif command == "pull": | 
|---|
|  | 185 | pull_files(MACHINE_PROD_CREDS, 'machine/%s' % guest) | 
|---|
|  | 186 | elif command == "pull-common": | 
|---|
|  | 187 | pull_files(COMMON_CREDS, 'common') | 
|---|
|  | 188 | pull_files(COMMON_PROD_CREDS,  'common') | 
|---|
|  | 189 |  | 
|---|
|  | 190 | if __name__ == "__main__": | 
|---|
|  | 191 | main() | 
|---|