The template user with PAM and login(1)

Aug. 3, 2018, 7:11 p.m.

When you build a new service (or an appliance) you need your users to be able to configure it from the command line. To accomplish this you can create system accounts for all registered users in your service and assign them a special login shell which provides such limited functionality. This can be painful if you have a dynamic user database.

Another challenge is authentication via remote services such as RADIUS. How can we implement  services when we authenticate through it and log into it as a different user? Furthermore, imagine a scenario when RADIUS decides on which account we have the right to access by sending an additional attribute.

To address these two problems we can use a "template" user. Any of the PAM modules can set the value of the PAM_USER item. The value of this item will be used to determine which account we want to login. Only the "template" user must exist on the local password database, but the credential check can be omitted by the module.

This functionality exists in the login(1) used by FreeBSD, HardenedBSD, DragonFlyBSD and illumos. The functionality doesn't exist in the login(1) used in NetBSD, and OpenBSD doesn't support PAM modules at all. In addition what  is also noteworthy is that such functionality was also in the OpenSSH but they decided to remove it and call it a security vulnerability (CVE 2015-6563). I can see how some people may have seen it that way, that’s why I recommend reading this article from an OpenPAM author and  a FreeBSD security officer at the time.

Knowing the background let's take a look at an example.
pam_sm_authenticate(pam_handle_t *pamh, int flags __unused,
    int argc __unused, const char *argv[] __unused)
        const char *user, *password;
        int err;

        err = pam_get_user(pamh, &user, NULL);
        if (err != PAM_SUCCESS)
                return (err);

        err = pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL);
        if (err == PAM_CONV_ERR)
                return (err);
        if (err != PAM_SUCCESS)
                return (PAM_AUTH_ERR);

        err = authenticate(user, password);
        if (err != PAM_SUCCESS) {
                return (err);

        return (pam_set_item(pamh, PAM_USER, "template"));

In the listing above we have an example of a PAM module. The pam_get_user(3) provides a username. The pam_get_authtok(3) shows us a secret given by the user. Both functions allow us to give an optional prompt which should be shown to the user. The authenticate function is our crafted function which authenticates the user. In our first scenario we wanted to keep all users in an external database. If authentication is successful we then switch to a template user which has  a shell set up for a script allowing us to configure the machine. In our second scenario the authenticate function authenticates the user in RADIUS.

Another step is to add our PAM module to the /etc/pam.d/system or to the /etc/pam.d/login configuration:
auth            sufficient            no_warn allow_localUnfortunately the description of all these options goes beyond this article - if you would like to know more about it you can find them in the PAM manual. The last thing we need to do is to add our template user to the system which  you can do by the adduser(8) command or just simply modifying the /etc/master.passwd file and use pwd_mkdb(8) program:
$ tail -n /etc/master.passwd
template:*:1000:1000::0:0:User &:/:/usr/local/bin/templatesh
$ sudo pwd_mkdb /etc/master.passwd
As you can see,the template user can be locked and we still can use it in our PAM module (the * character after login).

I would like to thank Dag-Erling Smørgrav for pointing this functionality out to me when I was looking for it some time ago.