chiark / gitweb /
Stamp out trailing whitespace.
[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@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 ---
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 ----------------------------------------------------