Shell scripts on Linux and dropping privileges

Tue Apr 2 19:22:36 UTC 2013

Shell scripts on Linux and dropping privileges

Assuming you want to make a shell script that runs as root, but after setting up stuff, drops privileges to the calling user and runs a specified command: how would one best achieve this?

In C, what you would do is somewhat like this:

setuid(getuid());
execvp(argv[1], &argv[1]);

This drops set-user-id rights, then chains to the program specified in $1 and forwards the command line arguments to it without causing any extra shell parsing.

But how to do this in a shell script?

The user ID problem

First of all: shell scripts can't be setuid. We need another mechanism to start them with root privileges. The tool of our choice shall be Sudo. This, however, changes the effective and real UID to the target UID, and stores the calling user ID in $SUDO_USER. Our first attempt then may be:

su "$SUDO_USER" -c "$*"

Avoiding argument parsing

This however fails: it causes another level of argument parsing:

[root@grawp ~]# set -- 'echo' '"' '-x'
[root@grawp ~]# "$@"
" -x
[root@grawp ~]# su rpolzer -c "$*"
zsh:1: unmatched "

So let's try to find something better...

[root@grawp ~]# su rpolzer -c "$@"

Right, -c only takes a single argument... but the argument is a full fledged shell script!

[root@grawp ~]# su rpolzer -c 'exec "$@"' sh "$@"
su: invalid option -- 'x'
[root@grawp ~]# su rpolzer -- -c 'exec "$@"' sh "$@"
" -x

It worked! Now we also want this shell to have some sensible preset variables, so we want to read .profile and such, as sudo has stripped most environment variables for security reasons... also, we totally want to avoid danger of possible option parsing. Also, what if the user's shell is not Bourne compatible and doesn't do $@? So let's do it:

[root@grawp ~]# su -s '/bin/sh' - rpolzer -- -l -c 'exec "$@"' -- "$@"
" -x

Therefore, the solution is:

su -s '/bin/sh' - "$SUDO_USER" -- -l -c 'exec "$@"' -- "$@"

And here is a full script (download here) to abstract away this mess (I call it asuser):

#!/bin/sh

user=$1
shift

case "$user" in
    ''|-*)
        echo >&2 "Bad/evil user name."
        exit 1
        ;;
esac

if [ $# -eq 0 ]; then
    exec su - "$user"
else
    exec su -s '/bin/sh' - "$user" -- -l -c 'exec "$@"' -- "$@"
fi

NOTE: $SUDO_USER still should to be checked to be sane. Especially, it should better not start with a dash... although normally its value can be assumed sane as it comes from sudo, in security critical applications this should not be taken for granted. What if someone does have a user name starting with a dash... the /etc/passwd format allows it, but sure, it would break many tools, such as sudo.

As for why one would need this:

One application

#!/bin/sh

exec ip netns "$SUDO_USER" exec asuser "$SUDO_USER" "$@"

would be a neat script to run a given command in a user's private network namespace.


Posted by OpBaI | Permanent link | File under: unix, shell