[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 | |
---|
[2044] | 62 | ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), |
---|
| 63 | ('signup', 0o600, 'etc/signup-ldap-pw'), |
---|
[1999] | 64 | ] |
---|
| 65 | |
---|
| 66 | MACHINE_PROD_CREDS = [ |
---|
| 67 | # XXX NEED TO CHECK THAT THESE ARE SENSIBLE |
---|
[2044] | 68 | ('root', 0o600, 'etc/krb5.keytab'), |
---|
| 69 | ('fedora-ds', 0o600, 'etc/dirsrv/keytab') |
---|
[1999] | 70 | ] |
---|
| 71 | |
---|
[2204] | 72 | def drop_caches(): |
---|
[2205] | 73 | with open("/proc/sys/vm/drop_caches", 'w') as f: |
---|
[2204] | 74 | f.write("1") |
---|
| 75 | |
---|
[2044] | 76 | def mkdir_p(path): # it's like mkdir -p |
---|
[1999] | 77 | try: |
---|
| 78 | os.makedirs(path) |
---|
[2044] | 79 | except OSError as e: |
---|
| 80 | if e.errno == errno.EEXIST: |
---|
[1999] | 81 | pass |
---|
| 82 | else: raise |
---|
| 83 | |
---|
[2044] | 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. |
---|
[1999] | 94 | class WithMount(object): |
---|
| 95 | """Context for running code with an extra mountpoint.""" |
---|
| 96 | guest = None |
---|
[2044] | 97 | types = None # comma separated, like the mount argument -t |
---|
[1999] | 98 | mount = None |
---|
| 99 | dev = None |
---|
[2044] | 100 | def __init__(self, guest, types): |
---|
[1999] | 101 | self.guest = guest |
---|
[2044] | 102 | self.types = types |
---|
[1999] | 103 | def __enter__(self): |
---|
[2204] | 104 | drop_caches() |
---|
[1999] | 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: |
---|
[2044] | 115 | shell.call("mount", "--types", self.types, mapper, self.mount) |
---|
[1999] | 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 |
---|
[2044] | 124 | def __exit__(self, _type, _value, _traceback): |
---|
[1999] | 125 | shell.call("umount", self.mount) |
---|
| 126 | os.rmdir(self.mount) |
---|
| 127 | shell.call("kpartx", "-d", self.dev) |
---|
[2204] | 128 | drop_caches() |
---|
[1999] | 129 | |
---|
| 130 | def main(): |
---|
[2044] | 131 | usage = """usage: %prog [push|pull|pull-common] GUEST""" |
---|
[1999] | 132 | |
---|
| 133 | parser = optparse.OptionParser(usage) |
---|
[2044] | 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() |
---|
[1999] | 141 | |
---|
[2044] | 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 |
---|
[1999] | 145 | |
---|
| 146 | os.umask(0o077) # overly restrictive |
---|
| 147 | |
---|
| 148 | if len(args) != 2: |
---|
[2044] | 149 | parser.print_help() |
---|
[1999] | 150 | raise Exception("Wrong number of arguments") |
---|
| 151 | |
---|
| 152 | command = args[0] |
---|
| 153 | guest = args[1] |
---|
| 154 | |
---|
[2044] | 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) |
---|
[1999] | 158 | def push_files(files, type): |
---|
[2044] | 159 | for (usergroup, perms, f) in files: |
---|
[1999] | 160 | dest = "%s/%s" % (tmp_mount, f) |
---|
[2044] | 161 | mkdir_p(os.path.dirname(dest)) # useful for .ssh |
---|
[1999] | 162 | # assuming OK to overwrite |
---|
[2044] | 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 |
---|
[1999] | 172 | def pull_files(files, type): |
---|
| 173 | for (_, _, f) in files: |
---|
[2044] | 174 | dest = "%s/%s/%s" % (options.creds_dir, type, f) |
---|
[1999] | 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() |
---|