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