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 | PROD_GUESTS = frozenset([ |
---|
14 | 'bees-knees', |
---|
15 | 'cats-whiskers', |
---|
16 | 'busy-beaver', |
---|
17 | 'pancake-bunny', |
---|
18 | 'whole-enchilada', |
---|
19 | 'real-mccoy', |
---|
20 | 'old-faithful', |
---|
21 | 'better-mousetrap', |
---|
22 | 'shining-armor', |
---|
23 | 'golden-egg', |
---|
24 | 'miracle-cure', |
---|
25 | 'lucky-star', |
---|
26 | ]) |
---|
27 | WIZARD_GUESTS = frozenset([ |
---|
28 | 'not-backward', |
---|
29 | ]) |
---|
30 | |
---|
31 | COMMON_CREDS = {} |
---|
32 | |
---|
33 | # Format here assumes that we always chmod $USER:$USER, |
---|
34 | # but note the latter refers to group... |
---|
35 | # |
---|
36 | # Important: no leading slashes! |
---|
37 | COMMON_CREDS['all'] = [ |
---|
38 | ('root', 0o600, 'root/.bashrc'), |
---|
39 | ('root', 0o600, 'root/.screenrc'), |
---|
40 | ('root', 0o600, 'root/.ssh/authorized_keys'), |
---|
41 | ('root', 0o600, 'root/.ssh/authorized_keys2'), |
---|
42 | ('root', 0o600, 'root/.vimrc'), |
---|
43 | ('root', 0o600, 'root/.k5login'), |
---|
44 | ] |
---|
45 | |
---|
46 | COMMON_CREDS['prod'] = [ |
---|
47 | ('root', 0o600, 'root/.ldapvirc'), |
---|
48 | ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'), |
---|
49 | ('root', 0o600, 'etc/ssh/ssh_host_key'), |
---|
50 | ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'), |
---|
51 | ('root', 0o600, 'etc/pki/tls/private/scripts-1024.key'), |
---|
52 | ('root', 0o600, 'etc/pki/tls/private/scripts.key'), |
---|
53 | ('root', 0o600, 'etc/whoisd-password'), |
---|
54 | ('afsagent', 0o600, 'etc/daemon.keytab'), |
---|
55 | |
---|
56 | ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'), |
---|
57 | ('root', 0o644, 'etc/ssh/ssh_host_key.pub'), |
---|
58 | ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'), |
---|
59 | |
---|
60 | ('sql', 0o600, 'etc/sql-mit-edu.cfg.php'), # technically doesn't have to be secret anymore |
---|
61 | ('sql', 0o600, 'etc/sql-password'), |
---|
62 | ('signup', 0o600, 'etc/signup-ldap-pw'), |
---|
63 | ('logview', 0o600, 'home/logview/.k5login'), # XXX user must be created in Kickstart |
---|
64 | ] |
---|
65 | |
---|
66 | # note that these are duplicates with 'prod', but the difference |
---|
67 | # is that the files DIFFER between wizard and prod |
---|
68 | COMMON_CREDS['wizard'] = [ |
---|
69 | ('root', 0o600, 'etc/ssh/ssh_host_dsa_key'), |
---|
70 | ('root', 0o600, 'etc/ssh/ssh_host_key'), |
---|
71 | ('root', 0o600, 'etc/ssh/ssh_host_rsa_key'), |
---|
72 | ('afsagent', 0o600, 'etc/daemon.keytab'), |
---|
73 | |
---|
74 | ('root', 0o644, 'etc/ssh/ssh_host_dsa_key.pub'), |
---|
75 | ('root', 0o644, 'etc/ssh/ssh_host_key.pub'), |
---|
76 | ('root', 0o644, 'etc/ssh/ssh_host_rsa_key.pub'), |
---|
77 | ] |
---|
78 | |
---|
79 | MACHINE_CREDS = {} |
---|
80 | |
---|
81 | MACHINE_CREDS['all'] = [ |
---|
82 | # XXX NEED TO CHECK THAT THE CONTENTS ARE SENSIBLE |
---|
83 | ('root', 0o600, 'etc/krb5.keytab'), |
---|
84 | ] |
---|
85 | |
---|
86 | MACHINE_CREDS['prod'] = [ |
---|
87 | ('fedora-ds', 0o600, 'etc/dirsrv/keytab'), |
---|
88 | ] |
---|
89 | |
---|
90 | MACHINE_CREDS['wizard'] = [] |
---|
91 | |
---|
92 | # Works for passwd and group, but be careful! They're different things! |
---|
93 | def lookup(filename): |
---|
94 | # Super-safe to assume and volume IDs (expensive to check) |
---|
95 | r = { |
---|
96 | 'root': 0, |
---|
97 | 'sql': 537704221, |
---|
98 | } |
---|
99 | with open(filename, 'rb') as f: |
---|
100 | reader = csv.reader(f, delimiter=':', quoting=csv.QUOTE_NONE) |
---|
101 | for row in reader: |
---|
102 | r[row[0]] = int(row[2]) |
---|
103 | return r |
---|
104 | |
---|
105 | def drop_caches(): |
---|
106 | with open("/proc/sys/vm/drop_caches", 'w') as f: |
---|
107 | f.write("1") |
---|
108 | |
---|
109 | def mkdir_p(path): # it's like mkdir -p |
---|
110 | try: |
---|
111 | os.makedirs(path) |
---|
112 | except OSError as e: |
---|
113 | if e.errno == errno.EEXIST: |
---|
114 | pass |
---|
115 | else: raise |
---|
116 | |
---|
117 | # XXX This code is kind of dangerous, because we are directly using the |
---|
118 | # kernel modules to manipulate possibly untrusted disk images. This |
---|
119 | # means that if an attacker can corrupt the disk, and exploit a problem |
---|
120 | # in the kernel vfs driver, he can escalate a guest root exploit |
---|
121 | # to a host root exploit. Ultimately we should use libguestfs |
---|
122 | # which makes this attack harder to pull off, but at the time of writing |
---|
123 | # squeeze didn't package libguestfs. |
---|
124 | # |
---|
125 | # We try to minimize attack surface by explicitly specifying the |
---|
126 | # expected filesystem type. |
---|
127 | class WithMount(object): |
---|
128 | """Context for running code with an extra mountpoint.""" |
---|
129 | guest = None |
---|
130 | types = None # comma separated, like the mount argument -t |
---|
131 | mount = None |
---|
132 | dev = None |
---|
133 | def __init__(self, guest, types): |
---|
134 | self.guest = guest |
---|
135 | self.types = types |
---|
136 | def __enter__(self): |
---|
137 | drop_caches() |
---|
138 | self.dev = "/dev/%s/%s-root" % (HOST, self.guest) |
---|
139 | |
---|
140 | mapper_name = shell.eval("kpartx", "-l", self.dev).split()[0] |
---|
141 | shell.call("kpartx", "-a", self.dev) |
---|
142 | mapper = "/dev/mapper/%s" % mapper_name |
---|
143 | |
---|
144 | # this is why bracketing functions and hanging lambdas are a good idea |
---|
145 | try: |
---|
146 | self.mount = tempfile.mkdtemp("-%s" % self.guest, 'vm-', '/mnt') # no trailing slash |
---|
147 | try: |
---|
148 | shell.call("mount", "--types", self.types, mapper, self.mount) |
---|
149 | except: |
---|
150 | os.rmdir(self.mount) |
---|
151 | raise |
---|
152 | except: |
---|
153 | shell.call("kpartx", "-d", self.dev) |
---|
154 | raise |
---|
155 | |
---|
156 | return self.mount |
---|
157 | def __exit__(self, _type, _value, _traceback): |
---|
158 | shell.call("umount", self.mount) |
---|
159 | os.rmdir(self.mount) |
---|
160 | shell.call("kpartx", "-d", self.dev) |
---|
161 | drop_caches() |
---|
162 | |
---|
163 | def main(): |
---|
164 | usage = """usage: %prog [push|pull] [common|machine] GUEST""" |
---|
165 | |
---|
166 | parser = optparse.OptionParser(usage) |
---|
167 | # ext3 will probably supported for a while yet and a pretty |
---|
168 | # reasonable thing to always try |
---|
169 | parser.add_option('-t', '--types', dest="types", default="ext4,ext3", |
---|
170 | help="filesystem type(s)") # same arg as 'mount' |
---|
171 | parser.add_option('--creds-dir', dest="creds_dir", default="/root/creds", |
---|
172 | help="directory to store/fetch credentials in") |
---|
173 | options, args = parser.parse_args() |
---|
174 | |
---|
175 | if not os.path.isdir(options.creds_dir): |
---|
176 | raise Exception("%s does not exist" % options.creds_dir) |
---|
177 | # XXX check owned by root and appropriately chmodded |
---|
178 | |
---|
179 | os.umask(0o077) # overly restrictive |
---|
180 | |
---|
181 | if len(args) != 3: |
---|
182 | parser.print_help() |
---|
183 | raise Exception("Wrong number of arguments") |
---|
184 | |
---|
185 | command = args[0] |
---|
186 | files = args[1] |
---|
187 | guest = args[2] |
---|
188 | |
---|
189 | if guest in PROD_GUESTS: |
---|
190 | mode = 'prod' |
---|
191 | elif guest in WIZARD_GUESTS: |
---|
192 | mode = 'wizard' |
---|
193 | else: |
---|
194 | raise Exception("Unrecognized guest %s" % guest) |
---|
195 | |
---|
196 | with WithMount(guest, options.types) as tmp_mount: |
---|
197 | uid_lookup = lookup("%s/etc/passwd" % tmp_mount) |
---|
198 | gid_lookup = lookup("%s/etc/group" % tmp_mount) |
---|
199 | def push_files(files, type): |
---|
200 | for (usergroup, perms, f) in files: |
---|
201 | dest = "%s/%s" % (tmp_mount, f) |
---|
202 | mkdir_p(os.path.dirname(dest)) # useful for .ssh |
---|
203 | # assuming OK to overwrite |
---|
204 | # XXX we could compare the files before doing anything... |
---|
205 | shutil.copyfile("%s/%s/%s" % (options.creds_dir, type, f), dest) |
---|
206 | try: |
---|
207 | os.chown(dest, uid_lookup[usergroup], gid_lookup[usergroup]) |
---|
208 | os.chmod(dest, perms) |
---|
209 | except: |
---|
210 | # never ever leave un-chowned files lying around |
---|
211 | os.unlink(dest) |
---|
212 | raise |
---|
213 | def pull_files(files, type): |
---|
214 | for (_, _, f) in files: |
---|
215 | dest = "%s/%s/%s" % (options.creds_dir, type, f) |
---|
216 | mkdir_p(os.path.dirname(dest)) |
---|
217 | # error if doesn't exist |
---|
218 | shutil.copyfile("%s/%s" % (tmp_mount, f), dest) |
---|
219 | |
---|
220 | # XXX ideally we should check these *before* we mount, but Python |
---|
221 | # makes that pretty annoying to do |
---|
222 | if command == "push": |
---|
223 | run = push_files |
---|
224 | elif command == "pull": |
---|
225 | run = pull_files |
---|
226 | else: |
---|
227 | raise Exception("Unknown command %s, valid values are 'push' and 'pull'" % command) |
---|
228 | |
---|
229 | if files == 'common': |
---|
230 | run(COMMON_CREDS['all'], 'all') |
---|
231 | run(COMMON_CREDS[mode], mode) |
---|
232 | elif files == 'machine': |
---|
233 | run(MACHINE_CREDS['all'], 'machine/%s' % guest) |
---|
234 | run(MACHINE_CREDS[mode], 'machine/%s' % guest) |
---|
235 | else: |
---|
236 | raise Exception("Unknown file set %s, valid values are 'common' and 'machine'" % files) |
---|
237 | |
---|
238 | if __name__ == "__main__": |
---|
239 | main() |
---|