This is a read-only archive. Find the latest Linux articles, documentation, and answers at the new Linux.com!

Linux.com

Feature

Custom scripting gives users a safe-du

By Philip J. Hollenback on December 07, 2005 (8:00:00 AM)

Share    Print    Comments   

As a system administrator, there are two ways you can interact with users: force them to follow the rules or encourage them with tools and guidelines. I prefer the second approach, as I think people generally want to do the right thing. Also, if people don't follow the rules at your company, that is a management problem, not a computer problem. Therefore, I prefer to concentrate my attention on helpful tools and scripts, which is exactly what I did recently to solve a typical system administrator problem.

My company has a Linux cluster with a terabyte of attached storage. Over time we noticed the head node was becoming more overloaded. Inspection of the system showed that users were starting dozens of copies of the du utility to determine disk space usage. This was a natural thing for them to do, because they had a need to know how much disk space was available. A lack of disk space would cause their software builds and tests to fail. The problem was that it takes five to seven hours for a du of the entire shared filesystem. Thus, when the filesystem was nearly full (as it of course usually was), the number of du processes would increase almost exponentially.

To address this problem, we first set up automated nightly disk space reports, so that users could check the status without running du. This still did not solve the problem, as the amount of used space could fluctuate dramatically over the course of 24 hours. Users still wanted and needed to run their own du processes throughout the workday.

While adding more disk space would have solved the problem, we are using a large disk array that is already filled to maximum capacity. In general, users tend to fill up all available disk space anyway, no matter how much you give them.

We then developed a policy: users could run du on any directory they owned. In addition, user du processes would be allowed to run for a maximum of one hour of wall time. Users in the wheel group would be exempt from these restrictions.

I was given the task of developing a tool to implement this policy. Some sort of wrapper around the existing du seemed like an obvious choice: the script could validate the input, abort if an invalid path was given, and terminate the du process if it ran too long.

I wrote a basic bash script in perhaps an hour's time. Then I thought about how to run it, and that is where I ran into trouble. I had thought that I would make the script set user id (setuid) or set group id (setgid) root, i.e. when run by any user it would actually run in the root group. Then, I could change the permissions on the real du so that only root could run it. The result would be that normal users could only access the real du through the wrapper script.

Of course that would make a pretty boring article, and in reality it didn't turn out to be that simple: you actually can't create setuid or setgid shell scripts on Linux. You can set the bit (chmod g+s script ) that tells the system to execute the script setgid, but the system ignores this on shell scripts. This behavior varies across different Unix and Unix-like platforms, but generally setuid/setgid shell scripts are frowned on due to security risks.

Thus my script was useless, except as an exercise to develop the logic necessary for the process. I just had to find a script language that allowed setgid scripts. Next I turned to the sysadmin's best friend, Perl. A little Web research showed that Perl fully supports setgid scripts and does so consistently across platforms. The key is the automatic use of taint mode, which forces programmers to deal with script inputs in a secure manner.

After a few more hours of programming I had a basic script, which I will now attempt to explain (and perhaps justify). For reference, here is the original script and a version with line numbers if you want to follow along.

safe-du script
     1	#!/usr/bin/perl -T
     2
     3	# Perl wrapper around du to only allow du on directories owned by the
     4	# user.  This prevents du sessions of (for example) a 2 terabyte raid
     5	# filesystem, which takes hours to finish.
     6
     7	# I started this in bash but switched to perl because you can't do
     8	# setgid shell scripts on Linux.  This script must be installed setgid
     9	# root.  That should be reasonably secure.
    10
    11	# You better set /usr/bin/du to not be executable by other, otherwise
    12	# this is pretty pointless.
    13
    14	# Phil Hollenback, philiph@pobox.com 9/22/05
    15
    16	use Getopt::Std;
    17	use Cwd;
    18	use POSIX qw(getuid);
    19
    20	# Have to set the path because we are in taint mode.
    21	$ENV{"PATH"} = "/bin:/usr/bin";
    22	$ENV{"BASH_ENV"} = "";
    23
    24	# how long do we allow du to run before killing it (in seconds)?
    25	$TimeOut=600;
    26
    27	$RealDu="/usr/bin/unsafe-du";
    28	-x $RealDu or die "can't execute $RealDu";
    29
    30	# Save the original command-line args.
    31	@ORGARGS=@ARGV;
    32
    33	# use getopt just to remove all options (so we are left with just path
    34	# arguments).
    35	getopt ":";
    36
    37	# Untaint the arguments.
    38	foreach (@ORGARGS)
    39	{
    40	    ($arg) = ($_ =~ /(.*)/);
    41	    push @NEWARGS,$arg;
    42	}
    43
    44	# Determine if user is in wheel group.
    45	$uid = getuid;
    46	$username = getpwuid($uid);
    47	# get the members of group wheel...
    48	($name,$passwd,$gid,$members) = getgrnam("wheel");
    49	# now see if our username is in the members list or our uid is 0.
    50	if (( $members =~ m|$username|) || ($uid == 0))
    51	{
    52	    # run the real du no questions asked.
    53	    exec("$RealDu @NEWARGS");
    54	    ### NOTREACHED ###
    55	}
    56
    57	# Now @ARGV contains _only_ non-option arguments.
    58	@PathList=@ARGV;
    59
    60	# Special case: if there aren't any arguments left, that means the
    61	# user ran du without a path specification.  Thus du should be run on
    62	# the current directory.  Push the current dir into the list to force
    63	# this.
    64	if ( $#PathList == -1 )
    65	{
    66	    push @PathList, getcwd;
    67	  }
    68
    69	# go through each remaining command-line argument (path) and see if it
    70	# is owned by the user.
    71	foreach (@PathList)
    72	{
    73	    -O $_ or die "you don't own $_";
    74	}
    75
    76	# Set a timer and run the real du.
    77	eval {
    78	local $SIG{ALRM} =
    79	    sub {
    80		# ignore SIGHUP here so the kill only affects children.
    81		local $SIG{HUP} = 'IGNORE';
    82		kill 1,(-$$);
    83		print STDERR "du terminated, max run time of $TimeOut seconds exceeded.\n";
    84	    };
    85	alarm $TimeOut;
    86	system ("$RealDu @NEWARGS") || die "failed to run $RealDu: $!";
    87	alarm 0;
    88	};
    89	$SIG{HUP} = 'DEFAULT';
    90
    91	exit 0;
    

First, everything up to line 32 sets up the initial environment. Notice lines 21 and 22: taint mode forces us to sanitize the environment. Note also that it doesn't matter if taint mode is enabled with the -T switch (on line 1) or not. The Perl interpreter automatically detects that a script is setuid or setgid and forces taint mode on.

Line 27 points to the system-supplied du. This brings up a security issue: the astute reader will notice that the du executable needs no special privileges to run. There's nothing to prevent a user from copying the real du to another location (like /tmp), changing the permissions and running it from there. The user could also copy a working du executable from another Linux system. That's okay -- as I said before, this script is designed to assist users, not completely lock them up. If you really want to restrict users then you need to look into something like SELinux.

Lines 30 through 35 save the command line arguments and strip all the options off the command line (thus leaving only file or directory arguments). I utilize a side-effect of getopt to do this: if you call it with only a colon as an parameter it removes all the options from ARGV (an option is any command-line argument which starts with a dash). This only works with getopt from the Getopt::Std package, not with Getopts from the package or with Getopt::Long. I could do the argument parsing myself, but why not rely on the standard facility?

Next I untaint the script input in lines 37 to 42. The Perl taint mechanism forces you to examine every script argument and strip it using regular expressions. This is where you do things like check that the arguments contain only alphanumeric characters and try to ensure that nothing funny (or malicious) slips through.

This script doesn't do any of that checking. Remember that I (mostly) trust the users of this script. Thus, all I do is run a regular expression that matches the entirety of each argument. This is the minimum necessary to satisfy Perl's taint mode. Any problem the Perl interpreter detects while in taint mode will just cause the script to abort.

This script also assumes that sysadmins know what they are doing. The check performed in lines 44 through 55 determines two things: If the userid is 0 (i.e. the script is being run by root), and if the user is a member of group wheel.

If either of these checks succeeds, the user is granted full access to the real du program. That's why I exec $RealDu on line 53 -- there's nothing more for the script to do, so there's no point in continuing -- just jump to the real du.

I'm not entirely happy with the way I determine if a user is a member of group wheel (doing a getpwuid lookup and a getgrnam lookup), but I was unable to determine a cleaner way to do this. I suspect a little thought would produce something more elegant. On the upside, this check should be fairly portable to other Unix platforms.

One of my design goals was to duplicate the behavior of normal du as much as possible, so users don't notice that they are actually running this script (unless they do something wrong, of course). That is the reason for lines 60 to 67. The default behavior of du is to operate on the current directory if the user fails to supply any directory argument so du is the same as du .. Without the check that's performed on these lines, my script would just silently exit. By pushing the current directory onto the directory list the script emulates the default behavior of du in this case and ensures any user used to that behavior doesn't get tripped up.

Now we begin to get to the real meat of the script. Lines 69 through 74 perform the permission check. If the user doesn't own any path specified on the command line, the script aborts with an error. Again, the simple way to implement this would be to assume that there is only one argument to du. This is how du is run probably 99% of the time. However, you are allowed to supply multiple path arguments to du, in which case it checks disk usage for each path. The script will iterate over the list of command-line arguments instead of assuming there is just one path specified. The actual file test is with one of Perl's file tests, -O, which will return true if the file is owned by the real user, rather than the -o test, which tests the user the script is running under. Since this is a setgid script, we want to know the real user has permission to read the file.

Remember I said there were two restrictions in my version of du: directory ownership and runtime limit. Lines 76 through 89 implement the runtime limit and then actually invoke the real du. This invocation took some thought. I first tried to set an alarm signal handler at the top of the script and then set the alarm timeout before invoking $RealDu. That kind of worked, but it resulted in both the child (the real du) and the parent (this script) dying at the same time. I wanted this script to continue after the child du failed. Again, in reality this was a small point, as the most the script would ever do afterward was print the error message about killing $RealDu because it ran too long.

A bit of Googling and head-scratching produced the answer: you have to wrap the whole thing in an eval and set the signal handler as shown. Then it's a simple matter to start my timer and fire off the real du command. If the real du exceeds $TimeOut seconds, the alarm signal fires and the kill command kills all the children of my script (just the real du, in this case). I ignore SIGHUP so my script doesn't catch it.

And that is the script in its entirety. To install it, move the real du to another location, such as /usr/bin/unsafe-du, and remove other execute permissions on it (chmod a-x /usr/bin/unsafe-du). Then install this script, owned as root, as /usr/bin/du with the setgid flag set.

The final permissions should look something like this:

-rwxr-sr-x 1 root root 2424 Sep 23 16:10 /usr/bin/du
-rwxr-xr-- 1 root root 25884 Mar 14 2001 /usr/bin/unsafe-du

Note that you may need to install the perl-suidperl package to enable Perl setuid scripts on some distros. This could all be done with setuid, but setgid is a lesser permission, so it seems appropriate to use it instead. A compromise would lead to the running in the root group, not running as uid 0.

As I said, the goal with this script was to assist users, not lock them out, while protecting system performance. People can still run du, as long as it is on their own directories or files. Meanwhile, the time limit helps limit the number of long-running du processes on the system. The limit of one hour is somewhat arbitrary: we just picked a number that seemed like it would work for most legitimate cases. It seems like that is enough, because I haven't heard any complaints from users.

Users, if they are so inclined, can circumvent these protections. In practice, this hasn't been a problem. I installed this script on the system several months ago. No users have complained, or even commented on it. At the same time, the number of random du processes being run on the system have dropped dramatically. Thus this script is a complete success, and a great example of how those everyday sysadmin tasks get solved -- one Perl script at a time.

Share    Print    Comments   

Comments

on Custom scripting gives users a safe-du

Note: Comments are owned by the poster. We are not responsible for their content.

why didn't you..

Posted by: Anonymous Coward on December 07, 2005 07:44 PM
Why didn't you just set a default alias (or replace du) with something that called du $HOME?

#

Re:why didn't you..

Posted by: Administrator on December 07, 2005 11:47 PM
Because users own more than just their home directories, such as scratch directories on other filesystems.

Besides, that woudln't make a very exciting article.<nobr> <wbr></nobr>:)

#

df

Posted by: Anonymous Coward on December 07, 2005 09:58 PM
Couldnt they have used the 'df' command instead?

#

Re:df

Posted by: Anonymous Coward on December 08, 2005 06:37 PM
exactly. Why educate your audience on df -h when you can write a 67 line script that creates confusion. Ever wonder why windows is popular, it does it automatically.

#

Run bash setuid/setgid

Posted by: Anonymous Coward on December 07, 2005 11:44 PM
From the <tt>bash</tt> manpage:
If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS variable, if it appears in the environment, is ignored, and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

Linux doesn't allow setuid shell scripts anyways, but the <tt>bash</tt> behavior (inherited from <tt>ksh</tt> I think) should be noted.

The better (but less fun) solution would be to make a script (chmod 700) and set up <tt>sudo</tt> so that the desired users can run that script.

#

Re:Run bash setuid/setgid

Posted by: Administrator on December 07, 2005 11:59 PM
I disagree that using a wrapper around sudo would be a better solution. The difference is that safe-du implemements finer control. It also helps provide against accidental mistakes - if I used sudo a user could still accidentally run du on an entire filesystem. That can't happen with safe-du.

#

security problem

Posted by: Anonymous Coward on December 08, 2005 02:50 AM
There's a reason that setgid scripts are discouraged; unless much care is taken, they tend to let unprivilege users run arbitrary code as root. For instance, it looks to me that running

  $ touch 'foo;sh'

  $ du 'foo;sh'
will give anyone on your system a root shell. You should untaint with greater care.

#

Yup

Posted by: Anonymous Coward on December 08, 2005 02:50 PM
You are correct.

#

Re:security problem

Posted by: Anonymous Coward on December 08, 2005 11:02 PM
Not on my system[1]:
(as a user)
$ touch 'foo;sh'
$ du 'foo;sh'
sh-3.00$ cd<nobr> <wbr></nobr>/root
sh: cd:<nobr> <wbr></nobr>/root: Permission denied
sh-3.00$ touch test
sh-3.00$ ls -l test
-rw-r--r-- 1 bogus users 0 2005-12-08 09:51 test<nobr> <wbr></nobr>...no security problem here.

[1] Slackware 10.2 / bash 3.0

#

relief joint

Posted by: Anonymous Coward on May 28, 2006 07:17 PM
[URL=http://painrelief.fanspace.com/index.htm] Pain relief [/URL]

  [URL=http://lowerbackpain.0pi.com/backpain.htm] Back Pain [/URL]

  [URL=http://painreliefproduct.guildspace.com] Pain relief [/URL]
[URL=http://painreliefmedic.friendpages.com] Pain relief [/URL]
[URL=http://nervepainrelief.jeeran.com/painrelief<nobr>.<wbr></nobr> htm] Nerve pain relief [/URL]

#

Architecture, Architecture, Architecture

Posted by: Anonymous Coward on December 08, 2005 09:32 AM
One big filesystem is harder to manage than several smaller ones. This is why I use Logical Volume Management (LVM) to deal with large storage devices. Instead of a bunch of directories under<nobr> <wbr></nobr>/big-disk-here, I create different logical volumes for each project and for home directories. Smaller volumes are also easier to back up.

Plus, your choice of filesystem is important. Programs like "du", "find", and your backup software have to walk the entire directory tree and run fstat() on every file entry each time they run, so it's important that "walking" the tree is efficient.

Walking the directory tree is horribly slow, for instance, on Sun UFS filesystems. Filesystems like ReiserFS and XFS are purported to be better than EXT-2/3 in this respect, so programs like "du" should run faster on these filesystems.

As usual, YMMV and Test Early And Often.

#

How about quotas?

Posted by: Anonymous Coward on December 08, 2005 05:34 PM
I think the quota system keeps track of usage continously, so perhaps it could fo the job. It would tell you about space usage in terms of the quota, not in terms of physical space, but there are some situations where you may make these equivalent.

Either make a quota policy and stick to it, or for a quick hack if all your users are in the same group set that group a quota equal to the disk size.

The quota command will tell the user how they're doing.

#

Re:How about quotas?

Posted by: Administrator on December 14, 2005 01:07 AM
I should have spoken a bit more about quotas in the article. I agree that they are probably a better solutiopn overall and we plan to move to using quotas long-term. This safe-du script is (hopefully) a temporary measure.

#

Re:Run bash setuid/setgid

Posted by: Anonymous Coward on December 13, 2005 01:09 AM
<tt>sudo</tt> can be set up to only allow a single command, with specific arguments, to specific users. It also logs all usage.

How is <tt>safe-du</tt> safer than that?

#

Re:Run bash setuid/setgid

Posted by: Administrator on December 13, 2005 04:42 AM
My point was that in this case safe-du allows finer-grained control. Consider this example (run as normal user):


$ safe-du<nobr> <wbr></nobr>/



vs.



$ sudo du<nobr> <wbr></nobr>/


The safe-du will abort because it will check if the user owns the root directory. The sudo will run because it doesn't check if the user owns the root directory.


I'm aware that you can specify the allowed arguments in sudo but in this case the possible arguments are too many - any directory or file is a valid argument to du.


So it's not exactly that safe-du is safer than 'sudo du', it's just in this case a better way of controlling usage.

#

This story has been archived. Comments can no longer be posted.



 
Tableless layout Validate XHTML 1.0 Strict Validate CSS Powered by Xaraya