From 94f7d494b460aaa27e4b54e1607fb2c2c6eb3774 Mon Sep 17 00:00:00 2001 From: Steve Traylen Date: Thu, 21 Dec 2023 11:30:58 +0100 Subject: [PATCH] Initial commit --- .gitignore | 8 ++ LICENSE | 1 + Makefile | 21 ++++ README.md | 39 +++++++ man/pam_krb5_cc_move.8 | 65 +++++++++++ pamtester/krb5_cc_move | 2 + src/pam_krb5_cc_move.c | 238 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 man/pam_krb5_cc_move.8 create mode 100644 pamtester/krb5_cc_move create mode 100644 src/pam_krb5_cc_move.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7483436 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +CMakeCache.txt +build +CMakeFiles/ +cmake_install.cmake +pam_krb5_cc_move.so +*.tar.gz +*.tgz +src/*.o diff --git a/LICENSE b/LICENSE index 261eeb9..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4a5e3a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +CC=gcc +CFLAGS="-fPIC -fno-stack-protector" +INSTALLDIR=/usr/lib64/security +MANDIR=/usr/share/man + +all: pam_krb5_cc_move.so + +src/pam_krb5_cc_move.o: src/pam_krb5_cc_move.c + gcc $(EXTRA_CFLAGS) -fPIC -fno-stack-protector -c src/pam_krb5_cc_move.c -o src/pam_krb5_cc_move.o + +pam_krb5_cc_move.so: src/pam_krb5_cc_move.o + gcc $(EXTRA_CFLAGS) -shared -o pam_krb5_cc_move.so src/pam_krb5_cc_move.o -lpam -lkrb5 + +install: all + install -d $(DESTDIR)$(MANDIR)/man8 + install -p -m 0644 man/pam_krb5_cc_move.8 $(DESTDIR)$(MANDIR)/man8/pam_krb5_cc_move.8 + install -p -m 0755 pam_krb5_cc_move.so $(DESTDIR)/$(INSTALLDIR)/pam_krb5_cc_move.so + +clean: + rm -rf build/ $(TARFILE) src/*.o *.so + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7b7b25 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# pam_krb5_cc_move + +Pam session module to move a Kerberos credential cache from one location to another. + +Default locations for source and destination are as below + +``` +session optional pam_krb5_cc_move.so source=FILE:/tmp/source destination=FILE:/tmp/destination debug +``` + +if the source cc is missing this is NOT considered an error. + +## Use case with SSHD and Kerberos + +With SSHD and Kerberos authentication it is not possible to create a keytab at `/run/user/` since this directory +does not exist when SSHD does the delegation. The workaround is have SSHD delegate to KEYRING and then this pam module +can move the credential to the correct location. + +* Configure `/etc/krb5.conf` with cache of `FILE:/run/user/%{uid}/krb5cc`. +* Run sshd with an environment of `KRB5_CONFIG=/etc/krb5_sshd.conf` to use a custom cache location for SSHD only. + +```ini +# File /etc/krb5_sshd.conf +[libdefaults] + default_ccache_name = KEYRING:persistent:%{uid} + +include /etc/krb5.conf +``` + +* Use two pam modules to create $XDG_RUNTIME_DIR and then move the credential there with each login. + +``` +session optional pam_xdg_runtime_dir.so +session optional pam_krb5_cc_move.so +``` + +Default source and destination in pam_krb5_cc_move is `KEYRING:persistant:%{uid}` and `FILE:/run/user/%{uid}/krb5cc` + + diff --git a/man/pam_krb5_cc_move.8 b/man/pam_krb5_cc_move.8 new file mode 100644 index 0000000..5d4868d --- /dev/null +++ b/man/pam_krb5_cc_move.8 @@ -0,0 +1,65 @@ +.TH PAM_KRB5_CC_MOVE 8 "January 2024" "Version 1.0" "pam_krb5_cc_move Manual" + +.SH NAME +pam_krb5_cc_move \- Pluggable Authentication Module for Kerberos Credential Cache Migration + +.SH SYNOPSIS +.B pam_krb5_cc_move.so + +.SH DESCRIPTION +pam_krb5_cc_move is a PAM (Pluggable Authentication Module) that facilitates the migration of Kerberos credential caches. It is designed to be called during the PAM session phase, specifically in the opening session. + +.SH OPTIONS +.TP +.B source= +Specifies the source credential cache format. The default is "KEYRING:persistent:%{uid}". + +.TP +.B destination= +Specifies the destination credential cache format. The default is "FILE:/run/user/%{uid}/krb5cc". + +.TP +.B debug +Enables debug mode, providing additional log information. + +.SH RETURN VALUES +.PP +.BR PAM_SUCCESS +Successful completion of the credential cache migration. + +.PP +.BR PAM_SESSION_ERR +Error during the PAM session phase. + +.PP +.BR PAM_AUTH_ERR +Authentication-related error. + +.SH EXAMPLES +The following PAM configuration demonstrates the usage of pam_krb5_cc_move in the /etc/pam.d/common-session file: + +.B +session required pam_krb5_cc_move.so source=KEYRING:persistent:%{uid} destination=FILE:/tmp/krb5cc debug + +.SH FILES +.TP +/etc/pam.d/common-session +PAM configuration file for session management. + +.SH SEE ALSO +.BR pam.conf (5), krb5_ccache (3) + +.SH BUGS +None known. + +.SH AUTHOR +Your Name + +.SH COPYRIGHT +Copyright (c) 2024 Your Organization. All rights reserved. + +.SH DISCLAIMER +This module is provided "as is" without any warranty. + +.SH NOTES + diff --git a/pamtester/krb5_cc_move b/pamtester/krb5_cc_move new file mode 100644 index 0000000..ce08ea0 --- /dev/null +++ b/pamtester/krb5_cc_move @@ -0,0 +1,2 @@ +# Just use for testing +session optional pam_krb5_cc_move.so debug source=FILE:/tmp/source destination=FILE:/tmp/destination diff --git a/src/pam_krb5_cc_move.c b/src/pam_krb5_cc_move.c new file mode 100644 index 0000000..374347c --- /dev/null +++ b/src/pam_krb5_cc_move.c @@ -0,0 +1,238 @@ +// Copyright 2024 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_LOG_LEVEL LOG_DEBUG + +// Function to replace %{uid} with the actual UID +void replaceSubstring(char *str, const char *search, const char *replace) { + char *ptr = strstr(str, search); + + while (ptr != NULL) { + int searchLen = strlen(search); + int replaceLen = strlen(replace); + int tailLen = strlen(ptr + searchLen); + + memmove(ptr + replaceLen, ptr + searchLen, tailLen + 1); + memcpy(ptr, replace, replaceLen); + + ptr = strstr(ptr + replaceLen, search); + } +} + +// Function to parse options with equal signs +void parseOptions(const char **argv, const char **source, const char **destination, int *debug) { + + for (int i = 0; argv[i] != NULL; ++i) { + if (strncmp(argv[i], "source=", 7) == 0) { + *source = &argv[i][7]; + } else if (strncmp(argv[i], "destination=", 12) == 0) { + *destination = &argv[i][12]; + } else if (strcmp(argv[i], "debug") == 0) { + *debug = 1; + } + } +} + +PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + const char *pam_user; + struct passwd *pwd; + krb5_context context; + krb5_principal principal; + krb5_ccache keyring_cache; + int ret; + + int debug = 0; + const char *source = "KEYRING:persistent:%{uid}"; + const char *destination = "FILE:/run/user/%{uid}/krb5cc"; + + // Parse options + parseOptions(argv, &source, &destination, &debug); + + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Debug mode enabled"); + } + + // Get the PAM_USER (user being authenticated) + ret = pam_get_user(pamh, &pam_user, NULL); + if (ret != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "Error getting PAM_USER: %s", pam_strerror(pamh, ret)); + return PAM_SESSION_ERR; + } + + // Get user information using getpwnam + pwd = getpwnam(pam_user); + if (pwd == NULL) { + pam_syslog(pamh, LOG_ERR, "Error getting user information for PAM_USER: %s", pam_user); + return PAM_SESSION_ERR; + } + char uidstr[20]; + sprintf(uidstr, "%d", pwd->pw_uid); + + // Fork as the user and do the migration + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Starting fork as uid %d", pwd->pw_uid); + } + + pid_t child_pid; + child_pid = fork(); + if (child_pid == -1) { + // Fork failed + pam_syslog(pamh, LOG_ERR, "Fork failed"); + return PAM_AUTH_ERR; + } + + if (child_pid == 0) { + ret = setgid(pwd->pw_gid); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error calling setgid to %d", pwd->pw_gid); + exit(EXIT_FAILURE); + } + + ret = setgroups(1, &pwd->pw_gid); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error calling setgroups to %d", pwd->pw_gid); + exit(EXIT_FAILURE); + } + + ret = setuid(pwd->pw_uid); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error calling setuid to %d", pwd->pw_uid); + exit(EXIT_FAILURE); + } + + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Forked off as user %d", pwd->pw_uid); + } + + // Initialize Kerberos context + ret = krb5_init_context(&context); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error initializing Kerberos context: %s", krb5_get_error_message(context, ret)); + exit(EXIT_FAILURE); + } + + // Construct KEYRING cache name with UID + char keyring_cache_name[1024]; + snprintf(keyring_cache_name, sizeof(keyring_cache_name), "%s", source); + replaceSubstring(keyring_cache_name, "%{uid}", uidstr); + + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Source Credential Cache %s", keyring_cache_name); + } + + // Open KEYRING cache + ret = krb5_cc_resolve(context, keyring_cache_name, &keyring_cache); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error resolving KEYRING cache: %s", krb5_get_error_message(context, ret)); + krb5_free_context(context); + exit(EXIT_FAILURE); + } + + // Get principal from KEYRING cache + ret = krb5_cc_get_principal(context, keyring_cache, &principal); + if (ret != 0) { + pam_syslog(pamh, LOG_INFO, "No delegate kerberos principal in KEYRING cache: %s", krb5_get_error_message(context, ret)); + krb5_cc_close(context, keyring_cache); + krb5_free_context(context); + exit(EXIT_SUCCESS); + } + + // Log the principal name + char *principal_name = NULL; + krb5_unparse_name(context, principal, &principal_name); + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Principal name: %s", principal_name); + } + free(principal_name); + + // Specify the path for the cache + char file_cache_name[PATH_MAX]; + snprintf(file_cache_name, sizeof(file_cache_name), "%s", destination); + replaceSubstring(file_cache_name, "%{uid}", uidstr); + + // Resolve and initialize the cache + krb5_ccache file_cache; + ret = krb5_cc_resolve(context, file_cache_name, &file_cache); + if (ret == KRB5_FCC_NOFILE) { + // If the cache file doesn't exist, initialize a new cache + ret = krb5_cc_initialize(context, file_cache, principal); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error initializing cache: %s", krb5_get_error_message(context, ret)); + krb5_free_principal(context, principal); + krb5_free_context(context); + exit(EXIT_FAILURE); + } + } else if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error resolving cache: %s", krb5_get_error_message(context, ret)); + krb5_free_principal(context, principal); + krb5_free_context(context); + exit(EXIT_FAILURE); + } + + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "File Cache Initialized at %s", file_cache_name); + } + + // Move credentials from KCM to file cache + ret = krb5_cc_move(context, keyring_cache, file_cache); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "Error moving credentials from KCM to file cache: %s", krb5_get_error_message(context, ret)); + krb5_cc_close(context, keyring_cache); + krb5_cc_close(context, file_cache); + krb5_free_principal(context, principal); + krb5_free_context(context); + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); + } else { + int status; + if (waitpid(child_pid, &status, 0) == -1) { + pam_syslog(pamh, LOG_ERR, "Wait failed"); + exit(EXIT_FAILURE); + } + + if (WIFEXITED(status)) { + if (debug) { + pam_syslog(pamh, DEBUG_LOG_LEVEL, "Child process exited with status %d", WEXITSTATUS(status)); + } + } else { + pam_syslog(pamh, LOG_ERR, "Child processes exited badly %d", WEXITSTATUS(status)); + return PAM_SESSION_ERR; + } + } + + return PAM_SUCCESS; +} + +PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + // Perform any cleanup if necessary + return PAM_SUCCESS; +}