userv - how to make cron (et al) not setuid

Aleph One (aleph1@DFW.NET)
Tue, 23 Dec 1997 11:12:52 -0600

---------- Forwarded message ----------
Date: Wed, 17 Dec 97 11:19 GMT
From: Ian Jackson <userv-maint@chiark.greenend.org.uk>
To: linux-security@redhat.com
Subject: [linux-security] userv - how to make cron (et al) not setuid

0. Introduction

Some time ago I posted on linux-security to say that I was working on
a client/server pair which would allow you to invoke a privileged
service in a more secure manner. I've now completed this, and it's
been service by way of alpha-test on my own system for some time,
implementing user-provided CGI scripts (which run as the user).

Perhaps I should explain some more. There are a number of places on a
Unix system where program invocations cross security boundaries. At
the moment this is handled using setuid programs, which has a number
of disadvantages: you can't easily draw the security boundary where
you want it; the setuid program really has to be written in C (with
attendant buffer overrun problems et al); and sanitising the
environment (not just the environment variables, but things like
umasks, ulimits, and all the other guff that children inherit) to make
it safe to execute is very difficult.

My replacement works as follows: you design your program so that the
security boundary is exactly at a program invocation, and replace the
setuid call with a call via my `userv' facility. userv is responsible
for properly enforcing the security boundary, so that the called parts
of your program can trust their PATH, ulimits, controlling tty, et
al. userv has to be secure, but you only have to write it once.
Furthermore, the client/server model means you don't have to worry so
much about resetting all the many inherited properties of processes.

1. Applications

The applications are many. A lot of Unix facilities have mutually
untrusting security domains (users) calling each others' programs.
For example, mail and lpr (with deferred printing) want to be able to
access the users' files without trusting those users, but the user
doesn't want to trust them more than they have to.

Below I'll describe an example of how to use userv to implement a more
secure cron/at daemon. I haven't actually gotten round to writing
this yet, mainly because it seemed an uninteresting problem - a few
fairly trivial Perl scripts. Compare that to the onerous task of
writing a secure cron subsystem in C using only conventional setuid
calls ...

3. Configuration

userv has a configuration language which allows users (and the system
administrator on behalf of users) to control which commands can be
executed as them and exactly how, by which other users. Details about
the invoking user are also passed to the executed program in special
USERV_... environment variables, so that it can implement its own
access control or other features.

The configuration scheme has the facility for system administrators to
provide default configurations of services, which the user can
override, or for the sysadmin to provide `mandatory' services. For
example, a sysadmin might wish to be able to securely remove old files
in /var/tmp, and could enforce the provision of a `remove this file in
/var/tmp' facility to some semi-trusted set of operator users.

4. Limitations

userv does not solve all security problems with privileged programs.
For example, privileged programs written in C invoked by userv will
still be vulnerable to any buffer overruns in their argument parsing
(if userv is configured to allow arguments to be passed to them).
However, with userv it is no longer necessary to use C for many
privileged programs. It is much easier to make a correct and secure
privileged program if it can be written in a language like Perl,
Python or whatever.

userv is also not suitable for all applications. Situations where the
privileged program needs (for example) to make ioctls on the
controlling tty cannot be resolved using userv, because userv isolates
the called program from the caller's tty. Programs like `really'
which are intended to allow already-trusted accounts (which are not
root to avoid mistakes rather than intrusion) to become root are not
appropriate for userv, because userv services do not inherit the
caller's environment.

5. URL

Anyway, userv has a WWW page:
<URL:http://www.chiark.greenend.org.uk/~ian/userv/>

userv is short for `user services', and is pronounced `you-serve'.

6. cron example

Here I will describe the overall design of a cron subsystem which has
a much smaller probability of having security bugs than a conventional
cron. Because userv is used to deal with many of the security issues,
the design can concentrate on scheduling (cron's ostensible task)
rather than security. The system is even extensible - new crontab
formats with enhanced semantics can be introduced without needing to
touch the system-provided core.

The system is split into two security areas: the user-side utilities,
and a system-provided daemon which calls back the user when the time
for a job has been reached. The daemon runs as `cron'; there is no
root component except userv.

The daemon maintains in core a list of times at which each user wanted
to be called back. It repeatedly sleeps until the next time has been
reached, and then it invokes
userv <user> cron/callback <execution-time>

The system configuration files for userv are arranged so that the
`cron' user (and no other) can run cron/callback as any user, and that
this causes the program /usr/lib/cron/services/callback to be run.
This program checks that the current time is `close enough' to the
time specified on its command line, scans the user's crontab file,
kept in ~/.userv/.servdata/cron/crontab (not in /var), and executes
each command which needs to be run that minute. It deals with mailing
the output (if any) back to the user.

How does the user update their crontab ? They call the `crontab'
command, in /usr/bin, as usual. This program stores the new crontab
in ~/.userv/.servdata/cron/crontab, and invokes
/usr/lib/cron/services/tellcron.

/usr/lib/cron/services/tellcron runs
userv cron cron/update <horizon-time>
feeding it on standard input a list of times before <horizon-time> (a
unix time in decimal) when the user wants to be woken up (this data
being derived from ~/.userv/.servdata/cron/crontab). The
<horizon-time> would be some fixed time (an hour, perhaps) from now.

The userv configuration arranges for any user to be able to run
/usr/lib/cron/services/update as cron by calling cron/update. The
update script checks that the calling user (in $ENV{'USERV_USER'}) is
allowed to use cron, checks the syntax of the input, and writes the
data (with the name of the user) to a named pipe in /var/run/cron
(some locking is required here, and formatting of the data in a way
that means that partial writes are not accepted and do not interfere
with the next update's write). It reads the pipe to wait for the cron
daemon's reply.

At system startup the cron daemon runs
userv <user> cron/tellcron
as each user who is allowed to run cron jobs. This is configured to
run /usr/lib/cron/services/tellcron.

Note that the system can easily be extended to at jobs, without
needing to change the daemon.

>From a security point of view, the worst that the cron daemon could do
would be to cause genuine cron jobs to be executed too often or not at
all, or to repeatedly request `tellcron'. The worst that a user can
do is repeatedly chop and change their list of pending times.

The data crossing the security boundary - just timestamps (with
usernames supplied by userv) - is very simple and so easy to check and
hard to misparse, and the programs can all be written in a
straightforward interpreted language. The user's crontab, whose
parsing is complicated, and which contains data which is to be
executed, stays entirely within the user's own security boundary -
noone but the user themselves ever needs to touch that data.

--
Ian Jackson, at home.           Local/personal: ijackson@chiark.greenend.org.uk
ian@chiark.greenend.org.uk         http://www.chiark.greenend.org.uk/~ijackson/

--