TITLE: More control and package management using package users (v0.7) LFS VERSION: All AUTHOR: Matthias S. Benkmann SYNOPSIS: -You want to know which packages your files belong to ? -You want to deinstall software that doesn't have make uninstall ? -You are bothered by programs installed setuid root behind your back ? -You don't want packages to overwrite files from other packages ? -You don't like package managers like RPM ? -YOU WANT TOTAL CONTROL USING ONLY UNIX BUILTINS ? HINT: LEGALESE: The author does not take responsibility for any damage of any kind caused directly or indirectly by applying the methods described below. There is no warranty of any kind, not even the implied warranty of fitness for the purposes mentioned in the text, on any of the programs/scripts/commands listed below and the author may not be held liable for any damage of any kind caused by the execution of said programs/scripts/commands. Proceed at your own risk! ########################################################################### Changelog ########################################################################### 2002-01-30 -added Changelog -moved "chown 0.10000 `cat /tmp/installdirs`" command up (before glibc package user is created) -add_package_user: create home directory with "mkdir -p" use $grpfile everywhere instead of /etc/group -improved mammoth sentence in Introduction -added note about possibility to have user name==group name -source bashrc_basic in bashrc_package -minor textual changes 2002-02-18 -added section "Security issues with NFS" -submitted v0.7 ########################################################################### Introduction ########################################################################### Let's say I have written a program that you would like to use. To make it easier for you I come over to install it for you. Would you give me the root account and then leave the room ? No ? Then why do you give it to complete strangers who you have never seen in your life, to install software packages pulled from some Internet server, that come with no warranty and don't even list their contents in the README, although they will happily spread them all over your system ? It is a mystery why Unix admins who wouldn't even trust their employer with more than a normal user account carelessly execute complex and incomprehensible installation scripts with full root rights. Users and groups are the basic security principle in a Unix system. They have been used successfully for a long time to control who has created a file and who is allowed to delete or change this file. But this control has only been imposed on the files of ordinary users. What a waste! I suggest to extend this control to all system files. ############################################################################# Package users ############################################################################# The basic idea of this scheme is easily explained. Every package belongs to a certain "package user". This is a special user account whose HOME directory is /usr/src/ (or wherever you keep the tarballs for the package). Usually this account has no password so that only root can su to a package user, which ensures that package users do not open an additional way into the system and undermine security. Of course you *may* set passwords anyway to allow a co-admin who you want to be able to install and maintain certain software packages to do so without having access to the actual root account. This co-admin could for instance install, delete, change additional libraries which might be necessary for his workgroup. He would be unable, though, to remove or modify libraries which don't belong to him/her, such as libc. Every package user has a primary group that reflects the purpose of the package, such as "system" for essential packages, "devel" for packages related to software development and so on. There are other useful organizational schemes, of course. You might want to have the group reflect the workgroup of the above-mentioned co-admin who installed the file for instance. Another possibility is to create a new group for every package user with the same name as the user name. That way, even if a file needs to be setuid root (and consequently has to be owned by root), the group of the file tells you about the package where it originates. In addition to the primary group, there is the secondary group which is the same for all package users. This is the "install" group. All directories that packages are allowed to install stuff in belong to the install group. This includes directories such as /bin and /usr/bin but excludes directories like /root or /. The directories owned by the install group are always group-writeable. Without further preparation this does not give added security or control because every package could replace the files from a different package (the change would be visible in the output from ls -la, though). For this reason we make all install directories sticky. The sticky attribute allows users to create new files and delete or modify their own files in the directory while preventing them from writing or deleting files from other users. ############################################################################ How to install software ############################################################################ Isn't this all very tedious ? Not if you use this script: -------------------- begin $LFS/sbin/install_package ------------------------ #!/bin/sh if [ $# -ne 3 ]; then echo USAGE: install_package description name group echo "!!Don't forget to put description in quotes if it contains spaces.!!" echo "This will create a new package user name in group. This will set up" echo "a home directory for this package user. After that, this script will" echo "automatically su to the new user so that you can begin with the" echo "installation right away. " exit 1 fi if [ $UID -ne 0 ]; then echo Please run this script as root. ; exit 1; fi add_package_user "${1}" $2 10000 20000 $3 10000 20000 || exit 1 su $2 ------------------- end $LFS/sbin/install_package ---------------------------- The above script is a shortcut for the following script, which does the actual adding of the package user in a certain range of UIDs and GIDs: -------------------- begin $LFS/sbin/add_package_user ----------------------- #!/bin/sh if [ $# -lt 7 ]; then echo 'Copyright (c) 2000 Matthias Benkmann' echo 'USAGE: ' echo 'add_package_user description name minuid maxuid group mingid maxgid [-d home]' echo echo Don\'t forget to put description in quotes. echo description must be valid for a /etc/passwd entry, that means especially echo that it must not contain \":\". echo If group doesn\'t exist, it will be added. echo group will be the user\'s main group. echo 'The user will also belong to group "install" (created if necessary).' echo 'A home directory /usr/src/name (or home if -d home specified) echo will be created if it doesn\'t exist echo 'and the files from /etc/skel-package will be copied into it (existing' echo 'target files will *not* be overwritten)' echo 'minuid (incl.) and maxuid (excl.) determine' echo 'the range of UIDs used by this script. The script will pick the first' echo 'UID greater than all UIDs from this range already in use.' echo 'If that UID is out of range (i.e. equal to maxuid), the' echo 'script will pick the first available UID in this range. If the range is' echo 'full, the script will give up. This process ensures that UIDs for packages' echo that have been removed, don\'t get reassigned unless really necessary. echo 'This avoids trouble if some files were overlooked when removing the package.' echo 'Otherwise these files might be assigned to the wrong package.' echo 'mingid (incl.) and maxgid (excl.) define' echo 'the permissible range for the GID of group if it has to be added (and ' echo 'the install group if it must be added).' exit 1 fi grpfile=/etc/group passwd=/etc/passwd homebase=/usr/src skel=/etc/skel-package description=$1 name=$2 minuid=$3 maxuid=$4 gname=$5 mingid=$6 maxgid=$7 home=$homebase/$name set -- "$@" _eNd_OF_lisT_ while [ "$1" != "_eNd_OF_lisT_" ]; do case "$1" in -d) shift 1 if [ "$1" = "_eNd_OF_lisT_" ]; then echo 1>&2 "-d directory name missing!" exit 1 fi home="$1" shift 1 ;; *) temp="$1" shift 1 set -- "$@" "$temp" ;; esac done shift 1 #throw away _eNd_OF_lisT_ if [ $UID -ne 0 ]; then echo Please run this script as root. ; exit 1; fi #test if user already exists grep "^$name:.*" $passwd if [ $? -eq 0 ]; then echo 'Package user does already exist! Do su '$name' to do maintenance work.' exit 1 fi #test if minuid, maxuid, mingid and maxgid are integers, otherwise set #to defaults. error=0 expr ${minuid} + 1 2>/dev/null 1>&2 || error=1 expr ${maxuid} + 1 2>/dev/null 1>&2 || error=1 expr ${mingid} + 1 2>/dev/null 1>&2 || error=1 expr ${maxgid} + 1 2>/dev/null 1>&2 || error=1 if [ $error -eq 1 ]; then echo Error: Illegal numeric value! exit 1 fi if [ $minuid -ge $maxuid ]; then echo 'Error: minuid must be less than maxuid !' exit 1 fi if [ $mingid -ge $maxgid ]; then echo 'Error: mingid must be less than maxgid !' exit 1 fi uidlist=`cut -d : -f 3 $passwd | sort -n` #find last used UID within range u=0 for i in $uidlist do if [ $i -ge $maxuid ]; then break; fi if [ $i -ge $minuid ]; then u=$i; fi done #if no UID from the range is used, pick the first, otherwise pick the one #immediately following the last UID in use. if [ $u -eq 0 ]; then u=$minuid; else u=`expr $u + 1`; fi #if the last UID used from the range is maxuid-1, i.e. we may #not use its successor as UID, then we look for the first unused uid #in the range. if [ $u -ge $maxuid ]; then u=$minuid for i in $uidlist do if [ $u -eq $i ]; then u=`expr $u + 1` ; fi if [ $i -ge $maxuid ]; then break; fi done if [ $u -ge $maxuid ]; then echo Error: UID range is full! exit 1 fi fi echo Will create user $name with uid: $u unset uidlist ############################################################################# # group ############################################################################# #execute the following for gname and "install" to get gids for those 2 groups g=0 creategroup=0 for group in install $gname do oldg=$g #save gid from previous run createinstall=$creategroup creategroup=0 #test if group already exists and extract gid if so g=`grep ^${group}:.\* $grpfile | cut -d : -f 3 -` #if group does not exist, then check range for a free gid if [ z$g = z ]; then creategroup=1 gidlist=`cut -d : -f 3 $grpfile | sort -n` #find last used GID within range g=0 for i in $gidlist do if [ $i -ge $maxgid ]; then break; fi if [ $i -ge $mingid ]; then g=$i; fi done #if no GID from the range is used, pick the first, otherwise pick the one #immediately following the last GID in use. if [ $g -eq 0 ]; then g=$mingid; else g=`expr $g + 1`; fi #don't reuse gid from previous run if [ $g -eq $oldg ]; then g=`expr $g + 1`; fi #if the last GID used from the range is maxgid-1, i.e. we may #not use its successor as GID, then we look for the first unused gid #in the range. if [ $g -ge $maxgid ]; then g=$mingid for i in $gidlist do if [ $g -eq $i ]; then g=`expr $g + 1` ; fi if [ $g -eq $oldg ]; then g=`expr $g + 1` ; fi if [ $i -ge $maxgid ]; then break; fi done if [ $g -ge $maxgid ]; then echo Error: GID range is full! exit 1 fi fi fi done unset gidlist if [ $createinstall -eq 1 ]; then echo Creating group install with gid $oldg groupadd -g $oldg install || exit 1 else echo Group install has gid $oldg fi if [ $creategroup -eq 1 ]; then echo Creating group $gname with gid $g groupadd -g $g $gname || exit 1 else echo Group $gname has gid $g fi useradd -c "${description}" -d ${home} -g ${gname} -G install \ -s /bin/bash -u ${u} ${name} || exit 1 mkdir -p $home || exit 1 yes n|cp -ai -R ${skel}/{[^.],.[^.],..?}* ${home} 2>/dev/null >/dev/null cd ${home} chown --recursive ${u}.${g} . exit 0 -------------------- end $LFS/sbin/add_package_user ------------------------ If you use these scripts, you can install a new package like this install_package 'Foo description' foo foogroup and the package user foo will be created and you will automatically be su'd to it and put into the package user's home directory. Now just copy the tarball from your download directory, untar and install. It's no more work than doing it as root. ############################################################################ Pitfalls and Solutions ############################################################################ There are some pitfalls when you use this system. The following situations are common: 1. A package install script is poorly written and insists on changing ownership/permissions of system directories or its own files. 2. A package install script has a legitimate reason to change ownership/permissions of files it installs. 3. Package A contains a program that is also part of package B. Package B was installed first. 4. A package tests if the user installing it is root and does not install certain programs otherwise. 5. A package creates a directory that other packages need to write to. 1-3 usually result in an "Operation not permitted" error during make install. Note that this is not actually a flaw of the package user scheme, it's a feature. It's the reason why we made the install directories sticky and use package users in the first place. We don't want stuff like this to happen without our (divine :-) intervention. Fixing these issues is usually easy: 1. Look at the output from "make install". It will contain the offending command such as "install --owner root foo /bin" right before the "Operation not permitted" error. grep through the Makefiles of the package and remove the offending switch (in this case "--owner foo"). Sometimes you may not want to remove it altogether but may need to change it to something harmless. For instance "install -m 4755 ..." which tries to set the setuid bit should be changed to "install -m 755 ...". You can automate these changes with a sed script or use a wrapper script around install (see below) to avoid having to manually change Makefiles. Note that you either have to perform these changes *after* the configure step or you have to modify the source Makefiles (usually Makefile.in or Makefile.am). 2. This is basically like 1 but in this case the package actually has a reason (that you accept) for doing what it does. This is seldom but happens for some packages that install setuid root programs that you want to have setuid root, such as ping. In this case, act like in case 1 and execute the appropriate changes manually as root afterwards, such as chown root.root /bin/ping && chmod 4755 /bin/ping. 3. In this case you have to decide which package you actually want to provide the program in question. If you want to replace package B's program with that of package A, simply delete the program manually as root. If you don't want package A to overwrite the program with its own, grep through the Makefiles for the name of the program in question (and its manpages). Usually you will find a list like PROGRAMS=foo bar fubar barfu and it is sufficient to remove the program from this list. In very rare cases you may need to remove some "install " lines from the Makefile. 4. This case is usually documented in the INSTALL file of the package and a make target to install the programs left out, such as "install-root", is provided. So this is not normally an issue. 5. This case is easy to fix by assigning the directory in question to the install group and making it group-writeable and sticky. This does not even have to be done as root. The package user who created the directory has the permissions to do it. So you can insert an appropriate line into the Makefile. Manually changing Makefiles is annoying, even if it happens only occasionally. Sed scripts do help but they have to be custom fitted to every package that needs them. There is a better solution, though. While building LFS with the package user scheme, I have noticed that although there are a lot of possible things a Makefile could contain which would trigger an "Operation not permitted" error, only few of them are actually used. So far I have only encountered these problems in connection with mkdir, chgrp, chown and install. I have written the following scripts to handle them automatically. If you make sure the directory where these scripts are located (in my case /usr/src/lfs) is the first in the PATH for every package user, you should be able to install most LFS packages without having to fix any Makefiles manually. ATTENTION! These wrapper scripts allow "make install" to complete without aborting due to "Operation not permitted" errors. However, some of the operations that are suppressed this way are legitimate (see 2. above). For that reason, the wrapper scripts output lines beginning with "***" to stderr that contain the original command that the installation wanted to execute. You *must* check these lines and if it was a legitimate operation you have to take appropriate action manually or the programs in question will not work properly. The following is a list of things you should look for and what you have to do: 1. "*** install -m 4xxx -o root" : This wants to make a binary setuid root. The important thing here is "-m 4x". Many install scripts try to use "install -m 755 -o root" or something similar. This can be ignored. The owner (specified with "-o owner") is only important for binaries with mode 4xxx (specified with "-m 4xxx"). If you decide that the binary really should be setuid root (for the LFS base packages this is usually okay) you have to do chown root.root chmod u+s 2. "*** chgrp xxx" : This wants to change the group of a binary. This is usually only done if a binary is made setgid. You will see that in the output of "ls -l" like this rwxr-sr-x 1 util-lin tty 8348 Dec 3 2000 write The "s" in the group field tells you this is a setgid binary. Right now the chgrp wrapper below only intercepts chgrp for xxx=tty because that is the only thing needed for installing LFS. If such a call has been intercepted and you want the binary to be setgid, you have to do the following: chgrp xxx chmod g+s TIP: It is important that you check the error log religiously when installing a package as a package user, especially if you use the scripts below. To make this easier, install like this make install 3>&1 1>&2 2>&3 | tee install.err This will give you the normal make output (i.e. errors and normal messages) to the screen but in addition to that it will put a copy of all error messages into install.err. You can "cat install.err" after the installation to examine error messages. Note that "make install | tee install.err" does *not* work. This would copy all normal messages *without* the errors. If you want to redirect screen output (i.e. the complete install log, including errors and non-errors) to a file, do it like this { make install 3>&1 1>&2 2>&3 | tee install.err ;} &>install.log You should probably create a shell function for this to save you typing. Here are the wrapper scripts: ------------- begin $LFS/usr/src/lfs/wrappers/mkdir -------------------------- #!/bin/sh DAISY_CHAIN="" for p in $(type -ap mkdir) ; do if [ ! $p -ef $0 ]; then DAISY_CHAIN=$p ; break ; fi done if [ ! -n "$DAISY_CHAIN" ]; then echo Cannot find real ${0##*/} command exit 1 fi if [ $UID == 0 ]; then exec $DAISY_CHAIN "$@" fi watchdir=/usr/share/locale cmdline="$@" dirs="" for((i=$#; $i>0;)) do a="$1" shift 1; i=$(($i-1)) case "$a" in $watchdir/*) dirs="$dirs ""`expr $a : "$watchdir/\(.*\)"`" set -- "$@" "$a" ;; *) set -- "$@" "$a" ;; esac done $DAISY_CHAIN "$@" || exit $? test z"$dirs" != z && echo 1>&2 '***' mkdir "$cmdline" for dir in $dirs ; do cumuldir="" for d in `echo $dirs | sed 's#/# #g' -` ; do cumuldir=$cumuldir$d/ chgrp install $watchdir/$cumuldir chmod g+w,o+t $watchdir/$cumuldir done done exit 0 ------------ end $LFS/usr/src/lfs/wrappers/mkdir -------------------------- ------------ begin $LFS/usr/src/lfs/wrappers/chgrp ------------------------ #!/bin/sh DAISY_CHAIN="" for p in $(type -ap chgrp) ; do if [ ! $p -ef $0 ]; then DAISY_CHAIN=$p ; break ; fi done if [ ! -n "$DAISY_CHAIN" ]; then echo Cannot find real ${0##*/} command exit 1 fi if [ $UID == 0 ]; then exec $DAISY_CHAIN "$@" fi if [ "$1" == "tty" ]; then echo 1>&2 '***' chgrp "$@" else $DAISY_CHAIN "$@" || exit $? fi exit 0 ------------ end $LFS/usr/src/lfs/wrappers/chgrp -------------------------- ------------ begin $LFS/usr/src/lfs/wrappers/chown ------------------------ #!/bin/sh DAISY_CHAIN="" for p in $(type -ap chown) ; do if [ ! $p -ef $0 ]; then DAISY_CHAIN=$p ; break ; fi done if [ ! -n "$DAISY_CHAIN" ]; then echo Cannot find real ${0##*/} command exit 1 fi if [ $UID == 0 ]; then exec $DAISY_CHAIN "$@" fi if [ "$1" == "root.root" ]; then echo 1>&2 '***' chown "$@" else $DAISY_CHAIN "$@" || exit $? fi exit 0 -------------- end $LFS/usr/src/lfs/wrappers/chown ------------------------ ------------ begin $LFS/usr/src/lfs/wrappers/install ---------------------- #!/bin/sh localedir=/usr/share/locale cmdline="$@" manpagesowner=man-pages DAISY_CHAIN="" for p in $(type -ap install) ; do if [ ! $p -ef $0 ]; then DAISY_CHAIN=$p ; break ; fi done if [ ! -n "$DAISY_CHAIN" ]; then echo Cannot find real ${0##*/} command exit 1 fi if [ $UID == 0 ]; then exec $DAISY_CHAIN "$@" fi #********** test if we create directories ******************** if [ \( z"$1" = z"-d" \) -o \( z"$1" = z"-m" -a z"$3" = z"-d" \) ]; then locdirs="" notify=0 havedir=0 for((i=$#; $i>0; )) do a="$1" shift 1; i=$(($i-1)) case "$a" in -o|-g|--owner|--group) notify=1 shift 1; i=$(($i-1)) set -- "$@" ;; $localedir/*) if [ ! -d "$a" ]; then locdirs="$locdirs ""`expr $a : "$localedir/\(.*\)"`" set -- "$@" "$a" havedir=1 else notify=1 set -- "$@" fi ;; */*|/sbin) if [ ! -d "$a" ]; then set -- "$@" "$a" havedir=1 else notify=1 set -- "$@" fi ;; *) set -- "$@" "$a" ;; esac done test $notify -eq 1 -o z"$locdirs" != z && \ echo 1>&2 '***' install "$cmdline" test $havedir -eq 0 && exit 0 $DAISY_CHAIN "$@" || exit $? test z"$locdirs" != z && for dir in $locdirs ; do cumuldir="" for d in `echo $locdirs | sed 's#/# #g' -` ; do cumuldir=$cumuldir$d/ if [ -d $localedir/$cumuldir ]; then chgrp install $localedir/$cumuldir chmod g+w,o+t $localedir/$cumuldir fi done done else #if "$1" != "-d" ,i.e. we do not create directories ***************** notify=0 for((i=$# ; $i>0; )) do a="$1" shift 1; i=$(($i-1)) case "$a" in -m) set -- "$@" "$a" a="$1" shift 1; i=$(($i-1)) case "$a" in 4755) notify=1 ; set -- "$@" "755" ;; *) set -- "$@" "$a" ;; esac ;; -m4755) notify=1 ; set -- "$@" "-m755" ;; -o|-g|--owner|--group) notify=1 shift 1; i=$(($i-1)) set -- "$@" ;; */man/man?/*) if [ -e "$a" -a ! -O "$a" ]; then if [ `find "$a" -printf \%u` = $manpagesowner ]; then notify=1 set -- "$@" not_installed else set -- "$@" "$a" fi else set -- "$@" "$a" fi ;; *) set -- "$@" "$a" ;; esac done test $notify -eq 1 && echo 1>&2 '***' install "$cmdline" $DAISY_CHAIN "$@" || exit $? fi exit 0 ----------- end $LFS/usr/src/lfs/wrappers/install -------------------------- If you improve any of these scripts, I would be grateful if you could mail me your changes together with a note saying which package prompted the change. Note that these scripts are supposed to have minimal intrusiveness. So writing a common case "-m*)" instead of the case "-m4755" is undesirable. The scripts should only deal with the problems that have actually been encountered, not all possible problems. ############################################################################# LFS specifics ############################################################################# For the user name of the package users I always use the name of the package without the version number, including dashes and possibly exceeding 8 characters in length, e.g. "util-linux". Aside from ls -l which chops off the last characters of the user name I have not noticed any problems with this. The other programs I have tested seem to deal with these user names just fine. If you encounter problems, please report them to me. Now how do we apply the above to the building of an LFS system ? ########################################################################## Pre-chroot phase ########################################################################## We don't use package users in the pre-chroot phase. This does not mean we install as root. I suggest building the pre-chroot system as a normal user. That way, you can later distinguish files created inside chroot (these have uid 0 or the uid of a package user) and remaining files created during the pre-chroot phase (these carry the uid of your normal user account on your host system). Building as an ordinary user also protects you against typos that might otherwise cause a package to overwrite files on your host system. The actual commands of the pre-chroot phase don't need be changed but you have to do some additional things. The first problem is that we need find right after installing glibc inside chroot. The best thing to do is to build a static version during the pre-chroot phase like this: ./configure --prefix=$LFS/usr --disable-nls && make LDFLAGS=-static CPPFLAGS=-Dre_max_failures=re_max_f2 && make libexecdir=$LFS/usr/bin install Another problem is that some programs such as chown can not resolve usernames before glibc is installed. Fortunately most programs accept a numeric UID or GID. There is one nasty exception, though: su. The most fundamental program when using package users doesn't swallow numeric ids :-| Now how do we become a package user before glibc is installed ? If you guessed that I have a written a script to replace it, you guessed ... ... wrong :-) I have written a C program to replace it. It just happens that sh-utils contains an su program which is not used in LFS. It gets installed statically linked during the pre-chroot phase, is never used there (assuming a standard LFS build), gets replaced with a dynamically linked version when sh-utils is reinstalled and then is finally replaced by the su from shadow which is the version we will use when the LFS system is finished. So all you need to do is to replace the file src/su.c in the sh-utils source tree with the following file before compiling your chapter 5 sh-utils. Note that if you compile chapter 5 as non-root, you need to issue the command `make install-root' after `make install' or sh-utils won't install su. -------- begin $LFS/usr/src/sh-utils/sh-utils-/src/su.c ------------ #include #include #include #include #include #include #include #include #define NUMGIDS 1024 int main(int argc,char* argv[]) { char* Res; char Buffy[4096]; uid_t tuid=-1,uid; gid_t gid; int i; FILE* File; char* command=NULL; char* shell; char* HOME; gid_t gid_list[NUMGIDS]; ssize_t NumGids=0; if (argc>1) { if (strcmp(argv[1],"--help")==0) { fprintf(stdout,"There is no help!\n"); exit(0);} if (strcmp(argv[1],"--version")==0) {fprintf(stdout,"0.6\n"); exit(0); } } if ((argc==4) && (strcmp(argv[2],"-c")==0)) command=argv[3]; else if (argc!=2) { fprintf(stdout,"USAGE: su username|uid [-c command]\n"); return 1; }; i=0; while(isdigit(argv[1][i])) ++i; if (argv[1][i]==0) tuid=atol(argv[1]); File=fopen("/etc/passwd","rb"); if (File==NULL) {perror("/etc/passwd"); return 1;}; while(1) { errno=0; Res=fgets(Buffy,1024,File); if (Res==NULL) { if (errno!=0) perror("/etc/passwd"); else fprintf(stderr,"su: User not found!\n"); return 1; }; Res=strtok(Buffy,":"); if (Res==NULL) continue; strtok(NULL,":"); uid=atol(strtok(NULL,":")); gid=atol(strtok(NULL,":")); strtok(NULL,":"); HOME=strtok(NULL,":"); shell=strtok(NULL,":"); if (tuid==uid) {argv[1]=strdup(Buffy); break;} if (strcmp(argv[1],Buffy)==0) break; }; HOME=strdup(HOME); shell=strdup(shell); File=fopen("/etc/group","rb"); if (File==NULL) {perror("/etc/group"); return 1;}; while(1) { ContinueReadingEtcGroup: errno=0; Res=fgets(Buffy,1024,File); if (Res==NULL) { if (errno!=0) {perror("/etc/group"); return 1;} else break; }; Res=strtok(Buffy,":,\n"); if (Res==NULL) continue; strtok(NULL,":,\n"); gid_list[NumGids]=atol(strtok(NULL,":,\n")); Res=strtok(NULL,":,\n"); while(Res!=NULL) { if (strcmp(Res,argv[1])==0) { ++NumGids; if (NumGids>=NUMGIDS) goto SetNewIdentity; else goto ContinueReadingEtcGroup; }; Res=strtok(NULL,":,\n"); }; }; SetNewIdentity: if (command==NULL) command=shell; setenv("HOME",HOME,1); setgroups(NumGids,gid_list); setgid(gid); setuid(uid); errno=0; i=system(command); if (((i<0) || (i==127)) && (errno!=0)) {perror("/bin/sh"); return 1;}; return i; }; -------- end $LFS/usr/src/sh-utils/sh-utils-/src/su.c ------------ This su program accepts user names as well as numeric UIDs. It does its own name resolution using /etc/passwd so it works even without glibc being installed. Note that sh-utils installs the su program with the setuid bit set. If you install the pre-chroot system as an ordinary user (which you should for safety reasons), this will result in a su that doesn't work even when executed by root. Simply remove the setuid bit like this chmod u-s $LFS/bin/su and su will work fine once we have entered the chroot environment (where we work as root). Note that this allows you to su from root to a package user but not the other way around. If you want to be able to su from a package user to root you will have to make su setuid root (i.e. chown root.root $LFS/bin/su && chmod u+s $LFS/bin/su). Make sure that you don't keep such an su lying around on a system that others have access to. The above su does not check for a password, so if you make it setuid root, everyone can use it to become root without a password! ########################################################################## Chroot phase - preparing the LFS system: ########################################################################## This is the tricky part. Once we have entered chroot, we have to assign install directories to the install group and make them group-writeable. However, since glibc is not yet installed, we can't use user names, yet. Furthermore, groupadd and useradd which are part of the shadow package are not installed, yet. This means that we can't create any package users, at least not without editing /etc/group and /etc/passwd manually. But don't despair. The following scripts make nice replacements for useradd and groupadd as long as shadow isn't there. Note that they only accept the exact syntax with which the useradd and groupadd commands are used in the add_package_user script. I assume you will be using the install_package/add_package_user scripts to add package users to the system. Here are the scripts: --------------------- begin $LFS/usr/sbin/useradd ---------------------------- #!/bin/sh if [ $# -ne 13 -o z$1 != z-c -o z$3 != z-d -o z$5 != z-g -o z$7 != z-G -o z$9 != z-s -o z${11} != z-u ]; then echo 1>&2 USAGE: useradd -c description -d home -g maingroup -G addgroup -s shell -u uid login exit 1 fi #test if user already exists grep "^${13}:.*" /etc/passwd if [ $? -eq 0 ]; then echo 1>&2 $0: User does already exist exit 1 fi g=`grep ^${6}:.\* /etc/group | cut -d : -f 3 -` if [ z${g} = z ]; then echo 1>&2 $0: Group ${6} does not exist! exit 1 fi grep ^${8}:.\* /etc/group >/dev/null || \ { echo 1>&2 $0: Group ${8} does not exist! exit 1 } cp /etc/passwd /tmp/passwd123456 echo "${13}:x:${12}:$g:$2:$4:/bin/bash" \ | sort -t : -k3,3n -m /tmp/passwd123456 - > /etc/passwd cp /etc/group /tmp/group123456 sed -e 's/^\('"${8}"':[^:]*:[0-9]*:..*\)$/\1,'"${13}"'/' \ -e 's/^\('"${8}"':[^:]*:[0-9]*\):$/\1:'"${13}"'/' \ /tmp/group123456 >/etc/group ---------------------- end $LFS/usr/sbin/useradd ---------------------------- --------------------- begin $LFS/usr/sbin/groupadd -------------------------- #!/bin/sh if [ $# -ne 3 -o z$1 != z-g ]; then echo 1>&2 USAGE: groupadd -g gid groupname exit 1 fi #test if group already exists grep "^${3}:.*" /etc/group if [ $? -eq 0 ]; then echo 1>&2 $0: Group does already exist exit 1 fi cp /etc/group /tmp/group123456 echo ${3}:x:${2}: | sort -t : -k3,3n -m /tmp/group123456 - > /etc/group --------------------- end $LFS/usr/sbin/groupadd ---------------------------- These scripts overcome the problem of the missing shadow utilities. With the above scripts and su program we now have everything we need to begin our chroot phase. ########################################################################### Tips before you chroot ########################################################################### Whenever add_package_user creates a new package user it copies the files from /etc/skel-package (if you create this directory) to the new package user's home directory. Symlinks are copied as symlinks. A useful symlink to place in /etc/skel-package is one like .bashrc -> /etc/bashrc_package with something like the following: ------------------ begin $LFS/etc/bashrc_package -------------------------- #first load basic configuration to make system work normal source /etc/bashrc_basic export PATH=/usr/src/lfs/wrappers:$PATH #make prompt reflect that we are a package user. Do it via USER_PROMPT_COMMAND #rather than PROMPT_COMMAND because we want to keep root's prompt if we su to root export USER_PROMPT_COMMAND='PS1="package \u:"`pwd`"> "' alias mafobu='make -f /usr/src/lfs/mafobu' #The following command will put us in the home directory. cd ------------------ end $LFS/etc/bashrc_package ---------------------------- ------------------ begin $LFS/etc/bashrc_basic ---------------------------- #This file should be sourced by all users' .bashrc files if [ $UID -eq 0 ]; then export PATH=/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin else export PATH=/usr/local/bin:/bin:/usr/bin fi #set a red prompt when user is root. If a user wants to override this #s/he should use USER_PROMPT_COMMAND instead of PROMPT_COMMAND in order to #keep #root's prompt when su'ed to root. If a user wants to change PS1 directly #s/he should do "unset USER_PROMPT_COMMAND". export PROMPT_COMMAND='if [ $UID -eq 0 ]; then \ PS1="\[\033[0;31m\]root@\h:"`pwd -P`"# \[\033[0m\]" ; \ else eval $USER_PROMPT_COMMAND ; fi' #make PROMPT_COMMAND read-only to protect against careless users declare -r PROMPT_COMMAND #set a reasonable default USER_PROMPT_COMMAND export USER_PROMPT_COMMAND='PS1="\u@\h:"`pwd`"> "' #make keys (del, bs, home, end,...) work normal export INPUTRC=/etc/inputrc bind -f $INPUTRC ------------------ end $LFS/etc/bashrc_basic ------------------------------ Another useful file to place in /etc/skel-package is a .project file like this: ------------------ begin $LFS/etc/skel-package/.project ------------------ DESCRIPTION: bogus package CONTENTS: foo,bar,fubar LAST UPDATED: 30 Feb 2042 DOWNLOAD LOCATION: ftp://ftp.gnu.org/gnu/foo/ WEB SITE: INSTALL NOTES: GENERAL NOTES: ----------------- end $LFS/etc/skel-package/.project ------------------ Update this file whenever you re/install a package. It is called .project so that it is automatically displayed when you issue the command "finger " or "pinky -l ". ########################################################################### Inside the chroot environment ########################################################################### After you have chroot'ed do groupadd -g 10000 install which will create the install group. Now use chown 0.10000 `cat /tmp/installdirs` chmod ug=rwx,o=rx `cat /tmp/installdirs` to assign directories to the install group. Note that we do not make the directories sticky, yet. This is because they still contain files belonging to root or some unknown user (if you installed pre-chroot as non-root). These files must be overwriteable. The file /tmp/installdirs is the following list: ----------------- begin $LFS/tmp/installdirs ------------------------------ /usr/bin /usr/etc /usr/sbin /usr/include /usr/lib /usr/man/man? /usr/doc /usr/info /usr/local/man/man? /usr/local/doc /usr/share /usr/share/dict /usr/share/doc /usr/share/info /usr/share/locale /usr/share/man/man1 /usr/share/man/man2 /usr/share/man/man3 /usr/share/man/man4 /usr/share/man/man5 /usr/share/man/man6 /usr/share/man/man7 /usr/share/man/man8 /usr/share/nls /usr/share/misc /usr/share/terminfo /usr/share/zoneinfo /usr/share/i18n /usr/share/aclocal /usr/local/bin /usr/local/etc /usr/local/include /usr/local/lib /usr/local/sbin /usr/local/share /usr/local/share/dict /usr/local/share/doc /usr/local/share/info /usr/local/share/locale /usr/local/share/man/man1 /usr/local/share/man/man2 /usr/local/share/man/man3 /usr/local/share/man/man4 /usr/local/share/man/man5 /usr/local/share/man/man6 /usr/local/share/man/man7 /usr/local/share/man/man8 /usr/local/share/nls /usr/local/share/misc /usr/local/share/terminfo /usr/local/share/zoneinfo /etc /sbin /bin /lib ----------------- end $LFS/tmp/installdirs -------------------------------- Now do chown 0.10000 /usr/share/info/dir chmod ug=rw,o=r /usr/share/info/dir which makes sure packages can install their info pages. ######################################################################### Chroot phase -installing the packages ######################################################################### Now we can finally install glibc. Create /dev/null as root by doing mknod -m 0666 /dev/null c 1 3 then do install_package "GNU C library" glibc system which will create the system group and a user glibc and will also su to the new glibc package user. Note that the mkdir and install wrapper don't work properly during the installation of glibc because they use the name "install" rather than the gid. This means we have to do the following *after* installing glibc: find /usr/share/locale/* -type d -user glibc -exec chmod ug=rwx,o=rxt \{\} \;\ -exec chgrp install \{\} \; Now do exit to become root and begin installing the rest of the packages. Use the command install_package to create and become the package user. Then unpack in that user's home directory and use the guidelines below and the book's directions to install each package. ############################################################################ Group guidelines ############################################################################ I recommend the following groups for use with package users: devel: development related stuff, e.g. compilers. This is not restricted to software development. TeX for instance would belong in this group. utils: Most software fits into this category, even somewhat essential software like grep or text editors. net: network related stuff such as an ftp daemon or a web browser. This group overlaps with other groups to a large extent. It should be used in preference of the other groups whenever a package is clearly focused towards Internet, LAN, WWW,... A utility like wget for instance would go in net rather than utils. Exceptions from this rule are the groups docs, addons and games. If a package fits into one of those groups, use the respective group instead of net. docs: Documentation related packages, such as a tarball with Linux howtos. Note that software to create documentation such as XML processors should probably go in devel and software to view or post-process documentation such as man or groff should probably go in utils. system: important system software, such as bash. This group should be used only for really essential packages. Most packages you would put in this group are better put into "utils". Vi for instance belongs in utils. It is unlikely that any package not part of basic LFS belongs in the system group. libs: What utils is for executables, libs is for libraries. Libraries that are not strongly related to any of the other categories should go here, such as zlib or libpng. Essential system libraries such as glibc, ncurses or gettext should go in system instead. The libs group is also used for run-time environments such as the Java Virtual Machine, dosemu and wine. Other emulators like MAME for instance should probably go into games instead. games: what do you expect ;-) apps: Applications such as spreadsheets and word processors (not text editors) but also CAD software and graphics software such as Gimp. The apps group is a bit like utils, but apps are usually more user friendly and more streamlined and focused than utils. Apps feel less geekish and nerdish than utils. Emacs for instance belongs in utils, not in apps. addons: plugins, filters and similar that are meant to be used in conjunction with another package. x: software that relates to the X Window System in general and does not fit into any of the other categories, such as the X server itself or window managers. Most X software should be put into other more specific groups. A game like xmines would go in games for instance and a text editor for X would go in utils. kde: Software that relates to KDE and does not fit into any other category. This group should be used with care. Do *not* use it for all KDE software. K Office for instance belongs in apps. Konqueror belongs in net. gnome: Software that relates to GNOME and does not fit into any other category. This group should be used with care. Do *not* use it for all GNOME software. Gimp for instance belongs in apps. A GNOME-aware window manager that works with plain X should go in the x group. The following is a list of the LFS packages with the user.group assignments I think are appropriate. The user name in general should be the name of the main tarball without extensions and version. The groups try to follow the guidelines above but sometimes it is hard to find the "correct" group. The list also contains some notes I made when I installed them. If you use the mkdir, chgrp and install wrappers, you should not have any trouble. glibc.system: see above. MAKEDEV: not installed as a package user man-pages.docs findutils.utils mawk.devel: rm /usr/bin/mawk /usr/share/man/man1/mawk.1 as root before `make install' ncurses.system vim.utils ed.utils gcc.devel: Before `make install' assign the /usr/lib/gcc-lib and /usr/include/g++ to the gcc package user by doing (as root) chown -R gcc. /usr/lib/gcc-lib /usr/include/g++ bison.devel less.utils groff.utils man.utils perl.utils m4.devel texinfo.utils autoconf.devel automake.devel: Creates the directory /usr/share/aclocal. This directory is written to by other packages, so chgrp install and chmod g+w,o+t it. bash.system: When ./configure is done as a package user (more precisely when done in a su environment), it will fail to detect /dev/stdin. This will cause bash to compile an emulation of /dev/std* and /dev/fd/*. I actually prefer this emulation because it matches the description in the manpage while the normal behaviour (i.e. to open the respective device) does not always. If a normal bash is desired, use sed to change configure and replace "test -r /dev/stdin" with "test -e /dev/stdin" (same for /dev/fd). [ This information is for bash 2.05, I reported the issue so it may be fixed in future versions. ] flex.devel file.utils libtool.devel bin86.devel binutils.devel bzip2.utils gettext.system kbd.system diffutils.utils e2fsprogs.utils fileutils.system grep.utils gzip.utils lilo.system make.devel: make wants to be setgid kmem in order for the -l option to work. If you are not a developer who knows what this option does, you don't need it and should not give make special privileges. linux kernel: not installed as package user modutils.system: The modutils check if modules are owned by root before using them. This is the reason why the linux kernel should be compiled and installed as root, rather than as a package user. netkit-base.net patch.utils procinfo.utils procps.utils psmisc.utils sed.utils sh-utils.system net-tools.net shadow.system: This package installs quite a few setuid root progs. If you're not religious about package users you might want to install this one as root.root to make life easier for yourself. If you do install as package user don't forget to make the binaries in question setuid root manually afterwards. After installing shadow make sure that the useradd and groupadd scripts have been properly replaced. Execute find / -maxdepth 3 -name groupadd to make sure that you don't have 2 copies (e.g. one in /sbin and one in /usr/sbin) sysklogd.system This package has /usr/bin/install hardwired, so install with make INSTALL=install install to make it use the wrapper. sysvinit.system: This one is a bit tricky. You have to create /dev/initctl manually as root *before* doing "make install" because a package user does not have permission to write to /dev. So do (as root!!) rm -f /dev/initctl mknod -m 600 /dev/initctl p tar.utils textutils.utils util-linux.system: Don't forget to make mount and umount setuid root manually after installing this package. And the write program wants to be setgid tty. ########################################################################### Final steps ########################################################################### After installing everything, make the install directories sticky: chmod ug=rwx,o=rxt `cat /tmp/installdirs` If you installed pre-chroot as a normal user there are now some directories owned by that user even though they should be owned by a package user. This happens because the package users in question did not/could not recreate these directories. The following script will fix that. It works by looking for directories not owned by a registered user (-nouser option). The list of these directories is processed in a for loop. For each directory find is then used to look for all files owned by a registered user and to list those users. With "sort -u" (unique) we get a list of registered users that have files in the directory in question and wc tells us how many of these users there are. If there is only one such user (i.e. all files in the directory belong either to an unregistered user or the one registered user we identified) then we assign the directory to this user. ----------------- begin $LFS/usr/src/lfs/fixowner ------------------------- #!/bin/sh #find all directories owned by an unknown user (i.e. the normal user #account you installed pre-chroot with, if you installed as a normal #user) and assign those that have an unambiguous intended owner (i.e. #only files belonging to a single owner aside from the unknown one #are found in the directory) to that owner. get_dir_owners() { owners=`find $1 -not -nouser -printf "%u\n" | sort -u ` num=`echo $owners | wc -w | tr -d " "` } for dir in `find / -path "/proc" -prune -or -type d -nouser -print` do get_dir_owners $dir if [ $num == 1 ]; then chown `echo $owners | tr -d " "`. $dir chmod u=rwx,go=rx $dir echo "Assigning $dir to $owners" fi done ----------------- end $LFS/usr/src/lfs/fixowner --------------------------- Finally you should use find to find files that still belong to the user you installed pre-chroot with. You can also use find to identify any directories that are group-writeable and not sticky. You should also check for setuid and setgid programs that don't belong to root. Apply your common sense to deal with these files (or send me a mail so I can mention the issue here). find / -path "/proc/*" -prune -or -perm +u+s -printf "%p: suid %u\n" find / -path "/proc/*" -prune -or -perm +g+s -printf "%p: sgid %g\n" find / -path "/proc/*" -prune -or \ -type d -perm +o+w -not -perm +o+t -printf "%p: world-writeable\n" find / -path "/proc/*" -prune -or -type d -perm +g+w -not -perm +o+w \ -not -perm +o+t -printf "%p: group-writeable\n" find / -path "/proc/*" -prune -or \ -not -type d -not -type l -perm +o+w -printf "%p: world-writeable\n" find / -path "/proc/*" -prune -or -path "/dev/*" -prune -or \ -not -type d -not -type l -perm +g+w -not -perm +o+w \ -printf "%p: group-writeable\n" find / -path "/proc/*" -prune -or -nouser -printf "%p: unknown user: %u\n" find / -path "/proc/*" -prune -or -nogroup -printf "%p: unknown group: %g\n" ########################################################################### Security issues with NFS ########################################################################### If you use the network filesystem NFS, there are some things you need to look out for when using the package user system described in this hint. A fundamental security problem with NFS is that it blindly trusts the uid and gid of the client. If an attacker can get access to the root account on a system in your network that is allowed to mount NFS shares from your server, or if the attacker can attach his own computer to your network, then this attacker can pretend to be anyone. NFS will happily allow the attacker to work in the NFS exported directory as any user he wants to be. The only exception is the root account. By default NFS exports directories with the root_squash option that maps all incoming requests from uid 0 to anonuid (65534 unless set in /etc/exports) and gid 0 to anongid (65534 unless set in /etc/exports). This protects files owned by root.root. On a normal system this includes most files in /bin, /etc, /lib and most other directories except /home. If you use the package user scheme, however, most of these files are owned by package users. These files are not protected by the root_squash option. In order to make NFS exports secure, you have to add the option "all_squash" to every entry in /etc/exports that exports a directory that contains files owned by package users. Note that all_squash is always a good idea because even systems that don't use package users usually have some programs owned by other users such as daemon or bin and other groups such as tty, because they need to be setuid or setgid.