diff options
author | Dave Olson <olson@cumulusnetworks.com> | 2018-04-12 23:57:55 -0700 |
---|---|---|
committer | Dave Olson <olson@cumulusnetworks.com> | 2018-04-13 15:04:03 -0700 |
commit | acc77c4757775bb7689ba769465951a65523db75 (patch) | |
tree | ac797a2985f5c472f83f42b13acb3499553f5a15 | |
parent | a0d0d2fb1b321d65425951fc70f5c42c2dcfda41 (diff) | |
download | libpam-radius-auth-acc77c4757775bb7689ba769465951a65523db75.tar.gz libpam-radius-auth-acc77c4757775bb7689ba769465951a65523db75.zip |
Add a new package radius-shell with a setcap radius_shell front end
Ticket: CM-19457
Reviewed By: nobody
Testing Done: multiple logins, separately and simultaneously
Because we can't determine privilege level separately and up front with
the RADIUS protocol, unlike TACACS+, we wind up with all logins as the
same unprivileged radius uid. But we can set the auid (accounting or
auditing uid) correctly, and a separate setcap radius_shell can be set as
the login shell, and can fixup the uid before running /bin/bash.
To set the auid correctly, we need to know the privileged radius user
account. Added mapped_priv_user to the configuration file to handle
that. mapped_priv_user has to match the account used by libnss-mapuser.
That's a bit ugly, but a common config file would be uglier.
The radius shell is in a new package, since it has binaries. The new
package is radius-shell. In it's post actions, it changes the radius
users shell to radius_shell if they are present, and back to /bin/bash
on package removal. It uses capabilities, tries to be very restrictive
in what it changes, and depends on being installed setcap cap_setuid
Make the existing libpam-radius-auth package depend on radius-shell, so
it will pull in the new package on upgrades.
Also fixed another issue with reparsing changed config file, have to
handle case where there were servers defined, but aren't any longer.
-rw-r--r-- | Makefile | 19 | ||||
-rw-r--r-- | debian/control | 13 | ||||
-rw-r--r-- | debian/radius-shell.install | 1 | ||||
-rw-r--r-- | debian/radius-shell.manpages | 1 | ||||
-rw-r--r-- | debian/radius-shell.postinst | 29 | ||||
-rw-r--r-- | debian/radius-shell.postrm | 23 | ||||
-rwxr-xr-x | debian/rules | 4 | ||||
-rw-r--r-- | pam_radius_auth.5 | 6 | ||||
-rw-r--r-- | pam_radius_auth.conf | 4 | ||||
-rw-r--r-- | radius_shell.8 | 47 | ||||
-rw-r--r-- | src/pam_radius_auth.c | 84 | ||||
-rw-r--r-- | src/pam_radius_auth.h | 1 | ||||
-rw-r--r-- | src/radius_shell.c | 118 | ||||
-rw-r--r-- | src/support.c | 6 |
14 files changed, 326 insertions, 30 deletions
@@ -24,14 +24,17 @@ CFLAGS := -Wall -Werror -fPIC ${CFLAGS} # Then copy pam_radius_auth.so to /usr/freeware/lib32/security (PAM dir) # CFLAGS = -#LDFLAGS += -shared -Wl,--version-script=pamsymbols.ver -LDFLAGS += -shared +#LDSHFLAGS += -shared -Wl,--version-script=pamsymbols.ver +LDSHFLAGS = -shared +LDLIBS += -laudit +BINLIBS += -lcap +LIBLIBS += -lpam ###################################################################### # # The default rule to build everything. # -all: pam_radius_auth.so +all: pam_radius_auth.so radius_shell ###################################################################### # @@ -48,6 +51,9 @@ src/pam_radius_auth.o: src/pam_radius_auth.c src/pam_radius_auth.h src/md5.o: src/md5.c src/md5.h @$(MAKE) -C src $(notdir $@) +src/radius_shell.o: src/radius_shell.c + @$(MAKE) -C src $(notdir $@) + # # This is what should work on Irix: #pam_radius_auth.so: pam_radius_auth.o md5.o @@ -67,7 +73,10 @@ src/md5.o: src/md5.c src/md5.h # gcc -shared pam_radius_auth.o md5.o -lpam -lc -o pam_radius_auth.so # pam_radius_auth.so: src/pam_radius_auth.o src/support.o src/md5.o - $(CC) $(LDFLAGS) $^ -lpam -o pam_radius_auth.so + $(CC) $(LDFLAGS) $(LDSHFLAGS) $^ $(LDLIBS) $(LIBLIBS) -o $@ + +radius_shell: src/radius_shell.o + $(CC) $(LDFLAGS) $^ $(LDLIBS) $(BINLIBS) -o $@ ###################################################################### # @@ -85,4 +94,4 @@ dist: # .PHONY: clean clean: - @rm -f *~ *.so *.o src/*.o src/*~ + @rm -f *~ *.so *.o src/*.o src/*~ radius_shell diff --git a/debian/control b/debian/control index b70c948..b8022db 100644 --- a/debian/control +++ b/debian/control @@ -3,13 +3,22 @@ Maintainer: dev-support <dev-support@cumulusnetworks.com> Section: libs Priority: extra Standards-Version: 3.9.6 -Build-Depends: libpam0g-dev | libpam-dev, debhelper (>= 9~), libaudit-dev +Build-Depends: libpam0g-dev | libpam-dev, debhelper (>= 9~), libaudit-dev, libcap-dev Package: libpam-radius-auth Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, libaudit1 +Depends: ${shlibs:Depends}, ${misc:Depends}, libaudit1, radius-shell Description: PAM RADIUS client authentication module This is the PAM to RADIUS authentication module. It allows any PAM-capable machine to become a RADIUS client for authentication and accounting requests. You will, however, need to supply your own RADIUS server to perform the actual authentication + +Package: radius-shell +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, libaudit1, libcap2-bin, libcap2, libnss-mapuser +Description: Shell front-end used for radius users. + This provides a uid fixup program. Due to the limitations of the + RADIUS protocol, we can't tell whether a user is privileged until + after authentication. This packages provides a shell front-end that + sets the uid to the auid, if set and > 1000, and not already matching. diff --git a/debian/radius-shell.install b/debian/radius-shell.install new file mode 100644 index 0000000..7671d36 --- /dev/null +++ b/debian/radius-shell.install @@ -0,0 +1 @@ +radius_shell sbin diff --git a/debian/radius-shell.manpages b/debian/radius-shell.manpages new file mode 100644 index 0000000..bb1a970 --- /dev/null +++ b/debian/radius-shell.manpages @@ -0,0 +1 @@ +radius_shell.8 diff --git a/debian/radius-shell.postinst b/debian/radius-shell.postinst new file mode 100644 index 0000000..55ebd22 --- /dev/null +++ b/debian/radius-shell.postinst @@ -0,0 +1,29 @@ +#! /bin/sh + +set -e + +# we depend on libnss-mapuser, so that the radius group will have been +# created before this script runs. + +case "$1" in + configure) + radshell=/sbin/radius_shell + chmod 750 $radshell + chgrp radius_users $radshell + setcap cap_setuid+ep $radshell + # The users will have been created by the libnss-mapuser package + # and possibly by an older version, so change the shells here. + # This also prevents a loop in package install ordering dependencies + for usr in radius_user radius_priv_user; do + uent="$(getent -s compat passwd $usr 2>/dev/null)" || true + [ -z "$uent" ] && continue + case "$uent" in + *${radshell}*) ;; + *) chsh -s $radshell $usr ;; + esac + done + ;; +esac + +#DEBHELPER# + diff --git a/debian/radius-shell.postrm b/debian/radius-shell.postrm new file mode 100644 index 0000000..89ae97f --- /dev/null +++ b/debian/radius-shell.postrm @@ -0,0 +1,23 @@ +#! /bin/sh + +set -e + +# we depend on libnss-mapuser, so that the radius group will have been +# created before this script runs. + +case "$1" in + remove|purge) + # fixup the shell for the users we may have modified on installation, + # if still present, and using our shell + for usr in radius_user radius_priv_user; do + uent="$(getent -s compat passwd $usr 2>/dev/null)" || true + [ -z "$uent" ] && continue + case "$uent" in + *${radshell}*) chsh -s /bin/bash $usr ;; + esac + done + ;; +esac + +#DEBHELPER# + diff --git a/debian/rules b/debian/rules index 52172f8..3039568 100755 --- a/debian/rules +++ b/debian/rules @@ -21,8 +21,10 @@ export CFLAGS # all the installing is here, not in Makefile. # The configuration file with the share secrets needs to be 600 override_dh_install: - dh_install -v --sourcedir=. + dh_install -v --sourcedir=. --package=libpam-radius-auth + dh_install -v --sourcedir=. --package=radius-shell chmod 600 debian/*/${PAM_CONF_FILE} + chmod 750 debian/*/sbin/radius_shell override_dh_fixperms: dh_fixperms --exclude ${PAM_CONF_FILE} diff --git a/pam_radius_auth.5 b/pam_radius_auth.5 index 05a9f0b..b4b67f8 100644 --- a/pam_radius_auth.5 +++ b/pam_radius_auth.5 @@ -66,6 +66,12 @@ to be considered a privileged login (ability to configure via nclu 'net' commands, and able to sudo. The default is 15. The range is 0-15. Only matters when the VSA attribute is returned. +.TP +.I mapped_priv_user accountname +This sets the name of the privileged mapping account in /etc/passwd. +It must match the privileged account name in +.IR nss_mapuser (5) +for correct operation. .SH "SEE ALSO" .BR pam_radius_auth (8), .BR nss_mapuser (5) diff --git a/pam_radius_auth.conf b/pam_radius_auth.conf index 199d383..51b6595 100644 --- a/pam_radius_auth.conf +++ b/pam_radius_auth.conf @@ -55,3 +55,7 @@ # # Uncomment to enable debugging, can be used instead of altering pam files # debug +# +# Account for privileged radius user mapping. If you change it here, you need +# to change /etc/nss_mapuser.conf as well +mapped_priv_user radius_priv_user diff --git a/radius_shell.8 b/radius_shell.8 new file mode 100644 index 0000000..94b1930 --- /dev/null +++ b/radius_shell.8 @@ -0,0 +1,47 @@ +.TH radius_shell 8 +.\" Copyright 2018 Cumulus Networks, Inc. All rights reserved. +.SH NAME +radius_shell - front end shell for radius users +.SH SYNOPSIS +.B /sbin/radius_shell +is RADIUS client front end shell that will ensure that the uid is set +to the auid (the accounting uid). +.SH DESCRIPTION +This shell front-end needed because at login, it's +not possible to determine if a user is privileged up front, because +the RADIUS protocol combines authentication and authorization in a single +transaction. +.P +That means that all RADIUS users login as the same base mapped user and therefore +the same UID, although the auid will be set differently. +.P +The +.B radius_shell +is installed with setcap permissions that allow it to set the uid. +It is set as the login shell for the radius users via the +.I libnss-mapuser +package. +.P +For security, the uid of the process is only changed if the auid is set, +and is 1000 or larger (this is the normal minimum uid for non-privileged +users via +.I adduser +and +.IR useradd . +The value is hardcoded in the source, it is not read from the +.I adduser.conf +configuration file. +.P +Whether the uid is changed or not, a login shell is exec'ed. +At this time, the login shell is only +.BR /bin/bash , +although the other shells listed in +.I /etc/shells +may be allowed in the future. +.SH "SEE ALSO" +.BR setcap (8), +.BR pam_radius_auth (8), +.BR nss_mapuser (5) +.SH FILES +.SH AUTHOR +Dave Olson <olson@cumulusnetworks.com> diff --git a/src/pam_radius_auth.c b/src/pam_radius_auth.c index c4274fa..7f29f37 100644 --- a/src/pam_radius_auth.c +++ b/src/pam_radius_auth.c @@ -203,10 +203,11 @@ static int get_ipaddr(char *host, struct sockaddr *addr, char *port) } /* - * take server->hostname, and convert it to server->ip + * Lookup server->hostname, to get server->ip * Done once when server list parsed. The last part, the * if port isn't set in config, it needs to be set to either * radius or raddacct + * returns 0 on success, otherwise non-zero */ static int host2server(pam_handle_t * pamh, radius_server_t * server) { @@ -216,7 +217,7 @@ static int host2server(pam_handle_t * pamh, radius_server_t * server) /* hostname might be [ipv6::address] */ strncpy(hostbuffer, server->hostname, sizeof(hostbuffer) - 1); - hostbuffer[sizeof(hostbuffer) - 1] = 0; + hostbuffer[sizeof(hostbuffer) - 1] = 0; /* ensure null term */ hostname = hostbuffer; portstart = hostbuffer; if (hostname[0] == '[') { @@ -653,7 +654,7 @@ static void cleanup_conf(pam_handle_t * pamh, void *arg, int unused) static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) { static struct stat last_st; - int line = 0, timeout; + int line = 0, timeout, ret = 0; const char *cfname = cf->conf_file; char *p; radius_server_t *server = NULL, *tmp; @@ -661,8 +662,10 @@ static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) char hostname[BUFFER_SIZE], secret[BUFFER_SIZE], buffer[BUFFER_SIZE]; char srcip[BUFFER_SIZE]; - if (!cfname || !*cfname) - return -1; + if (!cfname || !*cfname) { + ret = -1; + goto done; + } if (last_st.st_ino) { struct stat st; @@ -670,18 +673,22 @@ static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) rst = stat(cfname, &st); if (!rst && st.st_ino == last_st.st_ino && st.st_mtime == last_st.st_mtime && st.st_ctime == last_st.st_ctime) { + /* no changes to savconf, so just return */ return 1; } } - if (cf->server) /* we already had sockets open and bound, cleanup */ + if (cf->server) { /* we already had sockets open and bound, cleanup */ pam_set_data(pamh, "rad_conf_cleanup", NULL, NULL); + cf->server = NULL; /* in case reuse and no servers found */ + } /* the first time around, read the configuration file */ if ((fserver = fopen(cfname, "r")) == (FILE *) NULL) { _pam_log(pamh, LOG_ERR, "Could not open configuration file %s:" " %m", cfname); - return -1; + ret = -1; + goto done; } while (!feof(fserver) && @@ -746,6 +753,20 @@ static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) } } continue; + } else if (!strcmp(hostname, "mapped_priv_user")) { + /* mapped account name of radius privileged user for + * uid/auid fixup */ + if (scancnt < 2) + _pam_log(pamh, LOG_ERR, + "ERROR reading %s, line %d:" + " only %d fields", cf->conf_file, line, + scancnt); + else + snprintf(cf->privusrmap, sizeof cf->privusrmap, "%s", + secret); + snprintf(savconf.privusrmap, sizeof savconf.privusrmap, + "%s", secret); + continue; } else if (!strcmp(hostname, "debug")) { /* allow setting debug in config file as well */ cf->debug = cfg_debug = 1; @@ -766,7 +787,8 @@ static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) _pam_log(pamh, LOG_ERR, "Unable to allocate server info for %s: %m", hostname); - return -1; + ret = -1; + goto done; } tmp->sockfd = -1; /* mark as uninitialized */ if (server) { @@ -804,17 +826,18 @@ static int parse_conffile(pam_handle_t * pamh, radius_conf_t * cf) if (!cf->server) { /* no server found, die a horrible death */ _pam_log(pamh, LOG_ERR, "No server found in" " configuration file %s", cf->conf_file); - return -1; + ret = -1; } /* * save the server in savconf for next call (if any) to _parse_args() * for the same config file (will be overridden if a different config - * file + * file; need to do that even if NULL, so we don't re-use old bad data */ +done: savconf.server = cf->server; - return 0; + return ret; } static int setup_sock(pam_handle_t * pamh, radius_server_t * server, @@ -1178,7 +1201,7 @@ static int talk_radius(radius_conf_t * conf, AUTH_HDR * request, * If the user says he wants the bug, * give in. */ - } else { /* authentication request */ + } else { /* authentication request */ if (conf->accounting_bug) { p = ""; } @@ -1318,21 +1341,23 @@ static int rad_converse(pam_handle_t * pamh, int msg_style, char *message, /* * We'll create the home directory if needed, and we'll write the flat file * mapping entry. It's done at this point, because this is the end of the - * authentication phase (and authorization, too, since authorization is part of - * authentication phase for RADIUS) for ssh, login, etc. + * authentication phase (and authorization, too, since authorization is + * part of * authentication phase for RADIUS) for ssh, login, etc. */ static void -setup_userinfo(pam_handle_t * pamh, const char *user, int debug, int privileged) +setup_userinfo(pam_handle_t * pamh, radius_conf_t *cfg, const char *user, + int debug, int privileged) { struct passwd *pw; /* - * set SUDO_PROMPT in env so that it prompts as the login user, not the mapped - * user, unless (unlikely) the prompt has already been set. + * set SUDO_PROMPT in env so that it prompts as the login user, not the + * mapped * user, unless (unlikely) the prompt has already been set. * It won't hurt to do this if the user wasn't mapped. */ if (!pam_getenv(pamh, "SUDO_PROMPT")) { - char nprompt[strlen("SUDO_PROMPT=[sudo] password for ") + strlen(user) + 3]; /* + 3 for ": " and the \0 */ + char nprompt[strlen("SUDO_PROMPT=[sudo] password for ") + + strlen(user) + 3]; /* + 3 for ": " and the \0 */ snprintf(nprompt, sizeof nprompt, "SUDO_PROMPT=[sudo] password for %s: ", user); if (pam_putenv(pamh, nprompt) != PAM_SUCCESS) @@ -1349,6 +1374,27 @@ setup_userinfo(pam_handle_t * pamh, const char *user, int debug, int privileged) } /* + * because the RADIUS protocol is single pass, we always have the + * pw_uid of the unprivileged account at this point. Set things up + * so we use the uid of the privileged radius account. + */ + if (privileged) { + struct passwd *pwp; + if (!cfg->privusrmap[0] || !(pwp = getpwnam(cfg->privusrmap))) { + _pam_log(pamh, LOG_WARNING, "Failed to find uid for" + " privileged account %s, uid may be wrong" + " for user %s", + cfg->privusrmap[0] ? cfg->privusrmap : + "(unset in config)", user); + } + else if (pwp && pw->pw_uid != pwp->pw_uid) { + syslog(LOG_DEBUG, "OLSON wrmap user=%s, but uid=%u, change to %u", + user, pw->pw_uid, pwp->pw_uid); + pw->pw_uid = pwp->pw_uid; + } + } + + /* * We don't "fail" on errors here, since they are not fatal for * the session, although they can result in name or uid lookups not * working correctly. @@ -1601,7 +1647,7 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t * pamh, int flags, int argc, "=%d, min for priv=%d", privlvl, config.min_priv_lvl); } - setup_userinfo(pamh, user, debug, + setup_userinfo(pamh, &config, user, debug, privlvl >= config.min_priv_lvl); retval = PAM_SUCCESS; } else { diff --git a/src/pam_radius_auth.h b/src/pam_radius_auth.h index 2b1e48d..b1a3173 100644 --- a/src/pam_radius_auth.h +++ b/src/pam_radius_auth.h @@ -154,6 +154,7 @@ typedef struct radius_conf_t { int min_priv_lvl; char prompt[MAXPROMPT]; char vrfname[64]; + char privusrmap[64]; } radius_conf_t; void __write_mapfile(pam_handle_t * p, const char *usr, uid_t uid, int priv, diff --git a/src/radius_shell.c b/src/radius_shell.c new file mode 100644 index 0000000..a94c7f3 --- /dev/null +++ b/src/radius_shell.c @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2018 Cumulus Networks, Inc. + * All rights reserved. + * Author: Dave Olson <olson@cumulusnetworks.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program - see the file COPYING. + */ + +/* + * This program exists to set the uid of privileged radius login users. + * Due to the limitations of the RADIUS protocol, we can't determine + * whether a user is privileged or not until they have authenticated, + * and by then, some of the login mechanisms (openssh, e.g.) have already + * determined the uid. + * + * This program looks at the accounting uid, and if set, and not the same + * as the uid, and the auid is >= 1000, will try to reset the uid to the auid + * as well as the fsuid. + * + * For this to work, the program must be installed as setcap cap_setuid. + * As a minor additional safeguard, the program should be installed as + * a member of the radius_users group, and permissions 750. + * + * Errors are written to stderr so the user logging in will see them, + * rather than using syslog. + */ + +#define _GNU_SOURCE +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> +#include <libaudit.h> +#include <string.h> +#include <errno.h> +#include <stdio.h> +#include <sys/fsuid.h> +#include <sys/capability.h> + +int main(int cnt, char **args) +{ + uid_t uid, auid; + cap_value_t capability[] = { CAP_SETUID}; + cap_t capabilities; + char *shell = NULL, *check = NULL, execshell[64]; + + uid = getuid(); + auid = audit_getloginuid(); + + if (uid < 1000 || auid < 1000 || auid == (uid_t)-1 || uid == auid) { + /* We try to be careful in what we will change */ + goto execit; + } + + if (setfsuid(auid) == -1) + fprintf(stderr, "Failed to set fsuid to %u: %s\n", + auid, strerror(errno)); + if (setresuid(auid, auid, auid)) + fprintf(stderr, "Failed to set uid to %u: %s\n", + auid, strerror(errno)); + if (getuid() != auid) + fprintf(stderr, "Failed to set uid to %u it's still %u\n", + auid, getuid()); + +execit: + /* be paranoid, and clear our expected CAP_SETUID capability, + * even though it should be cleared on exec. + */ + capabilities = cap_get_proc(); + if (capabilities) { + if (!cap_set_flag(capabilities, CAP_EFFECTIVE, 1, + capability, CAP_CLEAR) && + !cap_set_flag(capabilities, CAP_PERMITTED, 1, + capability, CAP_CLEAR)) { + if (cap_set_proc(capabilities)) + fprintf(stderr, "Failed to clear cap_setuid: %s\n", + strerror(errno)); + } + } + +#ifdef LATER + /* + * Eventually handle this program being linked or symlinked + * and that the shell is one of the shells in /etc/shells + */ + shell = strrchr(args[0], '/'); + if (!shell) + shell = args[0]; + + if (*shell == '-') { + check = shell + 1; + } + else + check = shell; + + /* should really check this against /etc/shell */ + snprintf(execshell, sizeof execshell, "/bin/%s", check); +#else + check = "bash"; + shell = "-bash"; + snprintf(execshell, sizeof execshell, "/bin/%s", check); +#endif + + execl(execshell, shell, NULL); + fprintf(stderr, "Exec of shell %s failed: %s\n", execshell, + strerror(errno)); + exit(1); +} diff --git a/src/support.c b/src/support.c index ed72f85..78c21f4 100644 --- a/src/support.c +++ b/src/support.c @@ -107,12 +107,12 @@ __write_mapfile(pam_handle_t * pamh, const char *user, uid_t uid, } - /* won't hurt if it already exists, no more overhead than stat() first */ + /* won't hurt if it already exists, same overhead as stat() first */ mkdir(mapdir, 0755); snprintf(tmpstr, sizeof tmpstr, "%s/%u", mapdir, session); /* - * Only create if it doesn't exist. It might exist if we are called from - * su or sudo after a login, for example + * Only create if it doesn't exist. It might exist if we are called + * from su or sudo after a login, for example */ f = fopen(tmpstr, "wx"); if (!f) { |