Research Computing is in the process of migrating and expanding our
authentication system to support additional authentication methods.
Historically we’ve supported VASCO
IDENTIKEY
time-based one-time-password and pin to provide two-factor
authentication.
$ ssh user1234@login.rc.colorado.edu
user1234@login.rc.colorado.edu's password: <pin><otp>
[user1234@login04 ~]$
But the VASCO tokens are expensive, get lost or left at home, have a
battery that runs out, and have an internal clock that sometimes falls
out-of-sync with the rest of the authentication system. For these and
other reasons we’re provisioning most new account with
Duo, which provides iOS and Android
apps but also supports SMS and voice calls.
Unlike VASCO, Duo is only a single authentication factor; so we’ve also
added support for upstream CU-Boulder campus password authentication to
be used in tandem.
This means that we have to support both authentication mechanisms–VASCO
and password+Duo–simultaneously. A naïve implementation might just stack
these methods together.
auth sufficient pam_radius_auth.so try_first_pass # VASCO authenticates over RADIUS
auth requisite pam_krb5.so try_first_pass # CU-Boulder campus password
auth required pam_duo.so
This generally works: VASCO authentication is attempted first over
RADIUS. If that fails, authentication is attempted against the campus
password and, if that succeeds, against Duo.
Unfortunately, this generates spurious authentication failures in VASCO
when using Duo to authenticate: the VASCO method fails, then Duo
authentication is attempted. Users who have both VASCO and Duo
accounts (e.g., all administrators) may generate enough failures to
trigger the break-in mitigation security system, and the VASCO account
may be disabled. This same issue exists if we reverse the authentication
order to try Duo first, then VASCO: VASCO users might then cause their
campus passwords to become disabled.
In stead, we need to enable users to explicitly specify which
authentication method they’re using.
Separate sssd domains
Our first attempt to provide explicit access to different authentication
methods was to provide multiple redundant
sssd domains.
[domain/rc]
description = Research Computing
proxy_pam_target = curc-twofactor-vasco
[domain/duo]
description = Research Computing (identikey+duo authentication)
enumerate = false
proxy_pam_target = curc-twofactor-duo
This allows users to log in normally using VASCO, while password+Duo
authentication can be requested explicitly by logging in as
${user}@duo
.
$ ssh -l user1234@duo login.rc.colorado.edu
This works well enough for the common case of shell access over SSH:
login is permitted and, since both the default rc
domain and the
duo
alias domain are both backed by the same LDAP directory, NSS
sees no important difference once a user is logged in using either
method.
This works because POSIX systems store the uid number returned by
PAM and
NSS, and
generally resolve the uid number to the username on-demand. Not all
systems work this way, however. For example, when we attempted to use
this authentication mechanism to authenticate to our prototype
JupyterHub (web) service, jobs
dispatched to Slurm retained the
${user}@duo
username format. Slurm also uses usernames internally,
and the ${user}@duo
username is not populated within Slurm: only the
base ${user}
username.
Expecting that we would continue to find more unexpected side-effects of
this implementation, we started to look for an alternative mechanism
that doesn’t modify the specified username.
pam_authtok
In general, a user provides two pieces of information during
authentication: a username (which we’ve already determined we shouldn’t
modify) and an authentication token or password. We should be able to
detect, for example, a prefix to that authentication token to determine
what authentication method to use.
$ ssh user1234@login.rc.colorado.edu
user1234@login.rc.colorado.edu's password: duo:<password>
[user1234@login04 ~]$
But we found no such pam module that would allow us to manipulate the
authentication token… so we wrote
one.
auth [success=1 default=ignore] pam_authtok.so prefix=duo: strip prompt=password:
auth [success=done new_authtok_reqd=done default=die] pam_radius_auth.so try_first_pass
auth requisite pam_krb5.so try_first_pass
auth [success=done new_authtok_reqd=done default=die] pam_duo.so
Now our PAM stack authenticates against VASCO by default; but, if the
user provides a password with a duo:
prefix, authentication skips
VASCO and authenticates the supplied password, followed by Duo push. Our
actual production PAM stack is a bit more complicated, supporting a
redundant vasco:
prefix as well, for forward-compatibility should we
change the default authentication mechanism in the future. We can also
extend this mechanism to add arbitrary additional authentication
mechanisms in the future.