#! /usr/bin/env perl # vim: set filetype=perl ts=4 sw=4 sts=4 et: use common::sense; use Data::Dumper; use Term::ANSIColor; use Digest::MD5 (); use File::Basename; my $BASEDIR = dirname(__FILE__) . '/../..'; my $MIN_KEYGROUP_GID = 2000; my $MAX_KEYGROUP_GID = 99999; my @KEY_GROUPS_IGNORE = qw{ keeper reader }; my $HOME_SUBDIRS_IGNORE_RE = qr{^^}; my $bad; # generate a uniq prefix based on caller's lineno and caller's caller's lineno, useful to grep or grep -v sub _prefix { return uc(unpack('H*', pack('S', (caller(1))[2])) . unpack('H*', pack('S', (caller(2))[2]))) . ": "; } sub info { print $_[0] . "\n"; return 1; } ## no critic (RequireArgUnpacking) sub _wrn { $bad++; print colored(_prefix() . $_[0], "blue") . "\n"; return 1; } ## no critic (RequireArgUnpacking,ProhibitUnusedPrivateSubroutine) sub _err { $bad++; print colored(_prefix() . $_[0], "red") . "\n"; return 1; } ## no critic (RequireArgUnpacking) sub _crit { $bad++; print colored(_prefix() . $_[0], "bold red") . "\n"; return 1; } ## no critic (RequireArgUnpacking) # Linux and BSD don't always have the same account names for UID/GID 0 my ($UID0) = (qx{getent passwd 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators) my ($GID0) = (qx{getent group 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators) my $islinux = (($^O =~ /linux/i) ? 1 : 0); my $hasacls = (($^O =~ /linux|freebsd/i) ? 1 : 0); # get all the key* groups my %keygroupsbyname = (); my %aclkgroupsbyname = (); my %gkgroupsbyname = (); my %owgroupsbyname = (); my %keygroupsbyid = (); my %aclkgroupsbyid = (); my %gkgroupsbyid = (); my %owgroupsbyid = (); my $sudoers_dir = '/etc/sudoers.d'; if (!-d $sudoers_dir && -d '/usr/pkg/etc/sudoers.d') { $sudoers_dir = '/usr/pkg/etc/sudoers.d'; } elsif (!-d $sudoers_dir && !$islinux) { $sudoers_dir = '/usr/local/etc/sudoers.d'; } _err "/nonexistent exists" if -e "/nonexistent"; open(my $fh_group, '<', '/etc/group') or die $!; while (<$fh_group>) { /^key([^:]+):[^:]+:(\d+)/ or next; my $name = $1; my $id = $2; if (exists $keygroupsbyname{$name} or exists $gkgroupsbyname{$name} or exists $owgroupsbyname{$name} or exists $aclkgroupsbyname{$name}) { _err "group $name already seen!"; } if ($name =~ /-gatekeeper$/) { $gkgroupsbyname{$name} = {name => $name, id => $id}; } elsif ($name =~ /-aclkeeper$/) { $aclkgroupsbyname{$name} = {name => $name, id => $id}; } elsif ($name =~ /-owner$/) { $owgroupsbyname{$name} = {name => $name, id => $id}; } else { $keygroupsbyname{$name} = {name => $name, id => $id}; } if (exists $keygroupsbyid{$id} or exists $gkgroupsbyid{$id} or exists $owgroupsbyid{$id} or exists $aclkgroupsbyname{$id}) { _crit "group $name 's ID already seen!"; } if ($name =~ /-gatekeeper$/) { $gkgroupsbyid{$id} = {name => $name, id => $id}; } elsif ($name =~ /-aclkeeper$/) { $aclkgroupsbyid{$id} = {name => $name, id => $id}; } elsif ($name =~ /-owner$/) { $owgroupsbyid{$id} = {name => $name, id => $id}; } else { $keygroupsbyid{$id} = {name => $name, id => $id}; } if (grep { $name eq $_ } @KEY_GROUPS_IGNORE) { delete $keygroupsbyname{$name}; delete $keygroupsbyid{$id}; next; } if ($id > $MAX_KEYGROUP_GID) { _err "group $name id $id is too high"; } if ($id < $MIN_KEYGROUP_GID) { _err "group $name id $id is too low"; } } close($fh_group); info "found " . (scalar keys %keygroupsbyname) . " key groups"; # checking if allowkeeper is a member of all keygroups my @allowkeeper_groups = split(/ /, qx/groups allowkeeper/); ## no critic (ProhibitBacktickOperators) chomp @allowkeeper_groups; # some outputs of `groups` include "$username :" as a prefix, strip that if ($allowkeeper_groups[0] eq 'allowkeeper' && $allowkeeper_groups[1] eq ':') { @allowkeeper_groups = splice @allowkeeper_groups, 2; } foreach my $group (keys %keygroupsbyname) { _err "allowkeeper user is not a member of group key$group" if (not grep { $_ eq "key$group" } @allowkeeper_groups); } # now check if each key group has a gk # and vice versa foreach my $group (keys %keygroupsbyname) { next if exists $gkgroupsbyname{$group . "-gatekeeper"}; _err "key group $group is missing a gatekeeper group"; } foreach my $groupori (keys %gkgroupsbyname) { my $group = $groupori; $group =~ s/-gatekeeper$//; next if exists $keygroupsbyname{$group}; _err "gatekeeper group $group is missing a key group"; } foreach my $group (keys %keygroupsbyname) { next if exists $owgroupsbyname{$group . "-owner"}; _err "key group $group is missing an owner group"; } foreach my $groupori (keys %owgroupsbyname) { my $group = $groupori; $group =~ s/-owner$//; next if exists $keygroupsbyname{$group}; _err "owner group $group is missing a key group"; } # now check if each key group has a /home/key* $HOME # and vice versa my @keyhomesfound; opendir(my $dh, "/home/") or die $!; while (my $file = readdir($dh)) { next unless -d "/home/$file"; next if $file eq '.'; next if $file eq '..'; if ($file !~ /[a-zA-Z0-9_.-]+$/) { _err "bad chars in /home/$file"; next; } push @keyhomesfound, $file if $file =~ /^key/; } foreach my $file (@keyhomesfound) { my $file2 = $file; $file2 =~ s/^key//; next if exists $keygroupsbyname{$file2}; next if (grep { $file2 eq $_ } @KEY_GROUPS_IGNORE); _err "directory /home/key$file2 exists but no key group $file2"; } foreach my $group (keys %keygroupsbyname) { next if -d "/home/key$group"; _err "key group $group is missing /home/key$group"; } my %ALL_FILES; foreach (qx{find /home/key* /home/keykeeper /home/allowkeeper -print}) { ## no critic (ProhibitBacktickOperators) chomp; /$HOME_SUBDIRS_IGNORE_RE/ and next; $ALL_FILES{$_} = 1; } while (my $homedir = glob '/home/*') { -d $homedir or next; -d "$homedir/ttyrec" or next; next if $homedir eq '/home/proxyhttp'; next if $homedir eq '/home/healthcheck'; #$ALL_FILES{$_} = 1; #$ALL_FILES{$_.'/ttyrec'} = 1; #$ALL_FILES{$_.'/.ssh'} = 1; #$ALL_FILES{$_.'/osh.log'} = 1; my ($user) = $homedir =~ m{/([^/]+)$}; my $usertty = "$user-tty"; if (not getgrnam($usertty)) { $usertty = substr($user, 0, 5) . '-tty'; } check_file_rights("$homedir", ["# file: $homedir", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:$usertty:--x", "group:osh-auditor:--x", "mask::r-x", "other::---",], "drwxr-x--x", $user, $user); check_file_rights( "$homedir/ttyrec", [ "# file: $homedir/ttyrec", "# owner: $user", "# group: $user", "user::rwx", "group::---", "group:$usertty:r-x", "mask::r-x", "other::---", "default:user::rwx", "default:group::---", "default:group:$usertty:r-x", "default:mask::r-x", "default:other::---", ], "drwxrwxr-x", $user, $user ); check_file_rights("$homedir/.ssh", ["# file: $homedir/.ssh", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:osh-auditor:--x", "mask::r-x", "other::---",], "drwxr-x---", $user, $user); if (-e "$homedir/osh.log") # doesn't exist? nevermind { check_file_rights("$homedir/osh.log", ["# file: $homedir/osh.log", "# owner: $user", "# group: $user", "user::rw-", "group::r--", "other::---",], "-rw-r-----", $user, $user); } # now check all keys in ~/.ssh opendir(my $dh, "$homedir/.ssh") or die "$homedir/.ssh: $!"; while (my $keyfile = readdir($dh)) { next unless $keyfile =~ /^id_|private/; my $ret = check_file_rights( "$homedir/.ssh/$keyfile", [ "# file: $homedir/.ssh/$keyfile", "# owner: $user", "# group: $user", "user::r--", $keyfile =~ /\.pub$/ ? "group::r--" : "group::---", $keyfile =~ /\.pub$/ ? "other::r--" : "other::---", ], $keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--------", $user, $user ); if ($keyfile !~ /\.pub$/) { if (not $ret) { # wow ! private key readable ? _crit "due to above error, private key $homedir/.ssh/$keyfile might be readable !!"; } } else { # check for spurious "from" in .pub open(my $pubfh, '<', "$homedir/.ssh/$keyfile") or die "$homedir/.ssh/$keyfile: $!"; while (<$pubfh>) { /from=/ and _err "spurious from='...' in $homedir/.ssh/$keyfile"; } close($pubfh); } } close($dh); } sub check_file_rights { my $file = shift; my $expectedOutput = shift; my $expectedmodes = shift; my $expectedowner = shift; my $expectedgroup = shift; #info "checking rights of $file"; delete $ALL_FILES{$file}; my $ok = 1; if (not -e $file) { _err "file $file doesn't exist!"; $ok = 0; return $ok; } if (!$hasacls) { my ($modes, $owner, $group) = (qx{ls -ld $file})[0] =~ m{(\S+)\s+\d+\s+(\S+)\s+(\S+)}; ## no critic (ProhibitBacktickOperators) if ($modes ne $expectedmodes) { $ok = 0; _err "on $file got $modes wanted $expectedmodes"; } if ($owner ne $expectedowner) { $ok = 0; _err "on $file got $owner wanted $expectedowner"; } if ($group ne $expectedgroup) { $ok = 0; _err "on $file got $group wanted $expectedgroup"; } return $ok; } my $param = ($islinux ? '-p' : ''); my @out = qx{getfacl $param $file 2>/dev/null}; ## no critic (ProhibitBacktickOperators) chomp @out; my $lineno = -1; $expectedOutput = [sort @$expectedOutput]; @out = grep { /./ } sort @out; foreach my $outLine (@out) { next if not $outLine; $lineno++; $outLine eq $expectedOutput->[$lineno] and next; $ok = 0; _err "rights of $file, line$lineno, expected '" . $expectedOutput->[$lineno] . "' but got '" . $outLine . "'"; } if (@out != @$expectedOutput) { _err "rights of $file, number of lines unexpected (got " . @out . " instead of " . @$expectedOutput . ")"; $ok = 0; } return $ok; } # now check what is in /home/key* and the rights foreach my $file (@keyhomesfound) { delete $ALL_FILES{"/home/$file/.bash_logout"}; delete $ALL_FILES{"/home/$file/.bashrc"}; delete $ALL_FILES{"/home/$file/.profile"}; delete $ALL_FILES{"/home/$file/.ssh"}; delete $ALL_FILES{"/home/$file/.ssh/known_hosts"}; # check rights of /home/keytruc if (-e "/home/$file") { if ($file ne 'keykeeper' and $file ne 'keyreader') { check_file_rights( "/home/$file", [ "# file: /home/$file", "# owner: $file", "# group: $file", "user::rwx", "group::r-x", "group:osh-whoHasAccessTo:--x", "group:osh-auditor:--x", "group:osh-superowner:--x", "group:$file-aclkeeper:--x", "group:$file-gatekeeper:--x", "group:$file-owner:--x", "mask::r-x", "other::---", ], "drwxr-x--x", $file, $file ); } else { check_file_rights( "/home/$file", [ "# file: /home/$file", "# owner: $file", "# group: $file", "user::rwx", "group::r-x", $file eq 'keykeeper' ? "other::r-x" : "other::---", # special dir /home/keykeeper is 755 ], $file eq 'keykeeper' ? "drwxr-xr-x" : "drwxr-x---", $file, $file ); } } else { _err "/home/$file doesn't exist"; } next if (grep { $file eq "key$_" } @KEY_GROUPS_IGNORE); # check rights of /home/keytruc/allowed.ip if (-e "/home/$file/allowed.ip") { #not -s "/home/$file/allowed.ip" and _wrn "group $file has no servers"; check_file_rights("/home/$file/allowed.ip", ["# file: /home/$file/allowed.ip", "# owner: $file", "# group: $file-aclkeeper", "user::rw-", "group::rw-", "other::r--",], "-rw-rw-r--", $file, "$file-aclkeeper"); } else { _err "/home/$file/allowed.ip doesn't exist"; } # check rights of /home/keykeeper/keytruc/ if (-e "/home/keykeeper/$file") { check_file_rights("/home/keykeeper/$file", ["# file: /home/keykeeper/$file", "# owner: keykeeper", "# group: $file", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", "keykeeper", $file); } else { _err "/home/keykeeper/$file doesn't exist"; } # check rights of /home/keykeeper/keytruc/id_* opendir(my $dh, "/home/keykeeper/$file") or die "/home/keykeeper/$file: $!"; while (my $keyfile = readdir($dh)) { next unless $keyfile =~ /^id_/; # spurious files will be reported below my $ret = check_file_rights( "/home/keykeeper/$file/$keyfile", ["# file: /home/keykeeper/$file/$keyfile", "# owner: keykeeper", "# group: $file", "user::r--", "group::r--", $keyfile =~ /\.pub$/ ? "other::r--" : "other::---",], $keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--r-----", "keykeeper", $file ); if ($keyfile !~ /\.pub$/) { if (not $ret) { # wow ! private key readable ? _crit "due to above error, private key /home/keykeeper/$file/$keyfile might be readable !!"; } } else { # check for spurious "from" in .pub open(my $pubfh, '<', "/home/keykeeper/$file/$keyfile") or die "/home/keykeeper/$file/$keyfile: $!"; while (<$pubfh>) { /from=/ and _err "spurious from='...' in /home/keykeeper/$file/$keyfile"; } close($pubfh); } } close($dh); } # check some special dirs check_file_rights("/home/allowkeeper", ["# file: /home/allowkeeper", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", "allowkeeper", "allowkeeper"); check_file_rights("/home/keykeeper", ["# file: /home/keykeeper", "# owner: keykeeper", "# group: keykeeper", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", "keykeeper", "keykeeper"); check_file_rights("/home/logkeeper", ["# file: /home/logkeeper", "# owner: $UID0", "# group: bastion-users", "user::rwx", "group::-wx", "other::---",], "drwx-wx---", $UID0, "bastion-users"); check_file_rights("/home/passkeeper", ["# file: /home/passkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", $UID0, $GID0); check_file_rights("/home/oldkeeper", ["# file: /home/oldkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::---", "other::---",], "drwx------", $UID0, $GID0) if -e "/home/oldkeeper"; # now get all bastion users my %users; my %usersbyid; setpwent(); while (my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $home, $shell, $expire) = getpwent()) { if ($shell =~ /osh.pl$|diverter.sh$/) { if (exists $users{$name}) { _err "duplicate user $name"; } if (exists $usersbyid{$uid}) { _err "duplicate uid for user $name"; } if ($home ne "/home/$name") { _err "bad home for $name: $home"; } if (!-d $home) { _err "home of $name doesn't exist ($home)"; } $users{$name} = {name => $name, uid => $uid, gid => $gid, shell => $shell}; $usersbyid{$uid} = {name => $name, uid => $uid, gid => $gid, shell => $shell}; } # TODO check who has shell access } info "found " . (scalar keys %users) . " bastion users"; my %groups; my %usergroups; setgrent(); while (my ($name, $passwd, $gid, $members) = getgrent()) { $groups{$name} = {name => $name, gid => $gid, members => [split(/ /, $members)]}; foreach my $member (split(/ /, $members)) { push @{$usergroups{$member}}, $name; } } info "found " . (scalar keys %groups) . " groups"; # check that user keyreader is a member of all bastion users primary groups my %keyreaderuserseen; foreach my $group (@{$usergroups{'keyreader'}}) { $keyreaderuserseen{$group} = 1; } foreach my $user (keys %users) { next if (exists $keyreaderuserseen{$user}); _err "user $user primary group doesn't have keyreader as member"; if ($ENV{'FIX_KEYREADER'}) { system("usermod -a -G $user keyreader"); _err "... fixed!"; } } # check if user has /home/allowkeeper/testuser4/allowed.private foreach my $account (keys %users) { check_file_rights("/home/allowkeeper/$account", ["# file: /home/allowkeeper/$account", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", "allowkeeper", "allowkeeper"); check_file_rights( "/home/allowkeeper/$account/allowed.ip", ["# file: /home/allowkeeper/$account/allowed.ip", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",], "-rw-r--r--", "allowkeeper", "allowkeeper" ); check_file_rights( "/home/allowkeeper/$account/allowed.private", ["# file: /home/allowkeeper/$account/allowed.private", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",], "-rw-r--r--", "allowkeeper", "allowkeeper" ); if (!-e "/home/allowkeeper/$account/allowed.private" && $ENV{'FIX_MISSING_PRIVATE_FILES'}) { if (open(my $fh_priv, '>', "/home/allowkeeper/$account/allowed.private")) { close($fh_priv); } chmod 0644, "/home/allowkeeper/$account/allowed.private"; my (undef, undef, $allowkeeperuid, $allowkeepergid) = getpwnam("allowkeeper"); chown $allowkeeperuid, $allowkeepergid, "/home/allowkeeper/$account/allowed.private"; _err "... fixed!"; } # check all allowed.ip.GROUP symlinks my $dh; if (-d "/home/allowkeeper/$account") { opendir($dh, "/home/allowkeeper/$account"); while (my $file = readdir($dh)) { if ($file =~ /^config\.[a-zA-Z0-9_-]+$/) { delete $ALL_FILES{"/home/allowkeeper/$account/$file"}; next; } elsif ($file !~ /^allowed\.(ip|partial)\.([a-zA-Z0-9_-]+)$/) { next; } if (not grep { $2 eq $_ } keys %keygroupsbyname) { _err "file /home/allowkeeper/$account/$file has no corresponding known group"; } if ($1 eq 'ip') { if (not -l "/home/allowkeeper/$account/$file") { _err "file /home/allowkeeper/$account/$file should have been a symlink"; } } elsif ($1 eq 'partial') { if (not -f "/home/allowkeeper/$account/$file") { _err "file /home/allowkeeper/$account/$file should have been a plain file"; } } else { _err "hmm, bug in the script ? got a '$1'"; } delete $ALL_FILES{"/home/allowkeeper/$account/$file"}; } close($dh); } } delete $ALL_FILES{'/home/allowkeeper'}; delete $ALL_FILES{'/home/allowkeeper/.bash_logout'}; delete $ALL_FILES{'/home/allowkeeper/.bashrc'}; delete $ALL_FILES{'/home/allowkeeper/.profile'}; delete $ALL_FILES{'/home/allowkeeper/.ssh'}; delete $ALL_FILES{'/home/allowkeeper/activeLogin.json'}; delete $ALL_FILES{'/home/allowkeeper/expirationGrant.json'}; if (keys %ALL_FILES) { _err "got some potentially unknown files:"; print Dumper(sort keys %ALL_FILES); } # for new code, check sudo stuff sub _tocheck { my $file = shift; my $filesuffix = shift; my $tocheckref = shift; my %tocheck = %$tocheckref; if (exists $tocheck{'NEEDGROUP'}) { my $group = $tocheck{'NEEDGROUP'}[0]; my $gid = getgrnam($group); if (not defined $gid) { _err "missing group $group"; } elsif ($gid > 1000) { _err "group $group has a too high gid ($gid)"; } } my @stat = stat($file); if (exists $tocheck{'FILEMODE'}) { my $mode = sprintf '%04o', $stat[2] & oct(7777); if ($mode ne $tocheck{'FILEMODE'}[0]) { _err "bad file mode on $file, got $mode but expected " . $tocheck{'FILEMODE'}[0]; } } if (exists $tocheck{'FILEOWN'}) { my $uid = $stat[4]; my $gid = $stat[5]; my $wantuser = (split / /, $tocheck{'FILEOWN'}[0])[0]; my $wantgroup = (split / /, $tocheck{'FILEOWN'}[0])[1]; $wantuser = $UID0 if ($wantuser eq 'root' || $wantuser eq '0'); $wantgroup = $GID0 if ($wantgroup eq 'root' || $wantgroup eq '0'); my $wantuid = getpwnam($wantuser); my $wantgid = getgrnam($wantgroup); if ($uid ne $wantuid) { _err "bad owner on file $file (got $uid but wanted $wantuid aka $wantuser)"; } if ($gid ne $wantgid) { _err "bad group on file $file (got $gid but wanted $wantgid aka $wantgroup)"; } } if (exists $tocheck{'SUDOERS'}) { my $sudoersfile = "$sudoers_dir/osh-plugin-" . $filesuffix; if (not -f $sudoersfile) { _err "sudoers file $sudoersfile doesn't exists"; } else { my $mode = sprintf '%04o', (stat($sudoersfile))[2] & oct(7777); if ($mode ne "0440") { _err "sudoers file $sudoersfile has a bad mode $mode"; } if (!open(my $fh_sudoers, '<', $sudoersfile)) { _err "can't open sudoers file $sudoersfile to check"; } else { my @contents = <$fh_sudoers>; close($fh_sudoers); chomp @contents; foreach my $wantedline (@{$tocheck{'SUDOERS'}}) { if (not grep { $_ eq $wantedline } @contents) { _err "missing line in plugin $sudoersfile: $wantedline"; } } } } } if (exists $tocheck{'KEYSUDOERS'}) { my @contents; foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) { if (!open(my $fh_sudoers, '<', $sudoersfile)) { _err "can't open sudoers file template $sudoersfile to check"; } else { my @lines = <$fh_sudoers>; close($fh_sudoers); chomp @lines; push @contents, @lines; } } if (@contents) { foreach my $wantedline (@{$tocheck{'KEYSUDOERS'}}) { $wantedline =~ s'@KEYGROUP@'%GROUP%'g; if (not grep { $_ eq $wantedline } @contents) { _err "missing line in plugin sudoers.group.template: $wantedline"; } } } } foreach my $key (qw{ FILEMODE FILEOWN SUDOERS NEEDGROUP KEYSUDOERS }) { delete $tocheck{$key}; } if (keys %tocheck) { _err "hum sparse tocheck key: " . join(" ", sort keys %tocheck); } return 1; } while (my $file = glob "$BASEDIR/bin/helper/*") { my ($filesuffix) = $file =~ m{/osh-([a-zA-Z0-9_-]+$)}; if (!$filesuffix) { _err "helper file has a strange name ($file)"; next; } my $fh_helper; if (!open($fh_helper, '<', $file)) { _err "can't open helper file $file to check"; next; } my %tochecklocal; while (<$fh_helper>) { /^#/ or last; if (/^\s*#\s*$/) { _tocheck($file, $filesuffix, \%tochecklocal); %tochecklocal = (); next; } /^# ([A-Z0-9]+) (.+)$/ or next; my ($keyword, $line) = ($1, $2); push @{$tochecklocal{$keyword}}, $line; } close($fh_helper); if (%tochecklocal) { _tocheck($file, $filesuffix, \%tochecklocal); } } # check /etc/sudoers.d vs $BASEDIR/etc/sudoers.d # FIXME won't see if we have too many / old files in /etc/sudoers.d while (my $distfile = glob "$BASEDIR/etc/sudoers.d/*") { my $prodfile = $distfile; $prodfile =~ s=^\Q$BASEDIR\E/etc/sudoers.d=$sudoers_dir=; if (-e $prodfile) { my @md5sums; foreach my $file ($prodfile, $distfile) { if (open(my $fh, '<', $file)) { binmode($fh); push @md5sums, Digest::MD5->new->addfile($fh)->hexdigest; close($fh); } else { push @md5sums, "ERR($file)"; } } if ($md5sums[0] ne $md5sums[1]) { _err "sudoers file $distfile and $prodfile differ"; } } else { _err "sudoers file $prodfile not found"; } } if (1) { my @template; foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) { if (!open(my $fh_sudoers, '<', $sudoersfile)) { _err "can't open sudoers file template $sudoersfile to check"; } else { my @lines = <$fh_sudoers>; close($fh_sudoers); chomp @lines; push @template, @lines; } } my %seensudogroupfile; while (my $sudoersfile = glob "$sudoers_dir/osh-group-*") { # TODO check 0440 # TODO check there's a matching group (and the other way around) # the unescaped group should be present in the file (see GROUPNAME below), # but to backwards compatible, try to guess it by ourselves if it's not my $group = $sudoersfile; $group =~ s/^.*osh-group-key//; my $fh_sudoers; if (!open($fh_sudoers, '<', $sudoersfile)) { _err "can't open $sudoersfile file to check: $!"; next; } my @contents = <$fh_sudoers>; close($fh_sudoers); chomp @contents; # now try to get it from the file foreach (@contents) { /^# GROUPNAME=key(.+)/ and $group = $1; } $seensudogroupfile{$group} = 1; my @expected = @template; do { s/%GROUP%/key$group/g; s=%BASEPATH%=/opt/bastion=g; } for @expected; foreach my $wantedline (@expected) { if (not grep { $_ eq $wantedline } @contents) { _err "missing line in $sudoersfile: $wantedline"; } } } foreach my $group (keys %keygroupsbyname) { next if exists $seensudogroupfile{$group}; _err "missing $sudoers_dir/osh-group-key$group file"; } } exit($bad > 255 ? 255 : $bad);