1#!${PERLPATH}
2#! $Id: sudoscriptd-in,v 1.9 2004/11/12 05:27:54 hbo Exp $
3use strict;
4use warnings;
5use POSIX qw(mkfifo setsid);
6use POSIX qw(:sys_wait_h);
7use Fcntl qw(O_RDWR O_RDONLY O_WRONLY);
8use File::Path;
9use Sys::Syslog qw(:DEFAULT setlogsock);
10use File::Basename qw(dirname basename);
11use Getopt::Long;
12use lib ${SSLIBDIR};
13use Sudoscript;
14
15our $ss=Sudoscript->new();
16exit if (! defined $ss);
17
18# We take one option: --datefmt|-d which controls how dates are written in the log.
19my ($DATEFMT,$killme);
20GetOptions (
21	    "datefmt:s" => \$DATEFMT,
22	    "kill" => \$killme,
23	   );
24
25$DATEFMT='short' if (! defined $DATEFMT);
26
27my $dpid=$ss->checkpid();
28if ($killme){
29  if ($dpid){
30    `kill -HUP $dpid`;
31    print "Sudoscriptd at $dpid killed\n";
32  } else {
33    print "Can't find a sudoscriptd to kill!\n";
34  }
35  exit;
36}
37
38if ($dpid){
39  print STDERR "sudoscriptd already running at $dpid\n";
40  print STDERR "Can't start new sudoscriptd\n";
41  exit 1;
42}
43
44# Become a daemon
45my $pid=fork;
46exit if $pid;
47die "Couldn't fork $!" unless defined($pid);
48
49our $rundir="/var/run";
50our $fifodir="$rundir/sudoscript";
51our $logdir="/var/log";
52
53foreach my $tag ('','log','merge','comp'){
54  unlink "$fifodir/stderr$tag" if (-e "$fifodir/stderr$tag");
55}
56$ss->daemon_io();
57
58die "Couldn't start new session $!" unless POSIX::setsid();
59  # Change our ps id, if supported by OS
60$0="sudoscriptd: main";
61
62# 2 MiByte limit on log size before compression/rotation.
63my $MAXLOGSIZE=1024*1024*2;
64
65
66# Open syslog
67my $facility='AUTHPRIV';
68setlogsock 'unix';
69openlog("sudoscriptd",'pid',$facility) || die "Can't open syslog $!";
70syslog('info',"Master sudoscriptd starting");
71
72# Declare these for the interrupt handler below.
73# Hash of outstanding logger PIDs by session tag
74my %LOGGERS;
75# PID of the log merger
76my $mergerpid;
77# interrupt handler to reap outstanding children
78
79sub main_exit_handler{
80  # We could be called directly, or through a signal
81  # If the former, we'll have parameters.
82  my $exitseverity=shift;
83  my $exitmsg=shift;
84  $exitseverity="info" if (!$exitseverity || $exitseverity eq 'HUP');
85  $exitmsg="Master sudoscriptd caught signal. Exiting" if (!$exitmsg);
86  syslog($exitseverity,$exitmsg);
87  # This is main daemon shutdown. Kill all other daemons we know about.
88  foreach (keys %LOGGERS){
89    kill 1,$LOGGERS{$_};
90    waitpid $LOGGERS{$_},&WNOHANG;
91  }
92  if ($mergerpid){
93    kill 1, $mergerpid;
94    waitpid $mergerpid,&WNOHANG;
95  }
96  closelog();
97  exit;
98};
99$SIG{'HUP'} = \&main_exit_handler;
100
101# Check to see if we have a ssers group
102our ($grname,$grpasswd,$gid) = getgrnam 'ssers';
103$grpasswd=""; # make strict happy
104# Set up state directories
105create_state_dirs();
106
107# Record our PID for those who would wish us ill 8)
108if (! (open PID,">$rundir/sudoscriptd.pid")){
109  syslog('crit',"Can't open $rundir/sudoscriptd.pid %m");
110  closelog();
111  die "Couldn't record pid, $!";
112}
113
114print PID "$$\n";
115close PID;
116
117
118# The front-end FIFO
119my $fifo="$fifodir/rendezvous";
120# Create the FIFO that sudoshell will rendezvous on
121# FIFO must me writable by group if group exists
122if ($grname){
123  mktypescript($fifo,0,0620,$gid);
124 } else {
125  mktypescript($fifo,0,0600);
126}
127
128# Create the back-end merge daemon
129$mergerpid=log_merger($DATEFMT);
130
131my $handshake; # sudoshell's id string
132while (1){
133  # Open the rendezvous FIFO or exit
134  main_exit_handler('crit',"Couldn't open FIFO ($fifo), %m") if (!sysopen(FIFO, $fifo, O_RDONLY));
135
136  # Main input loop. ss is on the writing end.
137  while (<FIFO>){
138
139   if (/^HELO (.*)/){ # HELO handshake. Fork a new logger
140      $handshake=$1;
141       syslog('info',"Forking new logger for sudoshell $handshake");
142      $pid=new_logger(split /\s+/,$handshake);
143      # Recored the new logger's PID
144      $LOGGERS{$handshake}=$pid;
145    }
146    if (/^GDBY (.*)/){ # GDBY handshake. Signal and reap the old logger
147      $handshake=$1;
148      $pid=$LOGGERS{$handshake};
149      if ($pid){
150	my $count= kill 1,$pid;
151	waitpid $pid,0;
152	delete $LOGGERS{$handshake};
153	syslog('info',"session $handshake closed");
154      } else {
155	syslog('err',"No PID recorded for handshake $handshake. Can't clean up session!");
156      }
157    }
158  }
159  # End of master daemon
160}
161
162sub create_state_dirs{
163  # Set up logging and fifo directories
164  # Create directories as needed
165  if (!-d $rundir){
166    if (!mkdir $rundir,0755){
167      syslog('crit',"Can't mkdir $rundir %m");
168      closelog();
169      die "Can't mkdir $rundir $!";
170    }
171  }
172
173  # FIFO dir has to be group writeable if ss is to become a user other than root
174  my  $fifodirmode;
175  if ($grname){
176    $fifodirmode=0770;
177  } else {
178    $fifodirmode=0700;
179  }
180
181  `rm -fr $fifodir`;
182  if (!mkdir $fifodir,$fifodirmode){
183    syslog('crit',"Can't mkdir $fifodir %m");
184    closelog();
185    die "Can't mkdir $fifodir $!";
186  }
187
188  if ($grname){
189    chown 0,$gid,$fifodir;
190  }
191
192  if (!-d $logdir){
193    if(!mkdir $logdir,0700){
194      syslog('crit',"Can't mkdir $logdir %m");
195      closelog();
196      die "Can't mkdir $logdir $!";
197    }
198  }
199  # Let caller know where the fifo dir is
200  return $fifodir;
201}
202#
203#
204#  merger daemon -
205# Collect all session's data (pre tagged by the loggers) into
206# a single log file. Rotate the log if it exceeds $MAXLOGSIZE in size.
207sub log_merger{
208  my $ss=Sudoscript->new();
209  my $pid=fork;
210  return $pid if ($pid); # caller needs pid to signal us
211  if (!defined $pid){
212    syslog ('crit','Log merger encountered fork error %m');
213    closelog();
214    die "Couldn't fork $!";
215  }
216  $ss->daemon_io('merge');
217  my $session=POSIX::setsid();
218  if (!$session){
219    syslog ('crit','Log merger Couldn\'t setsid() %m');
220    closelog();
221    die "Couldn't start new session $!";
222  }
223  # Change our ps id, if supported by OS
224  $0="sudoscriptd: merger";
225
226  # close parent's syslog
227  closelog();
228
229  # close parent's rendezvous FIFO
230  close FIFO;
231
232  # Open our own syslog
233  my $facility='AUTHPRIV';
234  openlog("sudoscriptd-merger",'pid',$facility) || die "Can't open syslog $!";
235
236  # Predeclares for the signal handler
237  # Our FIFO
238  my $fifo=$fifodir."/merge";
239  # List of compressor PIDs
240  my @comppids;
241
242  # Merger signal handler
243  # Close and unlink FIFO
244  # Reap any outstanding compressors
245  $SIG{'HUP'} =
246    sub {
247      syslog('info',"Merger caught signal. Exiting");
248      print LOG "Merger caught signal. Exiting\n";
249      close LOG;
250      close MYFIFO;
251      unlink $fifo;
252      closelog();
253      foreach(@comppids){
254	kill 1,$_;
255	waitpid $_,&WNOHANG;
256      }
257      exit;
258    };
259
260  # Announce ourselves to syslog
261  syslog('info',"New merger");
262
263  # Open the log file, obtaining the current log size and possibly a compressor PID
264  my ($size,$comppid)=openmylog("$logdir/sudoscript");
265  # Save  PID, if any
266  push @comppids,$comppid if($comppid);
267
268  # Announce ourselves in the log file
269  print LOG $ss->datestamp($DATEFMT)." New Merger\n";
270
271  # Create out merge FIFO
272  mktypescript($fifo);
273  # Announce success
274  print LOG "opened FIFO $fifo\n";
275
276  # Merger input loop
277  while(1){
278    # open the FIFO
279    sysopen(MYFIFO, $fifo,O_RDONLY) or die "Couldn't open FIFO ($fifo), $!";
280
281    while (<MYFIFO>){
282
283      # Datestamp the input
284      $_=$ss->datestamp($DATEFMT)." $_";
285      # Keep track of the size
286      $size += length($_) +1;
287      # Log the input
288      print LOG;
289      # Rotate the log if $MAXLOGSIZE is exceeded
290      if ($size > $MAXLOGSIZE){
291	# as above
292	($size,$comppid)=openmylog("$logdir/sudoscript");
293	push @comppids,$comppid if($comppid);
294      }
295
296      # if we have outstanding compressor kids, wait for them here
297      if ($#comppids >=0){
298	if (waitpid $comppids[0],&WNOHANG >0){
299	  shift @comppids;
300	}
301      }
302    }
303  }
304  close MYFIFO;
305  # End of merger daemon
306}
307#
308#
309# Logger daemon
310# Creates a session FIFO for sudoshell
311# Accepts script(1) output from sudoshell
312# Tags it with session ID, and passes it on to the merger daemon
313#
314sub new_logger{
315  # real user, sudoshell pid, effective user (or root if undef)
316  my ($user,$sspid,$runas)=@_;
317  my $ss=Sudoscript->new();
318  my $pid=fork;
319  return $pid if ($pid);
320  if (!defined $pid){
321    syslog ('crit','Child logger encountered fork error %m');
322    closelog();
323    die "Couldn't fork $!";
324  }
325  $ss->daemon_io('log');
326  my $session=POSIX::setsid();
327  if (!$session){
328    syslog ('crit','Child logger Couldn\'t setsid() %m');
329    closelog();
330    die "Couldn't start new session $!";
331  }
332  # Close parent's syslog.
333  closelog();
334  # Close parent's rendezvous FIFO
335  close FIFO;
336
337  # Change our ps id, if supported by OS
338  $0="sudoscriptd: logger";
339
340  # Open our own syslog
341  my $facility='AUTHPRIV';
342  openlog("sudoscriptd-logger",'pid',$facility) || die "Can't open syslog $!";
343
344  # Set up the session FIFO directory
345  # We create a subdirectory in case we are granting access to the run
346  # directory to the ssers group. If we are, that group must have
347  # read/write access. The directory we create here has no group or other access,
348  # thereby limiting access to the user only. (This is still a security hole if we are enabling
349  # a role account shared by two or more real users.)
350  my $sessdir;
351  $sessdir=$fifodir."/ssd.${user}_";
352  if ($runas){
353    $sessdir .= "${runas}_$sspid";
354  } else {
355    $sessdir .= "root_$sspid";
356  }
357
358  if (!-d $sessdir){
359    `rm -fr $sessdir` if (-e $sessdir);
360    mkdir $sessdir,0700;
361  }
362  # If we have an "effective" username, get the UID and chown the session dir to that user.
363  my ($tuname,$tpasswd,$tuid); # "target" user
364  if (defined $runas){
365    ($tuname,$tpasswd,$tuid)=getpwnam $runas;
366  } else {
367    $tuid=0;
368  }
369  chown $tuid,0,$sessdir;
370
371  # Predeclare the merge FIFO name for the signal handler.
372  my $mergefifo="$fifodir/merge";
373
374  $SIG{'HUP'} =
375    # Remove the session directory and FIFO and exit
376    sub {
377      syslog('info',"logger ($user,$sspid) caught signal. Exiting");
378      print MERGEFIFO "logger ($user,$sspid) caught signal. Exiting\n";
379      close MERGEFIFO;
380      close SESSFIFO;
381      `/bin/rm -fr $sessdir`;
382      closelog();
383      exit;
384    };
385
386  # Announce ourselves to syslog
387  syslog('info',"new_logger for user $user with pid $sspid");
388
389  # Open the merge FIFO for output
390  if (!sysopen(MERGEFIFO, $mergefifo,O_WRONLY)){
391    syslog('crit',"Couldn't open FIFO ($mergefifo) %m");
392    closelog();
393    die "Couldn't open FIFO ($mergefifo), $!";
394  }
395  # Unbuffer our OUTPUT
396  my $foo=select MERGEFIFO;
397  $|=1;
398  select $foo;
399
400  # Announce ourselves on the merge FIFO
401  print MERGEFIFO "New logger for $user with pid $sspid\n";
402
403  # Create the session FIFO
404  my $sessfifo="$sessdir/$user$sspid.fifo";
405  mktypescript($sessfifo,$tuid,0200);
406
407  # Give ss time to Posix::pause
408  sleep 1;
409  # Signal sudoshell that we are ready
410  kill "WINCH",$sspid;
411
412  # Logger input/output loop
413  while(1){
414    # Open the session FIFO for read
415    if (!sysopen(SESSFIFO, $sessfifo,O_RDONLY)){
416      syslog('crit',"Couldn't open FIFO ($sessfifo) %m");
417      close MERGEFIFO;
418      closelog();
419      die "Couldn't open FIFO ($sessfifo), $!";
420    }
421    while (<SESSFIFO>){
422      # Tag the input with the session ID
423      $_="$user:$sspid $_";
424      # And send it to the merge FIFO
425      print MERGEFIFO;
426    }
427  }
428  # End of logger daemon
429}
430#
431#
432# Create a FIFO with optional owner group and mode overriding defaults
433sub mktypescript {
434  my ($fifo,$owner,$mode,$group)=@_;
435
436  # Delete the FIFO if it exists.
437  if (-e $fifo){
438    unlink $fifo;
439  }
440  # Default mode is r/w to owner
441  $mode=0600 if (! defined $mode);
442
443  if (!mkfifo($fifo,$mode)){
444    syslog('crit',"mktypescript() couldn't make new fifo $fifo %m");
445    closelog();
446    die "Can't make fifo $fifo $!";
447  }
448
449  # Default owner is root
450  $owner=0 if (!defined $owner);
451
452  # Default group is wheel/root
453  $group=0 if (!defined $group);
454  chown $owner,$group,$fifo;
455  chmod $mode,$fifo;
456}
457
458
459#
460# (re)open the log file
461# Rotate the logs if larger than $MAXLOGSIZE
462sub openmylog{
463  # Passed the log file name
464  my $log=shift;
465
466  # compressor PID
467  my $pid;
468
469  # File stat values
470  my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size);
471
472  # Get the logfile size if it exists
473  if (-e $log){
474    ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size)=stat $log;
475
476    # If te size exceeds $MAXLOGSIZE, fork a rotator/compressor
477    if ($size>$MAXLOGSIZE){
478      $pid=rotate_log($log);
479
480      # rotator/compressor moves old log out of our way
481      $size=0;
482    }
483  }
484
485  # Open the log file
486  # Default is create new ..
487  my $op = ">";
488  #.. but append if it already existts
489  $op.=">" if (-e $log);
490  chmod 0600,$log;
491  if (!open LOG, "$op$log"){
492    syslog('crit',"openmylog() couldn't open new log $op$log %m");
493    die "Couldn't open log ($op$log), $!";
494  }
495  my $foo=select LOG;
496  $|=1; # Unbuffered
497  select $foo;
498  # Return the current log size and any compressor PID that we may have spawned.
499  return ($size,$pid);
500}
501#
502#
503# Rotate and compress the log files.
504# Called by openmylog() when log file size exceeds $MAXLOGSIZE bytes
505sub rotate_log {
506  # We are passed the log file name
507  my $log =shift;
508  # We compute the dirname and basename
509  my $logdir=dirname($log);
510  my $logbase=basename($log);
511
512  # Temp log file name
513  my $tlog="$log.$$";
514
515  # Move the old log out of the way
516  link $log, $tlog;
517  unlink $log;
518
519  my $ss=Sudoscript->new();
520  # Fork a child to do the rotation/compression
521  my ($pid)=fork;
522  # Parent gets the child PID so we can interrupt if necessary
523  # We've moved the old log out of the way so the parent can create a new one.
524  return $pid if $pid;
525
526
527  # CHILD
528
529  $ss->daemon_io('comp');
530
531  die "Couldn't start new session $!" unless POSIX::setsid();
532
533  # Fix up our ps name if OS supports it
534  $0="sudoscriptd: compressor";
535
536  my $MAXLOGS=10;
537  # Rename the old log files incrementing numeric extensions
538  # The $MAXLOGth one goes in the bit bucket
539  my ($currlog,$lastlog);
540  for (my $i=$MAXLOGS-1;$i;$i--){
541    $currlog="$logdir/$logbase.$i.gz";
542    $lastlog="$logdir/$logbase.".($i+1).".gz";
543    if (-e $currlog){
544      unlink $lastlog;
545      link $currlog,$lastlog;
546    }
547  }
548  # dot one is now linked with dot two
549 # clean it up
550  unlink "$logdir/$logbase.1.gz";
551
552  # compress the temp log onto the dot one
553  my $cmd="gzip -c $tlog >$logdir/$logbase.1.gz";
554  `$cmd`;
555  chmod 0600,"$logdir/$logbase.1.gz";
556  unlink $tlog;
557  exit 0;
558}
559
560=pod
561
562=head1 NAME
563
564  sudoscriptd - logging daemons for sudoshell(1)
565
566=head1 SYNOPSIS
567
568  sudoscriptd [-d|--datefmt long|short|sortable]
569
570=head1 VERSION
571
572This manpage documents version ${VERSION} of sudoscriptd
573
574=head1 DESCRIPTION
575
576I<sudoscriptd> is a daemon for logging output from L<sudoshell(8)>.
577Used with that script, it provides an audit trail for shells run under
578sudo.
579
580=head1 README
581
582When I<sudoscriptd> starts, it creates a named pipe (FIFO) in a spool
583area. Then it forks a log management daemon that opens another FIFO
584and hangs around waiting for someone to write to it. When a new sudoshell
585starts, it writes the name of the user who ran it (from SUDO_UID) and its
586own PID to the first FIFO, then pauses waiting for a signal.
587Sudoscriptd forks a logger with the information given by sudoshell,
588which opens yet another FIFO, whose name is derived from the username and
589PID. The logger then sends the signal that sudoshell is waiting for.
590Sudoshell then runs script(1) on the session FIFO. The logger takes the
591output thus produced, tags it with a session ID, and writes it to the
592log management daemon's (remember him?) FIFO. The log daemon tags the data
593with a datestamp and writes it to a log file. It also manages the logs so
594they don't overflow the logging partition. When the user ends her script(1)
595session, sudoshell tells the front end daemon that it is done. The daemon
596signals the session logger to wrap up its work, which it does by deleting
597the session FIFO and exiting.
598
599=head1 CONFIGURATION
600
601I<sudoshell> uses L<sudo(8)> to perform all its authentication and
602privilege escalation.  The I<sudoshell> user must therefore be in the
603I<sudoers> file (See L<sudoers(5)>.)  with an entry that allows
604running I<sudoshell> as the desired user. See the SUDOCONFIG file in
605the distribution for details. (On Linux, this will be in
606/usr/share/doc/sudoscript-VERSION. Everywhere else, it's in
607/usr/local/doc/sudoscript-VERSION.)
608
609=head1 IS THIS SECURE?
610
611In a word, no. Giving a user a root shell is a bad idea if you don't trust him
612or her. There are countless ways to evade the audit trail provided by sudoscript,
613even without root privilege. Let me highlight the last part of that sentence: I<even
614without root privilege!> (Think about the implications of the fact that a user must have
615write access to the logging FIFO to see what I mean.) That means you can't rely on
616this tool to maintain security for you. So, what good is sudoscript? It's useful in an
617at least two environments. First, you trust your users, but need a record of what they
618do for auditing purposes. Second, you may or may not trust your users, but they have
619successfully agitated for a root (or other) shell. Sudoscript then provides an audit trail as
620long as your users don't try to evade it.
621
622See the file SECURITY (in the same place as SUDOCONFIG, above) for more on sudoscript's
623security assumptions.
624
625=head1 SWITCHES
626
627One optional switch, C<--datefmt>, is accepted by C<sudoscriptd>. This
628controls the format of the datestamps in the log file. Three options
629are available.
630
631=over 4
632
633=item long
634
635This selects a long date format of 'wdy mon dd hh:mm:dd ZZZ YYYY' where
636'wdy' is the weekday name, 'mon' is the three letter month name, 'dd'
637is the day of the month, hh:mm:ss' is the local time, 'ZZZ' is the local time
638zone name and 'YYYY' is the four digit year.
639
640=item short
641
642This selects a shorter date format of 'wdy mon dd hh:mm:dd'. This is
643just the long with the time zone and year removed. C<short> is the default
644format if no C<--datefmt> is given.
645
646=item sortable
647
648This selects a compressed and numerically sortable format of 'yyyymmddhhmmss'.
649
650=back
651
652=head1 FILES
653
654The front end fifo is /var/run/sudocript/rendezvous. The backend FIFO
655is /var/run/sudocript/merge. These two are semi-permanent. The session
656FIFOs are named /var/run/sudocript/ssd{username}{pid}. They go away once
657the session closes.
658
659The log file is named /var/log/sudoscript. When the backend daemon
660rotates the log, it forks a compressor that creates files called
661/var/log/sudoscript.{n}.gz, where {n} is one through ten.
662Sudoscriptd stores its PID in /var/run/sudoscriptd.pid.
663
664=head1 BUGS
665
666The script(1) output is pretty ugly. All control characters are preserved
667exactly as typed, or worse, as displayed by curses based console apps like
668vi. The content of such logs can look completely unintelligible unless
669they are cleaned up first. A shell script from the "Unix Power Tools" book
670that uses sed(1) to do a first pass over such logs is available at
671L<ftp://ftp.oreilly.com/pub/examples/power_tools/unix/split/script.tidy>.
672I considered building something like that into sudoscriptd, but rejected it
673for two reasons. First, the daemon needs to get back to reading the FIFO
674as quickly as possible to avoid losing data to an over-full buffer. Second,
675any cleanup of the logs would I<remove information>. This could be bad if
676I were over-zealous in my clean up. As it stands, you can run your own
677clean up on the log data without destroying the original log.
678
679The datestamp() routine is not locale aware and returns American
680English values.
681
682=head1 SEE ALSO
683
684sudoscript(8)
685
686sudoshell(1)
687
688Sudoscript(3pm)
689
690sudo(8)
691
692sudoers(5)
693
694
695=head1 PREREQUISITES
696
697sudo - L<http://www.courtesan.com/sudo/index.html>
698
699=head1 OSNAMES
700
701C<Solaris>
702
703C<Linux>
704
705C<FreeBSD>
706
707C<OpenBSD>
708
709C<HP-UX>
710
711=head1 SCRIPT CATEGORIES
712
713UNIX/System_administration
714
715=head1 CONTRIBUTORS
716
717The following people offered helpful advice and/or code:
718
719   Dan Rich       (drich@emplNOoyeeSPAMs.org)
720   Alex Griffiths (dag@unifiedNOcomputingSPAM.com)
721   Bruce Gray     (bruce.gray@aNOcSPAMm.org)
722   Chan Wilson    (cwilson@coNrOp.sSgPi.cAoMm>
723   Tommy Smith    (tsNmOith@eSaPtAeMl.net)
724   Donny Jekels   (donny@jNOeSkPeAlMs.com
725
726=head1 AUTHOR
727
728Howard Owen, E<lt>hbo@egbok.comE<gt>
729
730=head1 COPYRIGHT AND LICENSE
731
732Copyright 2002,2003 by Howard Owen
733
734sudoscript is free software; you can redistribute it and/or modify
735it under the same terms as Perl itself.
736
737=cut
738