b2ffb9b7 |
1 | #! @PERL@ |
e063712b |
2 | # |
3 | # Sanitise Linux password and group databases |
4 | # |
5 | # (c) 1998 Mark Wooding |
6 | # |
7 | |
8 | use MdwOpt; |
9 | use FileHandle; |
10 | use POSIX; |
11 | |
12 | #----- Documentation -------------------------------------------------------- |
13 | |
14 | =head1 NAME |
15 | |
16 | shadowfix - fix password and group files |
17 | |
18 | =head1 SYNOPSIS |
19 | |
20 | B<shadowfix> I<options>... |
21 | |
22 | =head1 DESCRIPTION |
23 | |
24 | Shadowfix trundles through your various password files and makes sure |
25 | that they're consistent with themselves. |
26 | |
27 | Currently, the checks Shadowfix makes, and their resolutions, are: |
28 | |
29 | =over 4 |
30 | |
31 | =item * |
32 | |
33 | Every user named in the password file should have a shadow password entry; |
34 | create a shadow password entry if necessary. |
35 | |
36 | =item * |
37 | |
38 | Every password field in the password file indicates only presence or absence |
39 | of a password; move a realistic-looking password to the shadow password file, |
40 | and ensure that the password entry in the password file is either empty |
41 | (signifying no password) or an `x' character (signifying a password). |
42 | |
43 | =item * |
44 | |
45 | The primary group of each user exists; warn about nonexistent primary groups. |
46 | |
47 | =item * |
48 | |
49 | Every user is a member of his primary group; add the user to the membership |
50 | list of the group where necessary. |
51 | |
52 | =item * |
53 | |
54 | There are no entries in the shadow password file which don't match entries in |
55 | the main password file; delete orphaned shadow password entries. |
56 | |
57 | =item * |
58 | |
59 | Check group and shadow group files for consistency, as for password and |
60 | shadow password files: every group entry has a shadow entry, no passwords in |
61 | the group file, no orphaned shadow group entries. |
62 | |
63 | =item * |
64 | |
65 | The lists of group members are consistent between group and shadow group |
66 | files; edit the shadow group list to match the main group list where |
67 | necessary. |
68 | |
69 | =item * |
70 | |
71 | Group administrators, listed in the shadow group file, are real users; warn |
72 | about nonexistent group administrators. |
73 | |
74 | =back |
75 | |
76 | A lot of the checks above only make sense when shadow password and group |
77 | files are used. When instructed not to create shadow files, Shadowfix will |
78 | perform the password/group consistency checks as described above. Also, if |
79 | given shadow files as input, and told not to create shadow files on output, |
80 | Shadowfix will merge the password information back into the main files. |
81 | Obviously, translating shadowed to non-shadowed files involved information |
82 | loss: in particular, information about password expiry and group |
83 | administration is lost. |
84 | |
85 | It's time to examine the command line options. |
86 | |
87 | =over 4 |
88 | |
89 | =item B<--passwd=>I<file> |
90 | |
91 | Use I<file> as the main password file. |
92 | |
93 | =item B<--group=>I<file> |
94 | |
95 | Use I<file> as the main group file. |
96 | |
97 | =item B<--shadow=>I<file> |
98 | |
99 | Use I<file> as the shadow password file. |
100 | |
101 | =item B<--gshadow=>I<file> |
102 | |
103 | Use I<file> as the shadow group file. |
104 | |
105 | =item B<--in-passwd=>I<file> |
106 | |
107 | Read main password file entries from I<file>. |
108 | |
109 | =item B<--in-group=>I<file> |
110 | |
111 | Read main group file entries from I<file>. |
112 | |
113 | =item B<--in-shadow=>I<file> |
114 | |
115 | Read shadow password file entries from I<file>. |
116 | |
117 | =item B<--in-gshadow=>I<file> |
118 | |
119 | Read shadow group file entries from I<file>. |
120 | |
121 | =item B<--quiet> |
122 | |
123 | Suppresses Shadowfix's informative messages about what it's doing to your |
124 | passwored files. Enabling this option is not recommended. |
125 | |
126 | =back |
127 | |
128 | If the input files aren't specified explitly, Shadowfix defaults to trying to |
129 | read the output files. These default sensibly to the system password and |
130 | shadow files in F</etc>. |
131 | |
132 | Shadowfix knows about locking password files, so C<passwd> and C<vipw> will |
133 | interact with it properly. |
134 | |
135 | =head1 FILES |
136 | |
137 | =over 4 |
138 | |
139 | =item F</etc/passwd>, F</etc/shadow>, F</etc/group>, F</etc/gshadow> |
140 | |
141 | System default password and group files. |
142 | |
143 | =back |
144 | |
145 | =head1 BUGS |
146 | |
147 | Shadowfix doesn't understand how to cope with YP password files. Yellow |
148 | Pages is a security hole; don't use it. |
149 | |
150 | =head1 AUTHOR |
151 | |
152 | Mark Wooding, <mdw@nsict.org> |
153 | |
154 | =cut |
155 | |
156 | #----- Configuration section ------------------------------------------------ |
157 | |
158 | $passwd = "/etc/passwd"; |
159 | $group = "/etc/group"; |
160 | $shadow = "/etc/shadow"; |
161 | $gshadow = "/etc/gshadow"; |
162 | $passwd_in = $shadow_in = undef; |
163 | $group_in = $gshadow_in = undef; |
164 | |
165 | $suyb = 0; |
166 | |
167 | #----- Subroutines ---------------------------------------------------------- |
168 | |
169 | sub hashify { |
170 | map { $_, 1 } @_; |
171 | } |
172 | |
173 | sub moan { |
174 | print STDERR "shadowfix: @_\n"; |
175 | } |
176 | |
177 | sub uidsort { |
178 | if ($a eq $b) { |
179 | return 0; |
180 | } elsif ($a eq "+") { |
181 | return +1; |
182 | } elsif ($b eq "+") { |
183 | return -1; |
184 | } elsif (!exists($ubynam{$a})) { |
185 | return +1; |
186 | } elsif (!exists($ubynam{$b})) { |
187 | return -1; |
188 | } else { |
189 | return $ubynam{$a}{uid} <=> $ubynam{$b}{uid} || $a cmp $b; |
190 | } |
191 | } |
192 | |
193 | sub gidsort { |
194 | if ($a eq $b) { |
195 | return 0; |
196 | } elsif ($a eq "+") { |
197 | return +1; |
198 | } elsif ($b eq "+") { |
199 | return -1; |
200 | } else { |
201 | return $gbynam{$a}{gid} <=> $gbynam{$b}{gid} || $a cmp $b; |
202 | } |
203 | } |
204 | |
205 | sub lockfile { |
206 | my $file = shift; |
207 | my $mode = shift or 0644; |
208 | my $fh = new FileHandle; |
209 | |
210 | $fh->open("${file}.lock", O_WRONLY | O_EXCL | O_CREAT, 0600) or |
211 | die "couldn't obtain lock file ${file}.lock"; |
212 | $fh->print($$); |
213 | $fh->close; |
214 | $fh->open("${file}.edit", O_WRONLY | O_TRUNC | O_CREAT, $mode) or do { |
215 | unlink "${file}.lock"; |
216 | die "open(${file}.edit): $!"; |
217 | }; |
218 | return $fh; |
219 | } |
220 | |
221 | sub unlockfile { |
222 | my $file = shift; |
223 | my $fh = shift; |
224 | $fh->close; |
225 | |
226 | # --- See whether the file changed --- |
227 | |
228 | CHECK: { |
229 | my ($ofh, $nfh); |
230 | my ($obuf, $nbuf); |
231 | my ($osz, $nsz); |
232 | |
233 | # --- Open the old and new versions for reading --- |
234 | |
235 | $ofh = new FileHandle $file, O_RDONLY; |
236 | $nfh = new FileHandle "${file}.edit", O_RDONLY; |
237 | last CHECK if !$ofh || !$nfh; |
238 | |
239 | # --- Read blocks from each and compare --- |
240 | |
241 | BLOCK: for (;;) { |
242 | $osz = sysread($ofh, $obuf, 4096); |
243 | $nsz = sysread($nfh, $nbuf, 4096); |
244 | last CHECK if !defined($osz) || !defined($nsz); |
245 | last CHECK if $obuf ne $nbuf; |
246 | last BLOCK if $sz == 0; |
247 | } |
248 | |
249 | # --- The files are identical --- |
250 | |
251 | # moan "file $file is unchanged"; |
252 | unlink("${file}.edit"); |
253 | unlink("${file}.lock"); |
254 | return; |
255 | } |
256 | |
257 | # --- Find the current owner --- |
258 | |
259 | # system("diff -u $file $file.edit"); |
260 | |
261 | if (-e $file) { |
262 | my ($mode, $uid, $gid); |
263 | (undef, undef, $mode, undef, $uid, $gid) = stat $file; |
264 | chmod $mode, "${file}.edit"; |
265 | chown $uid, $gid, "${file}.edit"; |
266 | } |
267 | |
268 | # --- Move the old file out of the way --- |
841e5aca |
269 | |
e063712b |
270 | !-e $file or rename("${file}", "${file}-") or do { |
271 | unlink "${file}.lock"; |
272 | unlink "${file}.edit"; |
273 | die "couldn't save backup copy of $file: $!"; |
274 | }; |
275 | |
276 | # --- Move the new one into place --- |
277 | |
278 | rename ("${file}.edit", "${file}") or do { |
279 | rename("${file}-", "${file}"); # This shouldn't happen! |
280 | unlink "${file}.lock"; |
281 | unlink "${file}.edit"; |
282 | die "HELP!!! couldn't save backup copy of $file: $!"; |
283 | }; |
284 | |
285 | # --- Release the lock --- |
286 | |
287 | moan "updated $file" |
288 | unless $suyb; |
289 | unlink("${file}.lock"); |
290 | } |
291 | |
292 | #----- Main code ------------------------------------------------------------ |
293 | |
294 | # --- Options parsing --- |
295 | |
296 | $longopts = { 'passwd' => { return => 'p', arg => 'opt' }, |
297 | 'in-passwd' => { return => 'ip', arg => 'opt' }, |
298 | 'shadow' => { return => 'ps', arg => 'opt' }, |
299 | 'in-shadow' => { return => 'ips', arg => 'opt' }, |
300 | 'group' => { return => 'g', arg => 'opt' }, |
301 | 'in-group' => { return => 'ig', arg => 'opt' }, |
302 | 'gshadow' => { return => 'gs', arg => 'opt' }, |
303 | 'in-gshadow' => { return => 'igs', arg => 'opt' }, |
304 | 'quiet' => { return => 'q', negate => 1 } }; |
305 | |
306 | $opts = MdwOpt->new("", $longopts, \@ARGV, ['negate', 'noshort']); |
307 | |
308 | OPT: while (($opt, $arg) = $opts->read, $opt) { |
309 | $passwd = $arg, next OPT if $opt eq 'p'; |
310 | $passwd_in = $arg, next OPT if $opt eq 'ip'; |
311 | $shadow = $arg, next OPT if $opt eq 'ps'; |
312 | $shadow_in = $arg, next OPT if $opt eq 'ips'; |
313 | $group = $arg, next OPT if $opt eq 'g'; |
314 | $group_in = $arg, next OPT if $opt eq 'ig'; |
315 | $gshadow = $arg, next OPT if $opt eq 'gs'; |
316 | $gshadow_in = $arg, next OPT if $opt eq 'igs'; |
317 | $suyb = 1, next OPT if $opt eq 'q'; |
318 | $suyb = 0, next OPT if $opt eq 'q+'; |
319 | die "bad option"; |
320 | } |
321 | |
322 | $passwd_in = $passwd unless $passwd_in; |
323 | $shadow_in = $shadow unless $shadow_in; |
324 | $group_in = $group unless $group_in; |
325 | $gshadow_in = $gshadow unless $gshadow_in; |
326 | |
327 | # --- Initialise the user tables --- |
328 | |
329 | %ubynam = %ubyuid = %subynam = (); |
330 | %gbynam = %gbygid = %sgbynam = (); |
331 | |
332 | # --- Slurp the user tables into memory --- |
333 | |
334 | $pw = new FileHandle $passwd_in, O_RDONLY or die "open($passwd_in): $!"; |
335 | while ($line = $pw->getline) { |
336 | chomp $line; |
dc56f242 |
337 | my @f = split /:/, $line; |
e063712b |
338 | $#f = 6; |
339 | $a = { data => [ @f ], name => $f[0], uid => $f[2], gid => $f[3] }; |
340 | $ubynam{$a->{name}} = $ubyuid{$a->{uid}} = $a; |
341 | } |
342 | $pw->close; |
343 | |
344 | $gr = new FileHandle $group_in, O_RDONLY or die "open($group_in): $!"; |
345 | while ($line = $gr->getline) { |
346 | chomp $line; |
dc56f242 |
347 | my @f = split /:/, $line; |
e063712b |
348 | $#f = 3; |
349 | $a = { data => [ @f ], |
350 | members => { hashify(split /,/, $f[3]) }, |
841e5aca |
351 | name => $f[0], gid => $f[2] }; |
e063712b |
352 | $gbynam{$a->{name}} = $gbygid{$a->{gid}} = $a; |
353 | } |
354 | $gr->close; |
355 | |
356 | undef $have_shadow; |
357 | if ($shadow_in) { |
358 | if ($spw = new FileHandle $shadow_in, O_RDONLY) { |
359 | while ($line = $spw->getline) { |
360 | chomp $line; |
dc56f242 |
361 | my @f = split /:/, $line; |
e063712b |
362 | $#f = 8; |
363 | $a = { data => [ @f ], name => $f[0] }; |
364 | $subynam{$a->{name}} = $a; |
365 | } |
366 | $spw->close; |
367 | $have_shadow = 1; |
368 | } else { |
369 | die "open($shadow_in): $!" unless $! == ENOENT; |
370 | } |
371 | } |
372 | |
373 | undef $have_gshadow; |
374 | if ($gshadow_in) { |
375 | if ($sgr = new FileHandle $gshadow_in, O_RDONLY) { |
376 | while ($line = $sgr->getline) { |
377 | chomp $line; |
dc56f242 |
378 | my @f = split /:/, $line; |
e063712b |
379 | $#f = 3; |
380 | $a = { data => [ @f ], |
381 | members => { hashify (split /,/, $f[3]) }, |
382 | name => $f[0] }; |
383 | $sgbynam{$a->{name}} = $a; |
384 | } |
385 | $sgr->close; |
386 | $have_gshadow = 1; |
387 | } else { |
388 | die "open($gshadow_in): $!" unless $! == ENOENT; |
389 | } |
390 | } |
391 | |
392 | # --- Check primary group memberships --- |
393 | |
394 | for $u (values %ubynam) { |
395 | $unam = $u->{name}; |
396 | if (exists $gbygid{$u->{gid}}) { |
397 | $g = $gbygid{$u->{gid}}; |
398 | unless ($unam eq "+" || exists($g->{members}{$unam})) { |
399 | moan "user $unam is not a member of his/her primary group" |
400 | unless $suyb; |
401 | $g->{members}{$unam} = 1; |
402 | } |
403 | } else { |
404 | moan "user $unam seems to belong to a nonexistant group" |
405 | unless $suyb; |
406 | } |
407 | } |
408 | |
409 | # --- Shadow password checks --- |
410 | |
411 | if ($shadow) { |
412 | |
413 | # --- Full shadowing checks --- |
414 | |
415 | for $u (values %ubynam) { |
416 | $unam = $u->{name}; |
417 | |
418 | # --- Ensure there's a shadow password entry --- |
419 | |
420 | unless ($unam eq "+" || exists($subynam{$unam})) { |
421 | moan "user $unam not in shadow password file: adding" |
422 | unless $suyb; |
423 | $subynam{$unam} = { name => $unam, |
424 | data => [$unam, |
425 | $u->{data}[1], |
426 | 10205, 0, 99999, 7, "", "", ""] }; |
427 | } |
428 | |
429 | # --- Mark unloginable shadow password entries --- |
430 | |
431 | $su = $subynam{$unam}; |
432 | $p = $su->{data}[1]; |
433 | if ($p ne "*" && length($p) > 0 && length($p) < 5) { |
434 | moan "blanked user ${unam}'s password" |
435 | unless $suyb; |
436 | $su->{data}[1] = "*"; |
437 | } |
438 | |
439 | # --- Blank out normal password entries --- |
440 | |
441 | if ($unam eq "+") { |
442 | # Nothing doing |
443 | } elsif ($p eq "") { |
444 | $u->{data}[1] = ""; |
445 | } else { |
446 | $u->{data}[1] = "x"; |
447 | } |
448 | } |
449 | |
450 | # --- Remove shadow entries which don't make sense any more --- |
451 | |
452 | for $su (values %subynam) { |
453 | $unam = $su->{name}; |
454 | unless (exists($ubynam{$unam})) { |
455 | moan "user $unam only in shadow password file: deleting" |
456 | unless $suyb; |
457 | delete $subynam{$su->{name}}; |
458 | } |
459 | } |
460 | |
461 | } elsif ($have_shadow) { |
462 | |
463 | # --- We have shadowing, but aren't writing out entries --- |
464 | |
465 | for $u (values %ubynam) { |
466 | $unam = $u->{name}; |
467 | $u->{data}[1] = $subynam{$unam}{data}[1] |
468 | if exists($subynam{$unam}); |
469 | } |
470 | } |
471 | |
472 | # --- Shadow group checks --- |
473 | |
474 | for $g (values %gbynam) { |
475 | $gnam = $g->{name}; |
476 | |
477 | # --- Ensure there's a shadow group entry --- |
478 | |
479 | unless (!$gshadow || $gnam eq "+" || exists($sgbynam{$gnam})) { |
480 | moan "group $gnam not in shadow group file: adding" |
481 | unless $suyb; |
482 | $sgbynam{$gnam} = { name => $gnam, |
483 | data => [$gnam, |
484 | $g->{data}[1], |
485 | "", |
486 | $g->{data}[3]], |
487 | members => { %{$g->{members}} } }; |
488 | } |
489 | |
490 | # --- Play games with passwords --- |
491 | |
492 | if ($gshadow) { |
493 | |
494 | # --- Mark unloginable shadow group entries --- |
495 | |
496 | $sg = $sgbynam{$gnam}; |
497 | $p = $sg->{data}[1]; |
498 | if ($p ne "*" && length($p) > 0 && length($p) < 5) { |
499 | moan "blanked group ${gnam}'s password" |
500 | unless $suyb; |
501 | $sg->{data}[1] = "*"; |
502 | } |
503 | |
504 | # --- Blank out normal passwords --- |
505 | |
506 | $g->{data}[1] = "x" unless $gnam eq "+"; |
507 | |
508 | # --- Check that the group's administrators exist --- |
509 | |
510 | if ($sg->{data}[2] ne "" && !$suyb) { |
841e5aca |
511 | my @admins = |
e063712b |
512 | my $admin; |
513 | foreach $admin (split(/,/, $sg->{data}[2])) { |
514 | exists $ubynam{$admin} or |
515 | moan "user $admin owns group $gnam but doesn't seem to exist"; |
516 | } |
517 | } |
518 | |
519 | } elsif ($have_gshadow) { |
520 | $g->{data}[1] = $sgbynam{$gnam}{data}[1] |
521 | if exists($sgbynam{$gnam}); |
522 | $sg = undef; |
523 | } |
524 | |
525 | # --- The group members should be consistent across both files --- |
526 | |
527 | for $i (keys %{$g->{members}}) { |
528 | exists $ubynam{$i} or $suyb or |
529 | moan "user $i is a member of group $gnam but doesn't seem to exist"; |
530 | unless (!$sg || exists($sg->{members}{$i})) { |
531 | moan "group $gnam does not include $i in shadow group file: adding" |
532 | unless $suyb; |
533 | $sg->{members}{$i} = 1; |
534 | } |
535 | } |
536 | if ($sg) { |
537 | for $i (keys %{$sg->{members}}) { |
538 | unless (exists($g->{members}{$i})) { |
539 | moan "group $gnam does not include $i in main group file: deleting" |
540 | unless $suyb; |
541 | delete $sg->{members}{$i}; |
542 | } |
543 | } |
544 | } |
545 | } |
546 | |
547 | # --- Remove entries which are only in the shadow file --- |
548 | |
549 | if ($gshadow) { |
550 | for $sg (values %sgbynam) { |
551 | $gnam = $sg->{name}; |
552 | unless (exists($gbynam{$gnam})) { |
553 | moan "group $gnam only in shadow group file: deleting" |
554 | unless $suyb; |
555 | delete $sgbynam{$gnam}; |
556 | } |
557 | } |
558 | } |
559 | |
560 | # --- Fix up the data blocks --- |
561 | |
562 | for $g (values %gbynam) { |
563 | $g->{data}[3] = join(",", sort uidsort keys %{$g->{members}}); |
564 | } |
565 | |
566 | if ($gshadow) { |
567 | for $sg (values %sgbynam) { |
568 | $sg->{data}[3] = join(",", sort uidsort keys %{$sg->{members}}); |
569 | } |
570 | } |
571 | |
572 | # --- Output the finished work of art --- |
573 | |
574 | $pw = lockfile($passwd, 0644); |
575 | for $unam (sort uidsort keys %ubynam) { |
576 | $pw->print(join(":", @{$ubynam{$unam}{data}}), "\n"); |
577 | } |
578 | unlockfile($passwd, $pw); |
579 | |
580 | if ($shadow) { |
581 | $spw = lockfile($shadow, 0640); |
582 | for $unam (sort uidsort keys %subynam) { |
583 | $spw->print(join(":", @{$subynam{$unam}{data}}), "\n"); |
584 | } |
585 | unlockfile($shadow, $spw); |
586 | } |
587 | |
588 | $gr = lockfile($group, 0644); |
589 | for $gnam (sort gidsort keys %gbynam) { |
590 | $gr->print(join(":", @{$gbynam{$gnam}{data}}), "\n"); |
591 | } |
592 | unlockfile($group, $gr); |
593 | |
594 | if ($gshadow) { |
595 | $sgr = lockfile($gshadow, 0640); |
596 | for $gnam (sort gidsort keys %sgbynam) { |
597 | $sgr->print(join(":", @{$sgbynam{$gnam}{data}}), "\n"); |
598 | } |
599 | unlockfile($gshadow, $sgr); |
600 | } |
601 | |
602 | #----- More subroutines ----------------------------------------------------- |
603 | |
604 | sub udump { |
605 | my $u = shift; |
606 | printf "name = %s\n", $u->{name}; |
607 | printf "uid = %d, gid = %d\n", $u->{uid}, $u->{gid}; |
608 | printf "data = %s\n", join(":", @{$u->{data}}); |
609 | print "\n"; |
610 | } |
611 | |
612 | sub sudump { |
613 | my $u = shift; |
614 | printf "name = %s\n", $u->{name}; |
615 | printf "data = %s\n", join(":", @{$u->{data}}); |
616 | print "\n"; |
617 | } |
618 | |
619 | sub gdump { |
620 | my $g = shift; |
621 | printf "name = %s\n", $g->{name}; |
622 | printf "gid = %d\n", $g->{gid}; |
623 | printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}}); |
624 | printf "data = %s\n", join(":", @{$g->{data}}); |
625 | print "\n"; |
626 | } |
627 | |
628 | sub sgdump { |
629 | my $g = shift; |
630 | printf "name = %s\n", $g->{name}; |
631 | printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}}); |
632 | printf "data = %s\n", join(":", @{$g->{data}}); |
633 | print "\n"; |
634 | } |
635 | |
636 | #----- That's all, folks ---------------------------------------------------- |