Skip to content

Latest commit

 

History

History
144 lines (87 loc) · 16.9 KB

File metadata and controls

144 lines (87 loc) · 16.9 KB

Functions performed by netlogind

The basic task of a daemon providing logins is to execute one or more commands in the correct context for a user, for that system. This is harder than it seems, given historical requirements derived from terminal logins, and differences in process attributes and credentials between platforms. Different steps must be precisely ordered to ensure correct set-up.

We describe here the steps performed by netlogind, the APIs invoked, and the ordering constraints between the calls.

Most basic steps to create a process running as a given user

struct passwd pw; //< the user
setgid(pw.pw_gid);
initgroups(pw.pw_name, pw.pw_gid);
setuid(pw.pw_uid);

(Error checking should be done.) In addition, for highly security-critical calls such as setuid, call getuid and geteuid afterwards to assert that the correct credentials were set. Continuing to execute code under the wrong uid is the worst disaster of all. setuid resets the saved-set-userid on all platforms where this is supported.

This does launch a process "as a given user" in a very rudimentary sense, but on most platforms, the resulting context is still very different from that obtained by the normal, platform-specific, way of obtaining a logon. In particular, for launching a shell or general user session, this is not sufficient.

PAM

PAM is an API allowing system administrators to configure how applications perform authentication and launch user processes. PAM is widely deployed.

PAM is used to set up session environment through the pam_setcred and pam_open_session functions. There are many issues with calling these functions portably, and constraints on the order.

  • They must be called from the same thread of execution as pam_authenticate if that was used to perform authentication. Some modules work by collecting credentials during the authentication conversation, and performing an action with them during the session phase (eg pam_mount). In particular, PAM modules that use pam_set_data internally will not work if pam_setcred/open_session is called from a different process to pam_authenticate (for example, some versions of pam_afs or pam_krb5).
  • They must be called as root.
  • They must be called after initgroups, as they may be used to set up extra group memberships.
  • There is debate over which order pam_setcred and pam_open_session should be called in. It seems preferable to invoke pam_setcred before pam_open_session on most modern platforms, as some modules reasonably require this.(*) However, there are reasons for wanting to order it the other way.(*) Regardless, the strictest constraint is that Solaris and HP-UX PAM will fail with certain modules unless pam_setcred comes second, so there is no much choice on those platforms (that is, you actually have to follow the order documented on those platforms). LinuxPAM's documentation says that pam_setcred should come first, the opposite to OpenPAM's documentation.
  • Ordering of pam_open_session/setcred relative to forking: fork with care between calling the PAM functions and setuid. If calling fork, after pam_open_session and before setuid, guard the fork with a setresuid(uid,-1,-1) (or similar) just before the fork and restore the privileged uid just after. This is because pam_limits applies resource limits to the calling process based on the real uid. If the target user has a limit applied on the number of processes, but root is already running more than the user is allowed to, the fork will fail because the user's limit is being tested against root's process count.
  • PAM bugs to be aware of: some vendor-supplied modules, eg on HP-UX, do not pass the appdata parameter to the conversation function. For portability, use a static variable instead to avoid relying on the appdata parameter. Other notable real-world compatibility issues: RedHat #126985, RedHat #127054
  • The PAM_TTY issue on Sun: (eg OpenSSH #687, thread). My understanding of the solution is that PAM_TTY should be exposed as a parameter on the relevant systems so users have the power to enable the workaround if they need to. It is definitely required for PAM_TTY to be set to a string beginning with "/dev/" on some versions of Solaris, including Solaris 10 in my testing. On Linux, the workaround is only needed to avoid problems in specific modules (eg. pam_time).
  • Very nasty issues with pam_setcred(DELETE_CRED) on HP-UX and Solaris, where pam_unix uses the uid of the process, rather than the PAM_USER field. Workaround is to seteuid for that call. HP-UX still spews an unnecessary message in this case about it not being able to delete the user's credentials. All these have specific error messages that can be googled, sadly.
  • There are ruid restrictions on pam_chauthtok (AIX requires ruid of 0 on old versions, but matches Solaris behaviour on 5.2+, Solaris requires ruid non-zero or else complexity restrictions are not checked, nor is the user prompted for his old password).

Setting up the execution environment for a user process

closefrom

Close all fds before exec'ing the user's command. Whether this should be done is debated, because it kills many implementations of posix_trace (for example). Although sometimes listed as one of the steps for daemonizing a process, it's a very paranoid thing to do. It's more justifiable to do though when creating a user session.

Platforms: native on Solaris, FreeBSD. Otherwise, emulate using fds listed in proc if available. On no account naively try to close up to getrlimit(RLIMIT_NOFILE) or similar, as this can be far too large a number to loop up to.

Call at: any time

See also:

setlogin

Invoke setlogin(pw.pw_name) to ensure that the session has the correct name associated with it.

Call: Right after a setsid; absolutely not from the same session the daemon is running in. Call as root.

Platforms: FreeBSD, Mac OS X. Because one uid may have several entries in the password database with different names, getpwuid(getuid()) mightn't tell you the username that was used to log on, so another function, getlogin, has to be provided to do this. The implementation may be done in terms of utmp (unreliable), or $LOGNAME (insecure). BSD-derived systems solve the problem in the ideal way by storing a username in the per-session kernel data structure. AIX solves this using usrinfo (below)

AIX: usrinfo, setpcred

On AIX, call usrinfo(SETUINFO, "LOGIN=<name>\0LOGNAME=<name>\0NAME=<name>\0\0", ...). This is similar in function to setlogin on BSD-derived systems. Call as root. Some applications apparently require TERM to be set too, but there may be no reasonable value to give it.

Use setpcred(pw.pw_name, NULL) to set up process limits and all process credentials correctly from the credentials in the user database. Use the second parameter to override specific credentials, for example, passing { "REAL_USER=root", 0 } instead of NULL for the second parameter overrides setting the uid only, which can be done later with setuid().

Environment variables

$USER, $HOME, $PATH, $LOGNAME, $SHELL, $LOGIN (legacy, AIX)

Optional: $MAIL, $TZ

Defaults may be in /etc/environment. Remember to read the variables set through PAM with pam_getenvlist, since some modules set crucial variables (eg. $KRB5CCACHE), and certain other authentication methods (eg. GSSAPI) may also define variables for the child to use ($KRB5CCACHE again being the main one).

The manpage for Solaris login explains that it does not allow certain variables to be set through PAM: $SHELL, $HOME, $LOGNAME, $MAIL, $CDPATH, $IFS, and $PATH. This is probably sensible, and many other implementations have adopted this. Also, all variables beginning with "LD_" are blocked in this and other implementations (including Mac OS X's login).

Linux: cgroups

On systems using cgroups, the user's processes may need to be placed into separate control groups from the daemon.

The API is not easy to use, and it is not clear how this should be done in the general case. If the daemon is being run from systemd, stack the pam_systemd module to perform the correct initialisation. Otherwise, ignore the whole mess.

SELinux

Setting the SELinux context of the child process is best done through PAM on Linux systems. It usually is achieved through setexeccon(), which does not alter the parent process's context, but sets it up to be applied on the next exec(). The complication is the the session functionality of some PAM modules is meant to be called under the user's SELinux context, but not for other modules. This requires very careful configuration of the PAM stack. In fact, pam_selinux has 'open' and 'close' arguments as a hack to allow its order in the stack to be different when pam_session_open and pam_session_close are called, precisely because the order is so delicate.

Very few applications should therefore try to set the SELinux themselves, and pam_selinux is the more recent way to do this, so applications that were prototyped to use SELinux directly in the early days are now moving to PAM. OpenSSH uses SELinux still because it needs to set the security properties on the tty it creates.

See further:

Mach namespace

Work In Progress Needs research! How to launch a GUI session still not worked out.

On Mac OS X, procesess ("tasks") have associated ports, which are similar in some ways to datagram pipes between processes, but operating on a rather different model. A new process does not inherit its parent's ports, except for a few ports associated with special fields. This includes the exception port and bootstrap port. The exception port should be reset so that the Apple crash handler receives core dumps of user processes (this is turned off for daemons). The bootstrap port needs to be carefully set. The bootstrap namespace needs to be carefully set, to associate the process with the correct context.

These functions may be done directly by an application (Screen Sharing) or through PAM (pam_launchd).

Audit userid

On some kernels, processes maintain an auid, an additional userid which is preserved when the user switches userid using su(1), for example. This permits actions taken to be logged and traced to the user who performed it.

On Linux: The auid is typically set using pam_loginid. But, to guarantee it is set even when PAM is not configured correctly, a daemon should write the user's uid to /proc/self/loginuid before exec'ing the user's session. The point is that whether or not the sysadmin remembers to add the module to the service's configuration, the kernel still has the field in its process entry, so setting it is not optional. A daemon must attempt to initialise ever uid for the processes it is launching. PAM can be then used to configure the disposition of the service on error, and to interact with user-space components: while the daemon may be lenient, pam_loginuid may block the login if, for example, the system administrator wishes to require the user-space auditd to be running.

See further: "The Linux Audit System, or Who Changed That File?", Rainer Wichmann

On Solaris, Mac OS X, and FreeBSD: The kernel also assigns an auid to processes. It should be set through the BSM audit API (see setaudit_addr). The API has some differences on different platforms:

  • Very old platforms, pre-IPv6, use setaudit, a narrower variant of the API.
  • Solaris's auditinfo_addr_t has no ai_flags field. Be aware that setting the four fields in the Solaris documentation will not completely initialise the structure in the OpenBSM implementation. For portability, call getaddr_info and then modify the relevant fields.
  • Some fields are not relevant for general applications, such as the terminal id. Set this to AU_IPv4 and 0.0.0.0.
  • On Mac OS X, the kernel will give you a uniquely-generated session id if the ai_asid field is set to AU_ASSIGN_ASID. On other platforms, generate one yourself to create a new audit session (eg. getpid() or getsid()).
  • If the audit subsystem is not present, Solaris returns EINVAL to BSM calls; on the systems using OpenBSM, it's ENOSYS as expected.

Solaris: contract(4)

Work in progress

Create a new contract for processes launched from a daemon. Otherwise, critical events in one user's session could result in all users' sessions being killed.

See example: "Creating subprocesses in new contracts on Solaris 10", Floris Bruynooghe

Solaris: project(4)

On Solaris, projects are used to set resource limits for groups of processes. A user session spawned from a daemon should have its project changed from the system project to that of the user.

The usual way to accomplish this is with PAM. Sun's pam_unix_cred puts the session process into the user's default project; alternatively, pam_user_project could be used to create and manage per-user projects on-the-fly.

Therefore, the theory behind netlogind's implementation is that actually performing the project initialisation is best left to the admin's preferred configuration. A daemon need do no more than check the user is allowed to run processes in the project, after PAM session management is complete. This just avoids the case that a misconfigured PAM stack allows a user to chew up unlimited processes in the "system" project.

To do this, after PAM has had a chance to set up a custom project:

  • Call getprojid() then getprojbyid() to identify our project.
  • Call inproj() to check that continuing with this project is allowed according to the user's privileges.

From OpenSSH bug #1824, the library calls utilized to put a process into a user's default project would be:

  • getdefaultproj(): Obtains the default project for the user logging in.
  • setproject(): Sets the project for the session. Requires special privs (uid=0) or will fail.

BSD: login_cap

Many of these tasks are factored out of login(1) into libutil on BSD systems. Further, an additional database of per-user limits and environment is kept in /etc/login.conf detailing the system's restrictions on user rights, and these limits must be applied.

The login class capabilities database contains information to be acted on in three ways:

  • The basic login class for each user describes, for members of the class, restrictions on their logons. auth_timeok() (and, where appropriate, auth_hostok() and auth_ttyok()) should be respected. For these purposes, fetch the user's login class with login_getpwclass(), which uses root's class to override if the uid is zero (this is the case that means fetching class by username alone isn't quite enough), and falls back to the default class nicely.
  • The login class itself can also be queried for specific properties in it (see 'ENVIRONMENT' in login.conf(5)). One or two of these look like they may be relevant for certain applications (eg requirehome, ignorenologon).
  • Finally, the class describes actionable properties for setting up a user's environment and limits. Set these using setusercontext(), which can be used to set both environment settings (LOGIN_SETUMASK|LOGIN_SETPATH|LOGIN_SETENV) and privileges (LOGIN_SETRESOURCES|LOGIN_SETPRIORITY|LOGIN_SETMAC|LOGIN_SETCPUMASK). Note that although it doesn't say so in the documentation, setusercontext() ought to be called twice, once before dropping root, and once after dropping root: only if the process's uid is right does the function read settings from the user's configuration (namely LOGIN_SETRESOURCES, LOGIN_SETUMASK, LOGIN_SETPATH, LOGIN_SETENV, and LOGIN_SETCPUMASK). Note this means that LOGIN_SETRESOURCES, LOGIN_SETCPUMASK should therefore be set twice, once as root in case the user is allowed higher limits than the daemon, and once afterwards to lower them according the user's own preferences.