#!/usr/local/bin/perl -nwT # # Copyright Hank Leininger , INIT { $::V = '$Id: yaas,v 1.1.1.1 2005/05/11 00:48:28 hlein Exp $'; $::V =~ s/^.*,v //; $::V =~ s/ .*//; } # Yet Another Auth-before-Something script. # # watch a syslog log for imapd logins, and update /etc/hosts.allow # with the IPs of authenticated users who are allowed to talk to the # whatever other services you wish to wrap (a stunnel'ed smtp listener, # sshd, whatever). # # /etc/hosts.* files are hopefully treated with sufficient paranoia. # On error, the original files should be left intact. # Errors writing the files are considered nonfatal--keep going through # the main loop and try again next time. # INIT { use strict; use Sys::Syslog; # report all errors/warnings via syslog use Time::Local; # to convert timestamps back to UNIX time require '/etc/yaas.cfg'; # All user-servicable knobs are in this file %::AllowedIPs = (); delete($ENV{PATH}); # keep perl -T happy umask 022; # force sane umask # We'll need to map from month -> monthnum when converting timestamps my $i = 0; %::Months = map { $_, $i++ } qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my $myname = $0; $myname =~ s%.*/%%; openlog($myname, 'pid,ndelay,nowait', 'local2') || die "openlog failed: $!\n"; syslog('info', 'starting up v' . $::V); syslog('info', "whitelisted IPs: " . join(' ', keys(%::WHITELIST_IPS)) ); &LoadAllow; &SaveAllow; open(STDIN, "/usr/bin/tail -q -n 10000 -f $::LOGFILE |") or die "open $::LOGFILE failed: $!\n"; } #### BEGIN MAIN CODE LOOP next unless (m{ ^([A-Z].{14}) # save the timestamp to $1 \s[^\s]+\simapd\[[0-9]+\]:\s (?:Login|Authenticated) # varies by client and auth method \suser= ([^?\s]+) # save the username to $2 \shost= (?:[^[\s]+\s)? # skip the resolved hostname (if any) \[ ([0-9.]+) # save the IP address to $3 \] }x); my ($time, $user, $ip) = ($1, $2, $3); # don't bother if it's a permanently-OK IP address next if exists ($::WHITELIST_IPS{$ip}); # convert syslog timestamp to UNIX time my ($thismonth, $thisyear) = ((localtime)[4,5]); my ($mon, $mday, $hour, $min, $sec) = split(/[ :]+/, $time); $mon = $::Months{$mon}; # if the logfile's month is in the future, guess it's from last year my $year = $mon <= $thismonth ? $thisyear : $thisyear - 1; my $timestamp = timelocal($sec, $min, $hour, $mday, $mon, $year); # skip records that are too old (when reloading logs from scratch, etc) next if time - $timestamp > $::MAX_IDLE; &LoadAllow; # load %::AllowedIPs, sync %::WHITELIST_IPS if (exists ($::AllowedIPs{$ip})) { syslog('info', "updating entry for $ip: $user: $timestamp"); } else { syslog('info', "adding entry for $ip: $user: $timestamp"); } $::AllowedIPs{$ip} = "$user $timestamp"; if (@::KeepLines or keys %::AllowedIPs) { &SaveAllow; } else { syslog('err', "Not flushing empty file--previous load failed?"); } ### END MAIN CODE LOOP # (re)load the allow list sub LoadAllow { unless (open (HOSTS_ALLOW, "< $::HOSTS_ALLOW")) { syslog('err', "open '$::HOSTS_ALLOW' for reading failed, unable to load: $!"); return 0; } %::AllowedIPs = (); # flush temporary-allowed list @::KeepLines = (); # flush preserve-and-ignore list while () { chomp; # entries should be of the form 'i.p.ad.dr # comment' # 'comment' is either 'username timestamp' (temporary) # or an arbitrary string (permanent) my $found = 0; foreach my $SERVICE_NAME (@::SERVICES) { if (/^$SERVICE_NAME:\s*([0-9.]+)\s*#\s*(.+)/) { my $ip = $1; my $comment = $2; $found++; if ($comment =~ /^([a-z]+) ([0-9]{10})/) { # entry is temporary, this is its timestamp. my $user = $1; my $timestamp = $2; # check that its timestamp is not too old (drop if it is) if (time - $timestamp > $::MAX_IDLE) { syslog('info', "expiring entry for $ip: $user: $timestamp: too old"); } else { # add this to our list to keep $::AllowedIPs{$ip} = $comment; } } else { # permanent entry; add to %::WHITELIST_IPS if not there already # overwrite comment if it is there already $::WHITELIST_IPS{$ip} = $comment; } last; # break out of for loop } } unless ($found) # we don't know what this is, so leave it alone { push(@::KeepLines, $_); } } close (HOSTS_ALLOW); } # save / flush the current list to disk. # write to a tempfile first, # then rename current to 'old', # then rename tempfile to current # XXX: small race condition here sub SaveAllow { unless (open (NEW_ALLOW, "> $::NEW_ALLOW")) { syslog('err', "open '$::NEW_ALLOW' for writing failed, unable to save: $!"); return 0; } # preserve / reproduce all the lines we didn't care about foreach my $line (@::KeepLines) { print NEW_ALLOW "$line\n"; } foreach my $SERVICE_NAME (@::SERVICES) { # add entries for each allowed host; preserve comments foreach my $ip (keys %::WHITELIST_IPS) { print NEW_ALLOW "$SERVICE_NAME: $ip # $::WHITELIST_IPS{$ip}\n"; } foreach my $ip (keys %::AllowedIPs) { print NEW_ALLOW "$SERVICE_NAME: $ip # $::AllowedIPs{$ip}\n"; } } close(NEW_ALLOW); unless (rename($::HOSTS_ALLOW, $::OLD_ALLOW)) { syslog('err', "rename $::HOSTS_ALLOW, $::OLD_ALLOW failed, unable to save: $!"); return 0; } unless (rename($::NEW_ALLOW, $::HOSTS_ALLOW)) { syslog('err', "rename $::NEW_ALLOW, $::HOSTS_ALLOW failed, unable to save: $!"); unless (rename($::OLD_ALLOW, $::HOSTS_ALLOW)) { syslog('err', "rename $::OLD_ALLOW, $::HOSTS_ALLOW failed, unable to restore: $!"); } } }