mtimeout.1: Use correct dash for number ranges.
[misc] / shadowfix.in
1 #! @PERL@
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@distorted.org.uk>
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 ---
269
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;
337 my @f = split /:/, $line;
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;
347 my @f = split /:/, $line;
348 $#f = 3;
349 $a = { data => [ @f ],
350 members => { hashify(split /,/, $f[3]) },
351 name => $f[0], gid => $f[2] };
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;
361 my @f = split /:/, $line;
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;
378 my @f = split /:/, $line;
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) {
511 my @admins =
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 ----------------------------------------------------