/* * Copyright 2009,2010 Red Hat, Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, and the entire permission notice in its entirety, * including the disclaimer of warranties. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define FCC_PREFIX "FILE:" #define DEFAULT_RETRY_TIME 300 /* A ccache that we need to maintain. */ static struct monger_entry { char *user; /* name of ccache owner */ uid_t uid; /* UID of ccache owner. */ gid_t gid; /* GID of ccache owner. */ char *keytab; /* Name of keytab. */ char *principal_name; /* Client name. */ char *fccache; /* Name of existing ccache, or NULL. */ char *fccache_pattern; /* Pattern to use for naming ccache. */ krb5_timestamp when; /* When the creds in the ccache expire. */ } **entries, **cleanup; /* My own user name. */ char *startup_user; /* Time to reload. */ static int reload = 0; /* Time to exit. */ static int quit = 0; /* Configuration directory. */ static const char *configdir = CONFIG_DIR; static void log_err(int level, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (getppid() == 1) { vsyslog(LOG_AUTHPRIV | level, fmt, ap); } else { vfprintf(stderr, fmt, ap); } va_end(ap); } /* Read the configuration and return a list of ccache names. */ static struct monger_entry ** entries_read(void) { char buf[LINE_MAX], *p, *uids, *keytab, *principal, *fccache_pattern; char fccache[PATH_MAX]; struct monger_entry **list, **tmp, *entry; struct passwd *pwd; uid_t uid; FILE *fp; int n_entries; DIR *dir; struct dirent *ent; list = NULL; n_entries = 0; dir = opendir(configdir); if (dir == NULL) { return NULL; } while ((ent = readdir(dir)) != NULL) { if (strchr(".#~", ent->d_name[0]) != NULL) { continue; } snprintf(buf, sizeof(buf), "%s/%.*s", configdir, NAME_MAX, ent->d_name); if (strlen(buf) > 0) { p = buf + strlen(buf) - 1; if (strchr("#~", *p) != NULL) { continue; } if (strcmp(buf + strlen(buf) - 8, ".rpmsave") == 0) { continue; } if (strcmp(buf + strlen(buf) - 8, ".rpmorig") == 0) { continue; } if (strcmp(buf + strlen(buf) - 7, ".rpmnew") == 0) { continue; } } fp = fopen(buf, "r"); if (fp != NULL) { while (fgets(buf, sizeof(buf), fp) != NULL) { /* Trim end-of-line characters. */ buf[strcspn(buf, "\r\n")] = '\0'; /* Skip comments. */ if (strchr("#:", buf[0]) != NULL) { continue; } /* Trim end-of-line characters. */ uids = buf; /* Skip ahead to the keytab name and close the * user name. */ keytab = strchr(uids, ':'); if (keytab == NULL) { log_err(LOG_ERR, "no keytab name field\n"); continue; } *keytab++ = '\0'; /* Skip ahead to the client principal name and * close the keytab name. */ principal = strchr(keytab, ':'); if (principal == NULL) { log_err(LOG_ERR, "no principal name field\n"); continue; } *principal++ = '\0'; /* Skip ahead to the ccache name pattern and * close the principal name. */ fccache_pattern = strchr(principal, ':'); if (fccache_pattern != NULL) { *fccache_pattern++ = '\0'; /* Close the principal name. */ p = strchr(fccache_pattern, ':'); if (p != NULL) { *p++ = '\0'; } } /* Now figure out the UID/GID. */ uid = (uid_t) strtol(uids, &p, 10); if ((p == NULL) || (*p != '\0')) { /* Not a number, so treat it as a user * name. */ pwd = getpwnam(uids); if (pwd == NULL) { log_err(LOG_ERR, "unknown user " "\"%s\"\n", uids); continue; } } else { /* Treat it as a number. */ pwd = getpwuid(uid); if (pwd == NULL) { log_err(LOG_ERR, "unknown user %lu\n", (unsigned long) uid); continue; } } /* Now go back and figure out the ccache name * pattern. If it's empty, assume that we want * a unique file in the temporary directory. */ if ((fccache_pattern == NULL) || (strlen(fccache_pattern) == 0)) { snprintf(fccache, sizeof(fccache), "FILE:%s/krb5cc_%lu_XXXXXX", getenv("TMPDIR") ?: "/tmp", (unsigned long) uid); } else { /* If the user supplied us with a typed * ccache name, just use it as-is. */ if (strncmp(fccache_pattern, FCC_PREFIX, strlen(FCC_PREFIX)) == 0) { snprintf(fccache, sizeof(fccache), "%s", fccache_pattern); } else { /* Mark it as a file-based * ccache, and take the * filename as-is. */ snprintf(fccache, sizeof(fccache), "FILE:%s", fccache_pattern); } } fccache_pattern = fccache; /* Make space for this entry in the list. */ tmp = realloc(list, sizeof(*list) * (n_entries + 2)); if (tmp == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } list = tmp; /* Allocate this entry. */ list[n_entries] = malloc(sizeof(**list)); if (list[n_entries] == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } entry = list[n_entries]; /* NULL-terminate the list. */ n_entries++; list[n_entries] = NULL; /* Initialize this entry. */ memset(entry, 0, sizeof(*entry)); entry->user = strdup(pwd->pw_name); if (entry->user == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } entry->uid = pwd->pw_uid; entry->gid = pwd->pw_gid; entry->keytab = strdup(keytab); if (entry->keytab == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } entry->principal_name = strdup(principal); if (entry->principal_name == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } entry->fccache_pattern = strdup(fccache_pattern); if (entry->fccache_pattern == NULL) { log_err(LOG_ERR, "out of memory\n"); break; } } fclose(fp); } } closedir(dir); return list; } static void free_unparsed_name(krb5_context ctx, char *unparsed) { #ifdef HAVE_KRB5_FREE_UNPARSED_NAME krb5_free_unparsed_name(ctx, unparsed); #else free(unparsed); #endif } /* Do the heavy lifting. */ static void entries_poll(void) { int i, j, fd, len, need_rename; krb5_error_code ret; krb5_context ctx; krb5_creds creds; krb5_keytab keytab; krb5_ccache ccache; krb5_principal client; krb5_get_init_creds_opt *gic_opts, gic_opts_st; char host[LINE_MAX], fccache[PATH_MAX + strlen(FCC_PREFIX) + 1]; char *principal_name, *oldfile; /* Figure out the client hostname. */ memset(host, '\0', sizeof(host)); if (gethostname(host, sizeof(host) - 1) != 0) { log_err(LOG_ERR, "error determining hostname: %s\n", strerror(errno)); snprintf(host, sizeof(host), "localhost"); } ctx = NULL; gic_opts = NULL; if ((i = krb5_init_context(&ctx)) != 0) { log_err(LOG_ERR, "error initializing Kerberos: %s\n", error_message(i)); } else { /* Initialize the get_init_creds options. */ #ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_ALLOC if (krb5_get_init_creds_opt_alloc(ctx, &gic_opts) != 0) { gic_opts = NULL; } #else gic_opts = &gic_opts_st; #endif #ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CANONICALIZE /* If the KDC has a better idea of the client's name, believe * it. */ if (gic_opts != NULL) { #ifdef KRB5_GET_INIT_CREDS_OPT_SET_CANONICALIZE_TAKES_3_ARGS krb5_get_init_creds_opt_set_canonicalize(ctx, gic_opts, 1); #else krb5_get_init_creds_opt_set_canonicalize(gic_opts, 1); #endif } #endif /* Walk the list of entries. */ for (i = 0; (entries != NULL) && (entries[i] != NULL); i++) { if (setreuid(0, 0) != 0) { log_err(LOG_CRIT, "error resetting uid\n"); _exit(1); } if (setregid(0, 0) != 0) { log_err(LOG_CRIT, "error resetting gid\n"); _exit(1); } if (initgroups(startup_user, 0) != 0) { log_err(LOG_CRIT, "error resetting " "supplemental group list\n"); _exit(1); } log_err(LOG_DEBUG, "[uid=%ld, keytab=%s, client=%s, ccache=%s]\n", (unsigned long) entries[i]->uid, entries[i]->keytab && strlen(entries[i]->keytab) ? entries[i]->keytab : "", entries[i]->principal_name, entries[i]->fccache_pattern); /* Drop privileges to the target user. */ if (initgroups(entries[i]->user, entries[i]->gid) != 0) { log_err(LOG_CRIT, "error resetting " "supplemental group list for \"%s\"\n", entries[i]->user); break; } if (setegid(entries[i]->gid) != 0) { log_err(LOG_CRIT, "error resetting egid for \"%s\"\n", entries[i]->user); break; } if (seteuid(entries[i]->uid) != 0) { log_err(LOG_CRIT, "error resetting euid for \"%s\"\n", entries[i]->user); break; } /* Open the keytab. */ keytab = NULL; if ((entries[i]->keytab == NULL) || (strlen(entries[i]->keytab) == 0)) { ret = krb5_kt_default(ctx, &keytab); } else { ret = krb5_kt_resolve(ctx, entries[i]->keytab, &keytab); } if (ret != 0) { log_err(LOG_ERR, "error resolving keytab: %s\n", error_message(ret)); continue; } /* If the principal name ends with "/", append the * local host name. Parse it. */ client = NULL; len = strlen(entries[i]->principal_name); if ((len > 0) && (entries[i]->principal_name[len - 1] == '/')) { principal_name = malloc(len + strlen(host) + 1); if (principal_name == NULL) { log_err(LOG_ERR, "out of memory\n"); krb5_kt_close(ctx, keytab); continue; } strcpy(principal_name, entries[i]->principal_name); strcat(principal_name, host); } else { principal_name = strdup(entries[i]->principal_name); if (principal_name == NULL) { log_err(LOG_ERR, "out of memory\n"); krb5_kt_close(ctx, keytab); continue; } } ret = krb5_parse_name(ctx, principal_name, &client); if (ret != 0) { log_err(LOG_ERR, "error parsing client \"%s\": %s\n", principal_name, error_message(ret)); free(principal_name); krb5_kt_close(ctx, keytab); continue; } free(principal_name); /* Now "unparse" it. */ principal_name = NULL; ret = krb5_unparse_name(ctx, client, &principal_name); if (ret != 0) { log_err(LOG_ERR, "error unparsing name: %s\n", error_message(ret)); krb5_kt_close(ctx, keytab); krb5_free_principal(ctx, client); continue; } /* Okay, we have everything we need. Give it a shot. */ memset(&creds, 0, sizeof(creds)); ret = krb5_get_init_creds_keytab(ctx, &creds, client, keytab, 0, NULL, gic_opts); if (ret != 0) { log_err(LOG_ERR, "error getting creds for %s: %s\n", principal_name, error_message(ret)); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); continue; } /* Create a temporary file to hold the creds. */ snprintf(fccache, sizeof(fccache), "%s", entries[i]->fccache_pattern); need_rename = 0; fd = mkstemp(fccache + strlen(FCC_PREFIX)); /* If we got EINVAL, then it's not a pattern, so create * a pattern and note that we'll need to rename it over * the "real" name when we're done setting it up. */ if ((fd == -1) && (errno == EINVAL)) { need_rename++; snprintf(fccache, sizeof(fccache), "%sXXXXXX", entries[i]->fccache_pattern); fd = mkstemp(fccache + strlen(FCC_PREFIX)); } if (fd == -1) { log_err(LOG_ERR, "error creating temporary ccache\n"); krb5_free_cred_contents(ctx, &creds); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); continue; } close(fd); /* Open the ccache. */ ccache = NULL; ret = krb5_cc_resolve(ctx, fccache, &ccache); if (ret != 0) { log_err(LOG_ERR, "error opening temporary ccache: %s\n", error_message(ret)); unlink(fccache + strlen(FCC_PREFIX)); krb5_free_cred_contents(ctx, &creds); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); continue; } /* Write the client's name to the ccache. */ ret = krb5_cc_initialize(ctx, ccache, client); if (ret != 0) { log_err(LOG_ERR, "error initializing temporary " "ccache: %s\n", error_message(ret)); krb5_cc_close(ctx, ccache); unlink(fccache + strlen(FCC_PREFIX)); krb5_free_cred_contents(ctx, &creds); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); continue; } /* Store the TGT. */ ret = krb5_cc_store_cred(ctx, ccache, &creds); if (ret != 0) { log_err(LOG_ERR, "error storing creds: %s\n", error_message(ret)); krb5_cc_close(ctx, ccache); unlink(fccache + strlen(FCC_PREFIX)); krb5_free_cred_contents(ctx, &creds); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); continue; } /* Done with the ccache structure. */ krb5_cc_close(ctx, ccache); /* If the configuration doesn't want a unique ccache * name, overwrite the destination with the contents of * this temporary file. */ if (need_rename) { if (rename(fccache + strlen(FCC_PREFIX), entries[i]->fccache_pattern + strlen(FCC_PREFIX)) == 0) { entries[i]->fccache = entries[i]->fccache_pattern; entries[i]->when = creds.times.endtime; } else { log_err(LOG_NOTICE, "error renaming \"%s\" " "to \"%s\"\n", fccache + strlen(FCC_PREFIX), entries[i]->fccache_pattern + strlen(FCC_PREFIX)); } } else { /* Remove the old file, and record this one. */ oldfile = entries[i]->fccache; entries[i]->fccache = strdup(fccache); if (entries[i]->fccache == NULL) { /* Better luck next time, which is * soon. */ entries[i]->fccache = oldfile; entries[i]->when = DEFAULT_RETRY_TIME; unlink(fccache + strlen(FCC_PREFIX)); } else { /* Get rid of a previously-initialized * file. */ if (oldfile != NULL) { unlink(oldfile + strlen(FCC_PREFIX)); free(oldfile); } /* Record the renewal time. */ entries[i]->when = creds.times.endtime; } } log_err(LOG_DEBUG, "saved creds for \"%s\" to \"%s\"\n", principal_name, entries[i]->fccache); krb5_free_cred_contents(ctx, &creds); krb5_kt_close(ctx, keytab); free_unparsed_name(ctx, principal_name); krb5_free_principal(ctx, client); } if (setreuid(0, 0) != 0) { log_err(LOG_CRIT, "error resetting uid\n"); _exit(1); } if (setregid(0, 0) != 0) { log_err(LOG_CRIT, "error resetting gid\n"); _exit(1); } if (initgroups(startup_user, 0) != 0) { log_err(LOG_CRIT, "error resetting " "supplemental group list\n"); _exit(1); } #ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_FREE if (gic_opts != &gic_opts_st) { #ifdef KRB5_GET_INIT_CREDS_OPT_ALLOC_FREE_TAKES_2_ARGS krb5_get_init_creds_opt_free(ctx, gic_opts); #else krb5_get_init_creds_opt_free(gic_opts); #endif } #endif krb5_free_context(ctx); } /* Walk the list of to-be-cleaned-up entries. */ for (i = 0; (cleanup != NULL) && (cleanup[i] != NULL); i++) { /* Remove the ccache, if there is one. */ if (cleanup[i]->fccache != NULL) { for (j = 0; (entries != NULL) && (entries[j] != NULL); j++) { /* ... unless it's being used elsewhere. */ if ((entries[j]->fccache != NULL) && (strcmp(cleanup[i]->fccache, entries[j]->fccache) == 0)) { break; } } if ((entries == NULL) || (entries[j] == NULL)) { log_err(LOG_DEBUG, "removing \"%s\"\n", cleanup[i]->fccache); unlink(cleanup[i]->fccache + strlen(FCC_PREFIX)); } } free(cleanup[i]->fccache); /* Free the pattern if it's not a plain filename. */ if (cleanup[i]->fccache != cleanup[i]->fccache_pattern) { free(cleanup[i]->fccache_pattern); } /* Finish up. */ free(cleanup[i]->principal_name); free(cleanup[i]->keytab); free(cleanup[i]); } free(cleanup); cleanup = NULL; } static void entries_reload(void) { struct monger_entry **list, **cleanup; list = entries_read(); cleanup = entries; entries = list; entries_poll(); } static void entries_unload(void) { cleanup = entries; entries = NULL; entries_poll(); } static int entries_waittime(void) { int i; krb5_timestamp last; for (i = 0, last = 0; (entries != NULL) && (entries[i] != NULL); i++) { if (entries[i]->fccache != NULL) { if ((last == 0) || (entries[i]->when < last)) { last = entries[i]->when; } } } if (last == 0) { i = DEFAULT_RETRY_TIME; } else { i = (last - time(NULL)) * 9 / 10; } return i; } static void setreload(int signum) { reload++; } static void setquit(int signum) { quit++; } int main(int argc, char **argv) { time_t howlong; char *pidfile; int c, nofork; FILE *fp; struct passwd *pwd; nofork = 0; pidfile = NULL; while ((c = getopt(argc, argv, "c:np:")) != -1) { switch (c) { case 'c': configdir = optarg; break; case 'n': nofork++; break; case 'p': pidfile = optarg; break; case 'h': default: printf("Usage: %s [-c configdir] [-n] [-p pidfile]\n", strchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0]); _exit(1); break; } } /* Load the configuration. */ entries = entries_read(); if (entries == NULL) { log_err(LOG_ERR, "nothing configured\n"); return 1; } /* Figure out our name. */ if ((pwd = getpwuid(getuid())) == NULL) { log_err(LOG_ERR, "unknown user\n"); return 1; } startup_user = strdup(pwd->pw_name); if (startup_user == NULL) { log_err(LOG_ERR, "out of memory\n"); return 1; } /* Go background. */ if (!nofork) { if (daemon(0, 0) != 0) { log_err(LOG_ERR, "error going to background: %s\n", strerror(errno)); return 1; } } /* Write the pidfile. */ fp = fopen(pidfile, "w"); if (fp != NULL) { fprintf(fp, "%lu\n", (long unsigned) (getpid())); fclose(fp); } /* Make sure we don't reinitialize the first time through. */ reload = 0; while (quit == 0) { /* Either (re-)initialize, or don't. */ if (reload) { /* Load or reload configuration. */ reload = 0; entries_reload(); /* If there are no entries now, bail. */ if (entries == NULL) { break; } } else { /* Just run through the entries. */ entries_poll(); } /* Figure out how long to wait. */ howlong = entries_waittime(); log_err(LOG_DEBUG, "waiting for %ld seconds\n", (long) howlong); signal(SIGHUP, setreload); signal(SIGINT, setquit); signal(SIGTERM, setquit); signal(SIGQUIT, setquit); howlong = sleep(howlong); }; /* Clean up. */ entries_unload(); if (pidfile != NULL) { log_err(LOG_DEBUG, "removing \"%s\"\n", pidfile); unlink(pidfile); } return 0; }