cronie/src/security.c

749 lines
18 KiB
C

/* security.c
*
* Implement Red Hat crond security context transitions
*
* Jason Vas Dias <jvdias@redhat.com> 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 <errno.h>
#include <grp.h>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "cronie_common.h"
#include "funcs.h"
#include "globals.h"
#include "macros.h"
#ifdef WITH_PAM
# include <security/pam_appl.h>
#endif
#ifdef WITH_SELINUX
# include <selinux/selinux.h>
# include <selinux/context.h>
# include <selinux/get_context_list.h>
#endif
#ifdef WITH_AUDIT
# include <libaudit.h>
#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 %lu\n", 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(&current_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);
}