xref: /illumos-gate/usr/src/lib/pam_modules/krb5/krb5_acct_mgmt.c (revision cbea7aca3fd7787405cbdbd93752998f03dfc25f)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  *
25  * Copyright 2023 OmniOS Community Edition (OmniOSce) Association.
26  */
27 
28 #include <kadm5/admin.h>
29 #include <krb5.h>
30 
31 #include <security/pam_appl.h>
32 #include <security/pam_modules.h>
33 #include <security/pam_impl.h>
34 #include <syslog.h>
35 #include <string.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <sys/types.h>
39 #include <pwd.h>
40 #include <libintl.h>
41 #include <netdb.h>
42 #include "utils.h"
43 #include <shadow.h>
44 
45 #include "krb5_repository.h"
46 
47 #define	KRB5_AUTOMIGRATE_DATA	"SUNW-KRB5-AUTOMIGRATE-DATA"
48 
49 #define	min(a, b) ((a) < (b) ? (a) : (b))
50 
51 /*
52  * pam_sm_acct_mgmt	  main account managment routine.
53  */
54 
55 static int
fetch_princ_entry(krb5_module_data_t * kmd,const char * princ_str,kadm5_principal_ent_rec * prent,int debug)56 fetch_princ_entry(
57 	krb5_module_data_t *kmd,
58 	const char *princ_str,
59 	kadm5_principal_ent_rec *prent,	/* out */
60 	int debug)
61 
62 {
63 	kadm5_ret_t		code;
64 	krb5_principal 		princ = 0;
65 	char 			admin_realm[1024];
66 	char			kprinc[2*MAXHOSTNAMELEN];
67 	char			*cpw_service, *password;
68 	void 			*server_handle;
69 	krb5_context		context;
70 	kadm5_config_params	params;
71 
72 	password = kmd->password;
73 	context = kmd->kcontext;
74 
75 	if ((code = get_kmd_kuser(context, princ_str,
76 	    kprinc, 2 * MAXHOSTNAMELEN)) != 0) {
77 		return (code);
78 	}
79 
80 	code = krb5_parse_name(context, kprinc, &princ);
81 	if (code != 0) {
82 		return (PAM_SYSTEM_ERR);
83 	}
84 
85 	if (strlen(password) == 0) {
86 		krb5_free_principal(context, princ);
87 		if (debug)
88 			__pam_log(LOG_AUTH | LOG_DEBUG,
89 			    "PAM-KRB5 (acct): fetch_princ_entry: pwlen=0");
90 		return (PAM_AUTH_ERR);
91 	}
92 
93 	(void) strlcpy(admin_realm,
94 		    krb5_princ_realm(context, princ)->data,
95 		    sizeof (admin_realm));
96 
97 	(void) memset((char *)&params, 0, sizeof (params));
98 	params.mask |= KADM5_CONFIG_REALM;
99 	params.realm = admin_realm;
100 
101 	if (kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service)) {
102 		__pam_log(LOG_AUTH | LOG_ERR,
103 			"PAM-KRB5 (acct):  unable to get host based "
104 			"service name for realm '%s'",
105 			admin_realm);
106 		krb5_free_principal(context, princ);
107 		return (PAM_SYSTEM_ERR);
108 	}
109 
110 	code = kadm5_init_with_password(kprinc, password, cpw_service,
111 					&params, KADM5_STRUCT_VERSION,
112 					KADM5_API_VERSION_2, NULL,
113 					&server_handle);
114 	if (code != 0) {
115 		if (debug)
116 			__pam_log(LOG_AUTH | LOG_DEBUG,
117 			    "PAM-KRB5 (acct): fetch_princ_entry: "
118 			    "init_with_pw failed: code = %d", code);
119 		krb5_free_principal(context, princ);
120 		return ((code == KADM5_BAD_PASSWORD) ?
121 			PAM_AUTH_ERR : PAM_SYSTEM_ERR);
122 	}
123 
124 	if (_kadm5_get_kpasswd_protocol(server_handle) != KRB5_CHGPWD_RPCSEC) {
125 		if (debug)
126 			__pam_log(LOG_AUTH | LOG_DEBUG,
127 			    "PAM-KRB5 (acct): fetch_princ_entry: "
128 			    "non-RPCSEC_GSS chpw server, can't get "
129 			    "princ entry");
130 		(void) kadm5_destroy(server_handle);
131 		krb5_free_principal(context, princ);
132 		return (PAM_SYSTEM_ERR);
133 	}
134 
135 	code = kadm5_get_principal(server_handle, princ, prent,
136 				KADM5_PRINCIPAL_NORMAL_MASK);
137 
138 	if (code != 0) {
139 		(void) kadm5_destroy(server_handle);
140 		krb5_free_principal(context, princ);
141 		return ((code == KADM5_UNK_PRINC) ?
142 			PAM_USER_UNKNOWN : PAM_SYSTEM_ERR);
143 	}
144 
145 	(void) kadm5_destroy(server_handle);
146 	krb5_free_principal(context, princ);
147 
148 	return (PAM_SUCCESS);
149 }
150 
151 /*
152  * exp_warn
153  *
154  * Warn the user if their pw is set to expire.
155  *
156  * We first check to see if the KDC had set any account or password
157  * expiration information in the key expiration field.  If this was
158  * not set then we must assume that the KDC could be broken and revert
159  * to fetching pw/account expiration information from kadm.  We can not
160  * determine the difference between broken KDCs that do not send key-exp
161  * vs. principals that do not have an expiration policy.  The up-shot
162  * is that pam_krb5 will probably not be stacked for acct mgmt if the
163  * environment does not have an exp policy, avoiding the second exchange
164  * using the kadm protocol.
165  */
166 static int
exp_warn(pam_handle_t * pamh,const char * user,krb5_module_data_t * kmd,int debug)167 exp_warn(
168 	pam_handle_t *pamh,
169 	const char *user,
170 	krb5_module_data_t *kmd,
171 	int debug)
172 
173 {
174 	int err;
175 	kadm5_principal_ent_rec prent;
176 	krb5_timestamp  now, days, expiration;
177 	char    messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE], *password;
178 	krb5_error_code code;
179 
180 	if (debug)
181 		__pam_log(LOG_AUTH | LOG_DEBUG,
182 		    "PAM-KRB5 (acct): exp_warn start: user = '%s'",
183 		    user ? user : "<null>");
184 
185 	password = kmd->password;
186 
187 	if (!pamh || !user || !password) {
188 		err = PAM_SERVICE_ERR;
189 		goto exit;
190 	}
191 
192 	/*
193 	 * If we error out from krb5_init_secure_context, then just set error
194 	 * code, check to see about debug message and exit out of routine as the
195 	 * context could not possibly have been setup.
196 	 */
197 
198 	if (code = krb5_init_secure_context(&kmd->kcontext)) {
199 		err = PAM_SYSTEM_ERR;
200 		if (debug)
201 			__pam_log(LOG_AUTH | LOG_ERR, "PAM-KRB5 (acct): "
202 			    "krb5_init_secure_context failed: code=%d",
203 			    code);
204 		goto exit;
205 	}
206 	if (code = krb5_timeofday(kmd->kcontext, &now)) {
207 		err = PAM_SYSTEM_ERR;
208 		if (debug)
209 			__pam_log(LOG_AUTH | LOG_ERR,
210 			    "PAM-KRB5 (acct): krb5_timeofday failed: code=%d",
211 			    code);
212 		goto out;
213 	}
214 
215 	if (kmd->expiration != 0) {
216 		expiration = kmd->expiration;
217 	} else {
218 		(void) memset(&prent, 0, sizeof (prent));
219 		if ((err = fetch_princ_entry(kmd, user, &prent, debug))
220 		    != PAM_SUCCESS) {
221 			if (debug)
222 				__pam_log(LOG_AUTH | LOG_DEBUG,
223 				"PAM-KRB5 (acct): exp_warn: fetch_pr failed %d",
224 				err);
225 			goto out;
226 		}
227 		if (prent.princ_expire_time != 0 && prent.pw_expiration != 0)
228 			expiration = min(prent.princ_expire_time,
229 				prent.pw_expiration);
230 		else
231 			expiration = prent.princ_expire_time ?
232 				prent.princ_expire_time : prent.pw_expiration;
233 	}
234 
235 	if (debug)
236 		__pam_log(LOG_AUTH | LOG_DEBUG,
237 		    "PAM-KRB5 (acct): exp_warn: "
238 		    "princ/pw_exp exp=%ld, now =%ld, days=%ld",
239 		    expiration,
240 		    now,
241 		    expiration > 0
242 		    ? ((expiration - now) / DAY)
243 		    : 0);
244 
245 	/* warn user if principal's pw is set to expire */
246 	if (expiration > 0) {
247 		days = (expiration - now) / DAY;
248 		if (days <= 0)
249 			(void) snprintf(messages[0],
250 				sizeof (messages[0]),
251 				dgettext(TEXT_DOMAIN,
252 				"Your Kerberos account/password will expire "
253 				"within 24 hours.\n"));
254 		else if (days == 1)
255 			(void) snprintf(messages[0],
256 				sizeof (messages[0]),
257 				dgettext(TEXT_DOMAIN,
258 				"Your Kerberos account/password will expire "
259 				"in 1 day.\n"));
260 		else
261 			(void) snprintf(messages[0],
262 				sizeof (messages[0]),
263 				dgettext(TEXT_DOMAIN,
264 				"Your Kerberos account/password will expire in "
265 				"%d days.\n"),
266 				(int)days);
267 
268 		(void) __pam_display_msg(pamh, PAM_TEXT_INFO, 1,
269 					messages, NULL);
270 	}
271 
272 	/* things went smooth */
273 	err = PAM_SUCCESS;
274 
275 out:
276 
277 	if (kmd->kcontext) {
278 		krb5_free_context(kmd->kcontext);
279 		kmd->kcontext = NULL;
280 	}
281 
282 exit:
283 
284 	if (debug)
285 		__pam_log(LOG_AUTH | LOG_DEBUG,
286 		    "PAM-KRB5 (acct): exp_warn end: err = %d", err);
287 
288 	return (err);
289 }
290 
291 /*
292  * pam_krb5 acct_mgmt
293  *
294  * we do
295  *    - check if pw expired (flag set in auth)
296  *    - warn user if pw is set to expire
297  *
298  * notes
299  *    - we require the auth module to have already run (sets module data)
300  *    - we don't worry about an expired princ cuz if that's the case,
301  *      auth would have failed
302  */
303 int
pam_sm_acct_mgmt(pam_handle_t * pamh,int flags,int argc,const char ** argv)304 pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
305 {
306 	const char *user = NULL;
307 	char *userdata = NULL;
308 	int err;
309 	int i;
310 	krb5_module_data_t *kmd = NULL;
311 	char messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE];
312 	int debug = 0;  /* pam.conf entry option */
313 	int nowarn = 0; /* pam.conf entry option, no expire warnings */
314 	const pam_repository_t *rep_data = NULL;
315 
316 	for (i = 0; i < argc; i++) {
317 		if (strcasecmp(argv[i], "debug") == 0)
318 			debug = 1;
319 		else if (strcasecmp(argv[i], "nowarn") == 0) {
320 			nowarn = 1;
321 			flags = flags | PAM_SILENT;
322 		} else {
323 			__pam_log(LOG_AUTH | LOG_ERR,
324 			    "PAM-KRB5 (acct): illegal option %s",
325 			    argv[i]);
326 		}
327 	}
328 
329 	if (debug)
330 		__pam_log(LOG_AUTH | LOG_DEBUG,
331 		    "PAM-KRB5 (acct): debug=%d, nowarn=%d",
332 		    debug, nowarn);
333 
334 	(void) pam_get_item(pamh, PAM_REPOSITORY, (const void **)&rep_data);
335 
336 	if (rep_data != NULL) {
337 		/*
338 		 * If the repository is not ours,
339 		 * return PAM_IGNORE.
340 		 */
341 		if (strcmp(rep_data->type, KRB5_REPOSITORY_NAME) != 0) {
342 			if (debug)
343 				__pam_log(LOG_AUTH | LOG_DEBUG,
344 					"PAM-KRB5 (acct): wrong"
345 					"repository found (%s), returning "
346 					"PAM_IGNORE", rep_data->type);
347 			return (PAM_IGNORE);
348 		}
349 	}
350 
351 
352 	/* get user name */
353 	(void) pam_get_item(pamh, PAM_USER, (const void **)&user);
354 
355 	if (user == NULL || *user == '\0') {
356 		err = PAM_USER_UNKNOWN;
357 		goto out;
358 	}
359 
360 	/* get pam_krb5_migrate specific data */
361 	err = pam_get_data(pamh, KRB5_AUTOMIGRATE_DATA,
362 					(const void **)&userdata);
363 	if (err != PAM_SUCCESS) {
364 		if (debug)
365 			__pam_log(LOG_AUTH | LOG_DEBUG, "PAM-KRB5 (acct): "
366 				"no module data for KRB5_AUTOMIGRATE_DATA");
367 	} else {
368 		/*
369 		 * We try and reauthenticate, since this user has a
370 		 * newly created krb5 principal via the pam_krb5_migrate
371 		 * auth module. That way, this new user will have fresh
372 		 * creds (assuming pam_sm_authenticate() succeeds).
373 		 */
374 		if (strcmp(user, userdata) == 0)
375 			(void) pam_sm_authenticate(pamh, flags, argc, argv);
376 		else
377 			if (debug)
378 				__pam_log(LOG_AUTH | LOG_DEBUG,
379 				"PAM-KRB5 (acct): PAM_USER %s"
380 				"does not match user %s from pam_get_data()",
381 				user, (char *)userdata);
382 	}
383 
384 	/* get krb5 module data  */
385 	if ((err = pam_get_data(pamh, KRB5_DATA, (const void **)&kmd))
386 	    != PAM_SUCCESS) {
387 		if (err == PAM_NO_MODULE_DATA) {
388 			/*
389 			 * pam_auth never called (possible config
390 			 * error; no pam_krb5 auth entry in pam.conf),
391 			 */
392 			if (debug) {
393 				__pam_log(LOG_AUTH | LOG_DEBUG,
394 				    "PAM-KRB5 (acct): no module data");
395 			}
396 			err = PAM_IGNORE;
397 			goto out;
398 		} else {
399 			__pam_log(LOG_AUTH | LOG_ERR,
400 				    "PAM-KRB5 (acct): get module"
401 				    " data failed: err=%d",
402 			    err);
403 		}
404 		goto out;
405 	}
406 
407 	debug = debug || kmd->debug;
408 
409 	/*
410 	 * auth mod set status to ignore, most likely cuz root key is
411 	 * in keytab, so skip other checks and return ignore
412 	 */
413 	if (kmd->auth_status == PAM_IGNORE) {
414 		if (debug)
415 			__pam_log(LOG_AUTH | LOG_DEBUG,
416 			    "PAM-KRB5 (acct): kmd auth_status is IGNORE");
417 		err = PAM_IGNORE;
418 		goto out;
419 	}
420 
421 	/*
422 	 * If there is no Kerberos related user and there is authentication
423 	 * data, this means that while the user has successfully passed
424 	 * authentication, Kerberos is not the account authority because there
425 	 * is no valid Kerberos principal.  PAM_IGNORE is returned since
426 	 * Kerberos is not authoritative for this user.  Other modules in the
427 	 * account stack will need to determine the success or failure for this
428 	 * user.
429 	 */
430 	if (kmd->auth_status == PAM_USER_UNKNOWN) {
431 		if (debug)
432 			syslog(LOG_DEBUG,
433 			    "PAM-KRB5 (acct): kmd auth_status is USER UNKNOWN");
434 		err = PAM_IGNORE;
435 		goto out;
436 	}
437 
438 	/*
439 	 * age_status will be set to PAM_NEW_AUTHTOK_REQD in pam_krb5's
440 	 * 'auth' if the user's key/pw has expired and needs to be changed
441 	 */
442 	if (kmd->age_status == PAM_NEW_AUTHTOK_REQD) {
443 		if (!nowarn) {
444 			(void) snprintf(messages[0], sizeof (messages[0]),
445 				dgettext(TEXT_DOMAIN,
446 				"Your Kerberos password has expired.\n"));
447 			(void) __pam_display_msg(pamh, PAM_TEXT_INFO,
448 					1, messages, NULL);
449 		}
450 		err = PAM_NEW_AUTHTOK_REQD;
451 		goto out;
452 	}
453 
454 	if (kmd->auth_status == PAM_SUCCESS && !(flags & PAM_SILENT) &&
455 	    !nowarn && kmd->password) {
456 		/* if we fail, let it slide, it's only a warning brah */
457 		(void) exp_warn(pamh, user, kmd, debug);
458 	}
459 
460 	/*
461 	 * If Kerberos is treated as optional in the PAM stack, it is possible
462 	 * that there is a KRB5_DATA item and a non-Kerberos account authority.
463 	 * In that case, PAM_IGNORE is returned.
464 	 */
465 	err = kmd->auth_status != PAM_SUCCESS ? PAM_IGNORE : kmd->auth_status;
466 
467 out:
468 	if (debug)
469 		__pam_log(LOG_AUTH | LOG_DEBUG,
470 		    "PAM-KRB5 (acct): end: %s", pam_strerror(pamh, err));
471 
472 	return (err);
473 }
474