3 # Sanitise Linux password and group databases
5 # (c) 1998 Mark Wooding
12 #----- Documentation --------------------------------------------------------
16 shadowfix - fix password and group files
20 B<shadowfix> I<options>...
24 Shadowfix trundles through your various password files and makes sure
25 that they're consistent with themselves.
27 Currently, the checks Shadowfix makes, and their resolutions, are:
33 Every user named in the password file should have a shadow password entry;
34 create a shadow password entry if necessary.
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).
45 The primary group of each user exists; warn about nonexistent primary groups.
49 Every user is a member of his primary group; add the user to the membership
50 list of the group where necessary.
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.
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.
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
71 Group administrators, listed in the shadow group file, are real users; warn
72 about nonexistent group administrators.
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.
85 It's time to examine the command line options.
89 =item B<--passwd=>I<file>
91 Use I<file> as the main password file.
93 =item B<--group=>I<file>
95 Use I<file> as the main group file.
97 =item B<--shadow=>I<file>
99 Use I<file> as the shadow password file.
101 =item B<--gshadow=>I<file>
103 Use I<file> as the shadow group file.
105 =item B<--in-passwd=>I<file>
107 Read main password file entries from I<file>.
109 =item B<--in-group=>I<file>
111 Read main group file entries from I<file>.
113 =item B<--in-shadow=>I<file>
115 Read shadow password file entries from I<file>.
117 =item B<--in-gshadow=>I<file>
119 Read shadow group file entries from I<file>.
123 Suppresses Shadowfix's informative messages about what it's doing to your
124 passwored files. Enabling this option is not recommended.
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>.
132 Shadowfix knows about locking password files, so C<passwd> and C<vipw> will
133 interact with it properly.
139 =item F</etc/passwd>, F</etc/shadow>, F</etc/group>, F</etc/gshadow>
141 System default password and group files.
147 Shadowfix doesn't understand how to cope with YP password files. Yellow
148 Pages is a security hole; don't use it.
152 Mark Wooding, <mdw@nsict.org>
156 #----- Configuration section ------------------------------------------------
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;
167 #----- Subroutines ----------------------------------------------------------
174 print STDERR "shadowfix: @_\n";
180 } elsif ($a eq "+") {
182 } elsif ($b eq "+") {
184 } elsif (!exists($ubynam{$a})) {
186 } elsif (!exists($ubynam{$b})) {
189 return $ubynam{$a}{uid} <=> $ubynam{$b}{uid} || $a cmp $b;
196 } elsif ($a eq "+") {
198 } elsif ($b eq "+") {
201 return $gbynam{$a}{gid} <=> $gbynam{$b}{gid} || $a cmp $b;
207 my $mode = shift or 0644;
208 my $fh = new FileHandle;
210 $fh->open("${file}.lock", O_WRONLY | O_EXCL | O_CREAT, 0600) or
211 die "couldn't obtain lock file ${file}.lock";
214 $fh->open("${file}.edit", O_WRONLY | O_TRUNC | O_CREAT, $mode) or do {
215 unlink "${file}.lock";
216 die "open(${file}.edit): $!";
226 # --- See whether the file changed ---
233 # --- Open the old and new versions for reading ---
235 $ofh = new FileHandle $file, O_RDONLY;
236 $nfh = new FileHandle "${file}.edit", O_RDONLY;
237 last CHECK if !$ofh || !$nfh;
239 # --- Read blocks from each and compare ---
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;
249 # --- The files are identical ---
251 # moan "file $file is unchanged";
252 unlink("${file}.edit");
253 unlink("${file}.lock");
257 # --- Find the current owner ---
259 # system("diff -u $file $file.edit");
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";
268 # --- Move the old file out of the way ---
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: $!";
276 # --- Move the new one into place ---
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: $!";
285 # --- Release the lock ---
289 unlink("${file}.lock");
292 #----- Main code ------------------------------------------------------------
294 # --- Options parsing ---
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 } };
306 $opts = MdwOpt->new("", $longopts, \@ARGV, ['negate', 'noshort']);
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+';
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;
327 # --- Initialise the user tables ---
329 %ubynam = %ubyuid = %subynam = ();
330 %gbynam = %gbygid = %sgbynam = ();
332 # --- Slurp the user tables into memory ---
334 $pw = new FileHandle $passwd_in, O_RDONLY or die "open($passwd_in): $!";
335 while ($line = $pw->getline) {
337 my @f = split /:/, $line;
339 $a = { data => [ @f ], name => $f[0], uid => $f[2], gid => $f[3] };
340 $ubynam{$a->{name}} = $ubyuid{$a->{uid}} = $a;
344 $gr = new FileHandle $group_in, O_RDONLY or die "open($group_in): $!";
345 while ($line = $gr->getline) {
347 my @f = split /:/, $line;
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;
358 if ($spw = new FileHandle $shadow_in, O_RDONLY) {
359 while ($line = $spw->getline) {
361 my @f = split /:/, $line;
363 $a = { data => [ @f ], name => $f[0] };
364 $subynam{$a->{name}} = $a;
369 die "open($shadow_in): $!" unless $! == ENOENT;
375 if ($sgr = new FileHandle $gshadow_in, O_RDONLY) {
376 while ($line = $sgr->getline) {
378 my @f = split /:/, $line;
380 $a = { data => [ @f ],
381 members => { hashify (split /,/, $f[3]) },
383 $sgbynam{$a->{name}} = $a;
388 die "open($gshadow_in): $!" unless $! == ENOENT;
392 # --- Check primary group memberships ---
394 for $u (values %ubynam) {
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"
401 $g->{members}{$unam} = 1;
404 moan "user $unam seems to belong to a nonexistant group"
409 # --- Shadow password checks ---
413 # --- Full shadowing checks ---
415 for $u (values %ubynam) {
418 # --- Ensure there's a shadow password entry ---
420 unless ($unam eq "+" || exists($subynam{$unam})) {
421 moan "user $unam not in shadow password file: adding"
423 $subynam{$unam} = { name => $unam,
426 10205, 0, 99999, 7, "", "", ""] };
429 # --- Mark unloginable shadow password entries ---
431 $su = $subynam{$unam};
433 if ($p ne "*" && length($p) > 0 && length($p) < 5) {
434 moan "blanked user ${unam}'s password"
436 $su->{data}[1] = "*";
439 # --- Blank out normal password entries ---
450 # --- Remove shadow entries which don't make sense any more ---
452 for $su (values %subynam) {
454 unless (exists($ubynam{$unam})) {
455 moan "user $unam only in shadow password file: deleting"
457 delete $subynam{$su->{name}};
461 } elsif ($have_shadow) {
463 # --- We have shadowing, but aren't writing out entries ---
465 for $u (values %ubynam) {
467 $u->{data}[1] = $subynam{$unam}{data}[1]
468 if exists($subynam{$unam});
472 # --- Shadow group checks ---
474 for $g (values %gbynam) {
477 # --- Ensure there's a shadow group entry ---
479 unless (!$gshadow || $gnam eq "+" || exists($sgbynam{$gnam})) {
480 moan "group $gnam not in shadow group file: adding"
482 $sgbynam{$gnam} = { name => $gnam,
487 members => { %{$g->{members}} } };
490 # --- Play games with passwords ---
494 # --- Mark unloginable shadow group entries ---
496 $sg = $sgbynam{$gnam};
498 if ($p ne "*" && length($p) > 0 && length($p) < 5) {
499 moan "blanked group ${gnam}'s password"
501 $sg->{data}[1] = "*";
504 # --- Blank out normal passwords ---
506 $g->{data}[1] = "x" unless $gnam eq "+";
508 # --- Check that the group's administrators exist ---
510 if ($sg->{data}[2] ne "" && !$suyb) {
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";
519 } elsif ($have_gshadow) {
520 $g->{data}[1] = $sgbynam{$gnam}{data}[1]
521 if exists($sgbynam{$gnam});
525 # --- The group members should be consistent across both files ---
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"
533 $sg->{members}{$i} = 1;
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"
541 delete $sg->{members}{$i};
547 # --- Remove entries which are only in the shadow file ---
550 for $sg (values %sgbynam) {
552 unless (exists($gbynam{$gnam})) {
553 moan "group $gnam only in shadow group file: deleting"
555 delete $sgbynam{$gnam};
560 # --- Fix up the data blocks ---
562 for $g (values %gbynam) {
563 $g->{data}[3] = join(",", sort uidsort keys %{$g->{members}});
567 for $sg (values %sgbynam) {
568 $sg->{data}[3] = join(",", sort uidsort keys %{$sg->{members}});
572 # --- Output the finished work of art ---
574 $pw = lockfile($passwd, 0644);
575 for $unam (sort uidsort keys %ubynam) {
576 $pw->print(join(":", @{$ubynam{$unam}{data}}), "\n");
578 unlockfile($passwd, $pw);
581 $spw = lockfile($shadow, 0640);
582 for $unam (sort uidsort keys %subynam) {
583 $spw->print(join(":", @{$subynam{$unam}{data}}), "\n");
585 unlockfile($shadow, $spw);
588 $gr = lockfile($group, 0644);
589 for $gnam (sort gidsort keys %gbynam) {
590 $gr->print(join(":", @{$gbynam{$gnam}{data}}), "\n");
592 unlockfile($group, $gr);
595 $sgr = lockfile($gshadow, 0640);
596 for $gnam (sort gidsort keys %sgbynam) {
597 $sgr->print(join(":", @{$sgbynam{$gnam}{data}}), "\n");
599 unlockfile($gshadow, $sgr);
602 #----- More subroutines -----------------------------------------------------
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}});
614 printf "name = %s\n", $u->{name};
615 printf "data = %s\n", join(":", @{$u->{data}});
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}});
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}});
636 #----- That's all, folks ----------------------------------------------------