/* security.c * * Implement Red Hat crond security context transitions * * Jason Vas Dias January 2006 * * Copyright(C) Red Hat Inc., 2006 * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "config.h" #include #include #include #include #include #include #include #include "cronie_common.h" #include "funcs.h" #include "globals.h" #include "macros.h" #ifdef WITH_PAM # include #endif #ifdef WITH_SELINUX # include # include # include #endif #ifdef WITH_AUDIT # include #endif #ifdef WITH_PAM static pam_handle_t *pamh = NULL; static int pam_session_opened = 0; //global for open session static int cron_conv(int num_msg, const struct pam_message **msgm, struct pam_response **response ATTRIBUTE_UNUSED, void *appdata_ptr ATTRIBUTE_UNUSED) { int i; for (i = 0; i < num_msg; i++) { switch (msgm[i]->msg_style) { case PAM_ERROR_MSG: case PAM_TEXT_INFO: if (msgm[i]->msg != NULL) { log_it("CRON", getpid(), "pam_message", msgm[i]->msg, 0); } break; default: break; } } return (0); } static const struct pam_conv conv = { cron_conv, NULL }; static int cron_open_pam_session(struct passwd *pw); # define PAM_FAIL_CHECK if (retcode != PAM_SUCCESS) { \ log_it(pw->pw_name, getpid(), "PAM ERROR", pam_strerror(pamh, retcode), 0); \ if (pamh != NULL) { \ if (pam_session_opened != 0) \ pam_close_session(pamh, PAM_SILENT); \ pam_end(pamh, retcode); \ pamh = NULL; \ } \ return(retcode); } #endif static char **build_env(char **cronenv); #ifdef WITH_SELINUX static int cron_change_selinux_range(user * u, security_context_t ucontext); static int cron_get_job_range(user * u, security_context_t * ucontextp, char **jobenv); #endif void cron_restore_default_security_context(void) { #ifdef WITH_SELINUX if (is_selinux_enabled() <= 0) return; if (setexeccon(NULL) < 0) log_it("CRON", getpid(), "ERROR", "failed to restore SELinux context", 0); #endif } int cron_set_job_security_context(entry *e, user *u ATTRIBUTE_UNUSED, char ***jobenv) { time_t minutely_time = 0; #ifdef WITH_PAM int ret; #endif if ((e->flags & MIN_STAR) == MIN_STAR) { /* "minute-ly" job: Every minute for given hour/dow/month/dom. * Ensure that these jobs never run in the same minute: */ minutely_time = time(NULL); Debug(DSCH, ("Minute-ly job. Recording time %lld\n", (long long)minutely_time)); } #ifdef WITH_PAM /* PAM is called only for non-root users or non-system crontab */ if ((!u->system || e->pwd->pw_uid != 0) && (ret = cron_start_pam(e->pwd)) != 0) { log_it(e->pwd->pw_name, getpid(), "FAILED to authorize user with PAM", pam_strerror(pamh, ret), 0); return -1; } #endif #ifdef WITH_SELINUX /* we must get the crontab context BEFORE changing user, else * we'll not be permitted to read the cron spool directory :-) */ security_context_t ucontext = 0; if (cron_get_job_range(u, &ucontext, e->envp) < OK) { log_it(e->pwd->pw_name, getpid(), "ERROR", "failed to get SELinux context", 0); return -1; } if (cron_change_selinux_range(u, ucontext) != 0) { log_it(e->pwd->pw_name, getpid(), "ERROR", "failed to change SELinux context", 0); if (ucontext) freecon(ucontext); return -1; } if (ucontext) freecon(ucontext); #endif #ifdef WITH_PAM if (pamh != NULL && (ret = cron_open_pam_session(e->pwd)) != 0) { log_it(e->pwd->pw_name, getpid(), "FAILED to open PAM security session", pam_strerror(pamh, ret), 0); return -1; } #endif if (cron_change_groups(e->pwd) != 0) { return -1; } *jobenv = build_env(e->envp); time_t job_run_time = time(NULL); if ((minutely_time > 0) && ((job_run_time / 60) != (minutely_time / 60))) { /* if a per-minute job is delayed into the next minute * (eg. by network authentication method timeouts), skip it. */ struct tm tmS, tmN; char buf[256]; localtime_r(&job_run_time, &tmN); localtime_r(&minutely_time, &tmS); snprintf(buf, sizeof (buf), "Job execution of per-minute job scheduled for " "%.2u:%.2u delayed into subsequent minute %.2u:%.2u. Skipping job run.", tmS.tm_hour, tmS.tm_min, tmN.tm_hour, tmN.tm_min); log_it(e->pwd->pw_name, getpid(), "INFO", buf, 0); return -1; } return 0; } #if defined(WITH_PAM) int cron_start_pam(struct passwd *pw) { int retcode = 0; retcode = pam_start("crond", pw->pw_name, &conv, &pamh); PAM_FAIL_CHECK; retcode = pam_set_item(pamh, PAM_TTY, "cron"); PAM_FAIL_CHECK; retcode = pam_acct_mgmt(pamh, PAM_SILENT); PAM_FAIL_CHECK; retcode = pam_setcred(pamh, PAM_ESTABLISH_CRED | PAM_SILENT); PAM_FAIL_CHECK; return retcode; } #endif #if defined(WITH_PAM) static int cron_open_pam_session(struct passwd *pw) { int retcode; retcode = pam_open_session(pamh, PAM_SILENT); PAM_FAIL_CHECK; if (retcode == PAM_SUCCESS) pam_session_opened = 1; return retcode; } #endif void cron_close_pam(void) { #if defined(WITH_PAM) if (pam_session_opened != 0) { pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT); pam_close_session(pamh, PAM_SILENT); } if (pamh != NULL) { pam_end(pamh, PAM_SUCCESS); pamh = NULL; } #endif } int cron_change_groups(struct passwd *pw) { pid_t pid = getpid(); if (setgid(pw->pw_gid) != 0) { log_it("CRON", pid, "ERROR", "setgid failed", errno); return -1; } if (initgroups(pw->pw_name, pw->pw_gid) != 0) { log_it("CRON", pid, "ERROR", "initgroups failed", errno); return -1; } #if defined(WITH_PAM) /* credentials may take form of supplementary groups so reinitialize * them here */ if (pamh != NULL) { pam_setcred(pamh, PAM_REINITIALIZE_CRED | PAM_SILENT); } #endif return 0; } int cron_change_user_permanently(struct passwd *pw, char *homedir) { if (setreuid(pw->pw_uid, pw->pw_uid) != 0) { log_it("CRON", getpid(), "ERROR", "setreuid failed", errno); return -1; } if (chdir(homedir) == -1) { log_it("CRON", getpid(), "ERROR chdir failed", homedir, errno); return -1; } log_close(); return 0; } #ifdef WITH_SELINUX static int cron_authorize_context(security_context_t scontext, security_context_t file_context) { struct av_decision avd; int retval; security_class_t tclass; access_vector_t bit; tclass = string_to_security_class("file"); if (!tclass) { log_it("CRON", getpid(), "ERROR", "Failed to translate security class file", errno); return 0; } bit = string_to_av_perm(tclass, "entrypoint"); if (!bit) { log_it("CRON", getpid(), "ERROR", "Failed to translate av perm entrypoint", errno); return 0; } /* * Since crontab files are not directly executed, * crond must ensure that the crontab file has * a context that is appropriate for the context of * the user cron job. It performs an entrypoint * permission check for this purpose. */ retval = security_compute_av(scontext, file_context, tclass, bit, &avd); if (retval || ((bit & avd.allowed) != bit)) return 0; return 1; } #endif #ifdef WITH_SELINUX static int cron_authorize_range(security_context_t scontext, security_context_t ucontext) { struct av_decision avd; int retval; security_class_t tclass; access_vector_t bit; tclass = string_to_security_class("context"); if (!tclass) { log_it("CRON", getpid(), "ERROR", "Failed to translate security class context", errno); return 0; } bit = string_to_av_perm(tclass, "contains"); if (!bit) { log_it("CRON", getpid(), "ERROR", "Failed to translate av perm contains", errno); return 0; } /* * Since crontab files are not directly executed, * so crond must ensure that any user specified range * falls within the seusers-specified range for that Linux user. */ retval = security_compute_av(scontext, ucontext, tclass, bit, &avd); if (retval || ((bit & avd.allowed) != bit)) return 0; return 1; } #endif #if WITH_SELINUX /* always uses u->scontext as the default process context, then changes the level, and retuns it in ucontextp (or NULL otherwise) */ static int cron_get_job_range(user * u, security_context_t * ucontextp, char **jobenv) { char *range; if (is_selinux_enabled() <= 0) return 0; if (ucontextp == NULL) return -1; *ucontextp = NULL; if ((range = env_get("MLS_LEVEL", jobenv)) != NULL) { context_t ccon; if (!(ccon = context_new(u->scontext))) { log_it(u->name, getpid(), "context_new FAILED for MLS_LEVEL", range, 0); context_free(ccon); return -1; } if (context_range_set(ccon, range)) { log_it(u->name, getpid(), "context_range_set FAILED for MLS_LEVEL", range, 0); context_free(ccon); return -1; } if (!(*ucontextp = context_str(ccon))) { log_it(u->name, getpid(), "context_str FAILED for MLS_LEVEL", range, 0); context_free(ccon); return -1; } if (!(*ucontextp = strdup(*ucontextp))) { log_it(u->name, getpid(), "strdup FAILED for MLS_LEVEL", range, 0); return -1; } context_free(ccon); } else if (!u->scontext) { /* cron_change_selinux_range() deals with this */ return 0; } else if (!(*ucontextp = strdup(u->scontext))) { log_it(u->name, getpid(), "strdup FAILED for MLS_LEVEL", range, 0); return -1; } return 0; } #endif #ifdef WITH_SELINUX static int cron_change_selinux_range(user * u, security_context_t ucontext) { char *msg = NULL; if (is_selinux_enabled() <= 0) return 0; if (u->scontext == NULL) { if (security_getenforce() > 0) { log_it(u->name, getpid(), "NULL security context for user", "", 0); return -1; } else { log_it(u->name, getpid(), "NULL security context for user, " "but SELinux in permissive mode, continuing", "", 0); return 0; } } if (!ucontext || strcmp(u->scontext, ucontext)) { if (!cron_authorize_range(u->scontext, ucontext)) { if (security_getenforce() > 0) { # ifdef WITH_AUDIT if (asprintf(&msg, "cron: Unauthorized MLS range acct=%s new_scontext=%s old_scontext=%s", u->name, (char *) ucontext, u->scontext) >= 0) { int audit_fd = audit_open(); audit_log_user_message(audit_fd, AUDIT_USER_ROLE_CHANGE, msg, NULL, NULL, NULL, 0); close(audit_fd); free(msg); } # endif if (asprintf (&msg, "Unauthorized range in %s for user range in %s", (char *) ucontext, u->scontext) >= 0) { log_it(u->name, getpid(), "ERROR", msg, 0); free(msg); } return -1; } else { if (asprintf (&msg, "Unauthorized range in %s for user range in %s," " but SELinux in permissive mod, continuing", (char *) ucontext, u->scontext) >= 0) { log_it(u->name, getpid(), "WARNING", msg, 0); free(msg); } } } } if (setexeccon(ucontext) < 0) { if (security_getenforce() > 0) { if (asprintf (&msg, "Could not set exec or keycreate context to %s for user", (char *) ucontext) >= 0) { log_it(u->name, getpid(), "ERROR", msg, 0); free(msg); } return -1; } else { if (asprintf (&msg, "Could not set exec context to %s for user," " but SELinux in permissive mode, continuing", (char *) ucontext) >= 0) { log_it(u->name, getpid(), "WARNING", msg, 0); free(msg); } return 0; } } return 0; } #endif #ifdef WITH_SELINUX int get_security_context(const char *name, int crontab_fd, security_context_t * rcontext, const char *tabname) { security_context_t scontext = NULL; security_context_t file_context = NULL; security_context_t rawcontext=NULL; context_t current_context = NULL; int retval; char *current_context_str = NULL; char *seuser = NULL; char *level = NULL; *rcontext = NULL; if (is_selinux_enabled() <= 0) return 0; if (name != NULL) { if (getseuserbyname(name, &seuser, &level) < 0) { log_it(name, getpid(), "getseuserbyname FAILED", name, 0); return security_getenforce() > 0 ? -1 : 0; } retval = get_default_context_with_level(seuser, level, NULL, &scontext); } else { const char *current_user, *current_role; if (getcon(¤t_context_str) < 0) { log_it(name, getpid(), "getcon FAILED", "", 0); return (security_getenforce() > 0); } current_context = context_new(current_context_str); if (current_context == NULL) { log_it(name, getpid(), "context_new FAILED", current_context_str, 0); freecon(current_context_str); return (security_getenforce() > 0); } current_user = context_user_get(current_context); current_role = context_role_get(current_context); retval = get_default_context_with_rolelevel(current_user, current_role, level, NULL, &scontext); freecon(current_context_str); context_free(current_context); } if (selinux_trans_to_raw_context(scontext, &rawcontext) == 0) { freecon(scontext); scontext = rawcontext; } free(seuser); free(level); if (retval) { if (security_getenforce() > 0) { log_it(name, getpid(), "No SELinux security context", tabname, 0); return -1; } else { log_it(name, getpid(), "No security context but SELinux in permissive mode, continuing", tabname, 0); return 0; } } if (fgetfilecon(crontab_fd, &file_context) < OK) { if (security_getenforce() > 0) { log_it(name, getpid(), "getfilecon FAILED", tabname, 0); freecon(scontext); return -1; } else { log_it(name, getpid(), "getfilecon FAILED but SELinux in permissive mode, continuing", tabname, 0); *rcontext = scontext; return 0; } } if (!cron_authorize_context(scontext, file_context)) { char *msg=NULL; if (asprintf(&msg, "Unauthorized SELinux context=%s file_context=%s", (char *) scontext, file_context) >= 0) { log_it(name, getpid(), msg, tabname, 0); free(msg); } else { log_it(name, getpid(), "Unauthorized SELinux context", tabname, 0); } freecon(scontext); freecon(file_context); if (security_getenforce() > 0) { return -1; } else { log_it(name, getpid(), "SELinux in permissive mode, continuing", tabname, 0); return 0; } } freecon(file_context); *rcontext = scontext; return 0; } #endif #ifdef WITH_SELINUX void free_security_context(security_context_t * scontext) { if (*scontext != NULL) { freecon(*scontext); *scontext = NULL; } } #endif #ifdef WITH_SELINUX int crontab_security_access(void) { int selinux_check_passwd_access = -1; if (is_selinux_enabled() > 0) { security_context_t user_context; if (getprevcon_raw(&user_context) == 0) { security_class_t passwd_class; access_vector_t crontab_bit; struct av_decision avd; int retval = 0; passwd_class = string_to_security_class("passwd"); if (passwd_class == 0) { fprintf(stderr, "Security class \"passwd\" is not defined in the SELinux policy.\n"); retval = -1; } if (retval == 0) { crontab_bit = string_to_av_perm(passwd_class, "crontab"); if (crontab_bit == 0) { fprintf(stderr, "Security av permission \"crontab\" is not defined in the SELinux policy.\n"); retval = -1; } } if (retval == 0) retval = security_compute_av_raw(user_context, user_context, passwd_class, crontab_bit, &avd); if ((retval == 0) && ((crontab_bit & avd.allowed) == crontab_bit)) { selinux_check_passwd_access = 0; } freecon(user_context); } if (selinux_check_passwd_access != 0 && security_getenforce() == 0) selinux_check_passwd_access = 0; return selinux_check_passwd_access; } return 0; } #endif /* Build up the job environment from the PAM environment plus the * crontab environment */ static char **build_env(char **cronenv) { char **jobenv; #ifdef WITH_PAM char *cronvar; int count = 0; if (pamh == NULL || (jobenv=pam_getenvlist(pamh)) == NULL) { #endif jobenv = env_copy(cronenv); if (jobenv == NULL) log_it("CRON", getpid(), "ERROR", "Initialization of cron environment variables failed", 0); return jobenv; #ifdef WITH_PAM } /* Now add the cron environment variables. Since env_set() * overwrites existing variables, this will let cron's * environment settings override pam's */ while ((cronvar = cronenv[count++])) { if (!(jobenv = env_set(jobenv, cronvar))) { log_it("CRON", getpid(), "Setting Cron environment variable failed", cronvar, 0); return NULL; } } return jobenv; #endif } /* int in_file(const char *string, FILE *file, int error) * return TRUE if one of the lines in file matches string exactly, * FALSE if no lines match, and error on error. */ static int in_file(const char *string, FILE * file, int error) { char line[MAX_TEMPSTR]; char *endp; if (fseek(file, 0L, SEEK_SET)) return (error); while (fgets(line, MAX_TEMPSTR, file)) { if (line[0] != '\0') { endp = &line[strlen(line) - 1]; if (*endp != '\n') return (error); *endp = '\0'; if (0 == strcmp(line, string)) return (TRUE); } } if (ferror(file)) return (error); return (FALSE); } /* int allowed(const char *username, const char *allow_file, const char *deny_file) * returns TRUE if (allow_file exists and user is listed) * or (deny_file exists and user is NOT listed). * root is always allowed. */ int allowed(const char *username, const char *allow_file, const char *deny_file) { FILE *fp; int isallowed; char buf[128]; if (getuid() == 0) return TRUE; isallowed = FALSE; if ((fp = fopen(allow_file, "r")) != NULL) { isallowed = in_file(username, fp, FALSE); fclose(fp); if ((getuid() == 0) && (!isallowed)) { snprintf(buf, sizeof (buf), "root used -u for user %s not in cron.allow", username); log_it("crontab", getpid(), "warning", buf, 0); isallowed = TRUE; } } else if ((fp = fopen(deny_file, "r")) != NULL) { isallowed = !in_file(username, fp, FALSE); fclose(fp); if ((getuid() == 0) && (!isallowed)) { snprintf(buf, sizeof (buf), "root used -u for user %s in cron.deny", username); log_it("crontab", getpid(), "warning", buf, 0); isallowed = TRUE; } } #ifdef WITH_AUDIT if (isallowed == FALSE) { int audit_fd = audit_open(); audit_log_user_message(audit_fd, AUDIT_USER_START, "cron deny", NULL, NULL, NULL, 0); close(audit_fd); } #endif return (isallowed); }